From ab0fc98e00d2c4ff529946921a0a26b4c79eb7c7 Mon Sep 17 00:00:00 2001 From: Roblox Client Tracker Date: Wed, 7 Oct 2020 12:04:14 -0500 Subject: [PATCH] 0.451.0.412446 (Scripts) --- ...etFFlagHideLoadToastIfAnimationClipped.lua | 5 + .../Src/Components/DopeSheetController.lua | 4 +- .../UILibrary/_internal/Utils/Symbol.spec.lua | 4 +- .../Core/Components/ToolboxPlugin.lua | 23 +- .../Requests/ChangeMarketplaceTab.lua | 3 +- .../Core/Types/Category.lua | 12 +- .../Core/Util/InsertAsset.lua | 2 +- .../Framework/ContextServices/Theme.spec.lua | 7 + .../ContextServices/UILibraryWrapper.spec.lua | 35 +- .../StudioUI/TitledFrame/test.spec.lua | 2 +- .../Framework/StudioUI/createPluginWidget.lua | 19 +- .../Libs/Framework/Style/Stylizer.lua | 4 +- .../Framework/Style/getRawComponentStyle.lua | 1 + .../TestHelpers/provideMockContext.lua | 2 +- .../Libs/Framework/UI/Box/test.spec.lua | 2 +- .../Libs/Framework/UI/Button/test.spec.lua | 2 +- .../Libs/Framework/UI/Container/test.spec.lua | 2 +- .../Libs/Framework/UI/DropdownMenu.lua | 3 +- .../Framework/UI/FakeLoadingBar/test.spec.lua | 2 +- .../Libs/Framework/UI/Image/test.spec.lua | 2 +- .../UI/InstanceTreeView/InstanceTreeRow.lua | 11 +- .../Libs/Framework/UI/LinkText/test.spec.lua | 2 +- .../Framework/UI/LoadingBar/test.spec.lua | 2 +- .../Framework/UI/RadioButton/test.spec.lua | 2 +- .../Libs/Framework/UI/RoundBox/test.spec.lua | 2 +- .../Libs/Framework/UI/TextLabel/test.spec.lua | 2 +- .../Framework/UI/ToggleButton/test.spec.lua | 2 +- .../Libs/Framework/UI/Tooltip/test.spec.lua | 2 +- .../Libs/Framework/UI/TreeView/test.spec.lua | 2 +- .../Libs/Framework/Util.lua | 1 + .../Util/CrossPluginCommunication.lua | 6 +- .../Util/CrossPluginCommunication.spec.lua | 8 +- .../Libs/Framework/Util/Palette.spec.lua | 13 +- .../Libs/Framework/Util/Typecheck/t.lua | 2 + .../Libs/Framework/Util/getTestVariation.lua | 30 + .../Framework/Util/getTestVariation.spec.lua | 41 ++ .../UILibrary/_internal/Utils/Symbol.spec.lua | 4 +- .../AssetManager/Bin/main.server.lua | 26 +- .../Framework/ContextServices/Theme.spec.lua | 7 + .../ContextServices/UILibraryWrapper.spec.lua | 35 +- .../StudioUI/TitledFrame/test.spec.lua | 2 +- .../Framework/StudioUI/createPluginWidget.lua | 19 +- .../Packages/Framework/Style/Stylizer.lua | 4 +- .../Framework/Style/getRawComponentStyle.lua | 1 + .../TestHelpers/provideMockContext.lua | 2 +- .../Packages/Framework/UI/Box/test.spec.lua | 2 +- .../Framework/UI/Button/test.spec.lua | 2 +- .../Framework/UI/Container/test.spec.lua | 2 +- .../Packages/Framework/UI/DropdownMenu.lua | 3 +- .../Framework/UI/FakeLoadingBar/test.spec.lua | 2 +- .../Packages/Framework/UI/Image/test.spec.lua | 2 +- .../UI/InstanceTreeView/InstanceTreeRow.lua | 11 +- .../Framework/UI/LinkText/test.spec.lua | 2 +- .../Framework/UI/LoadingBar/test.spec.lua | 2 +- .../Framework/UI/RadioButton/test.spec.lua | 2 +- .../Framework/UI/RoundBox/test.spec.lua | 2 +- .../Framework/UI/TextLabel/test.spec.lua | 2 +- .../Framework/UI/ToggleButton/test.spec.lua | 2 +- .../Framework/UI/Tooltip/test.spec.lua | 2 +- .../Framework/UI/TreeView/test.spec.lua | 2 +- .../AssetManager/Packages/Framework/Util.lua | 1 + .../Util/CrossPluginCommunication.lua | 6 +- .../Util/CrossPluginCommunication.spec.lua | 8 +- .../Packages/Framework/Util/Palette.spec.lua | 13 +- .../Packages/Framework/Util/Typecheck/t.lua | 2 + .../Framework/Util/getTestVariation.lua | 30 + .../Framework/Util/getTestVariation.spec.lua | 41 ++ .../UILibrary/_internal/Utils/Symbol.spec.lua | 4 +- .../Src/Components/RecentlyImportedView.lua | 4 +- .../AssetManager/Src/Components/TopBar.lua | 15 + .../Src/Resources/PluginTheme.lua | 2 +- .../ConvertToPackage/Packages/UILibrary.lua | 21 +- .../Packages/UILibrary/_internal/Camera.lua | 29 + .../_internal/Components/BaseDialog.lua | 128 ++++ .../_internal/Components/BaseDialog.spec.lua | 121 ++++ .../_internal/Components/BulletPoint.lua | 93 +++ .../_internal/Components/BulletPoint.spec.lua | 36 ++ .../UILibrary/_internal/Components/Button.lua | 142 +++++ .../_internal/Components/Button.spec.lua | 79 +++ .../_internal/Components/CheckBox.lua | 96 +++ .../_internal/Components/CheckBox.spec.lua | 64 ++ .../_internal/Components/DetailedDropdown.lua | 353 +++++++++++ .../Components/DetailedDropdown.spec.lua | 34 ++ .../_internal/Components/DragTarget.lua | 58 ++ .../_internal/Components/DragTarget.spec.lua | 38 ++ .../_internal/Components/DropShadow.lua | 58 ++ .../_internal/Components/DropShadow.spec.lua | 30 + .../_internal/Components/DropdownMenu.lua | 241 ++++++++ .../Components/DropdownMenu.spec.lua | 228 +++++++ .../_internal/Components/ExpandableList.lua | 108 ++++ .../Components/ExpandableList.spec.lua | 143 +++++ .../Components/InfiniteScrollingFrame.lua | 136 +++++ .../InfiniteScrollingFrame.spec.lua | 69 +++ .../_internal/Components/LoadingBar.lua | 115 ++++ .../_internal/Components/LoadingIndicator.lua | 139 +++++ .../Components/LoadingIndicator.spec.lua | 16 + .../Components/MultilineTextEntry.lua | 143 +++++ .../Components/MultilineTextEntry.spec.lua | 47 ++ .../Components/PluginWidget/Dialog.lua | 93 +++ .../Components/Preview/ActionBar.lua | 192 ++++++ .../Components/Preview/ActionBar.spec.lua | 26 + .../Components/Preview/AssetDescription.lua | 98 +++ .../Preview/AssetDescription.spec.lua | 28 + .../Components/Preview/AssetPreview.lua | 512 ++++++++++++++++ .../Components/Preview/AssetPreview.spec.lua | 71 +++ .../Components/Preview/AudioControl.lua | 137 +++++ .../Components/Preview/AudioControl.spec.lua | 41 ++ .../Components/Preview/AudioPreview.lua | 356 +++++++++++ .../Components/Preview/AudioPreview.spec.lua | 25 + .../Components/Preview/Favorites.lua | 122 ++++ .../Components/Preview/Favorites.spec.lua | 78 +++ .../Components/Preview/ImagePreview.lua | 57 ++ .../Components/Preview/ImagePreview.spec.lua | 27 + .../Preview/InstanceTreeViewItem.lua | 161 +++++ .../Preview/InstanceTreeViewItem.spec.lua | 44 ++ .../Components/Preview/MediaControl.lua | 128 ++++ .../Components/Preview/MediaControl.spec.lua | 34 ++ .../Components/Preview/MediaProgressBar.lua | 179 ++++++ .../Preview/MediaProgressBar.spec.lua | 30 + .../Components/Preview/ModelPreview.lua | 216 +++++++ .../Components/Preview/ModelPreview.spec.lua | 28 + .../Components/Preview/PreviewController.lua | 350 +++++++++++ .../Preview/PreviewController.spec.lua | 36 ++ .../Components/Preview/SearchLinkText.lua | 141 +++++ .../Preview/SearchLinkText.spec.lua | 51 ++ .../Preview/ThumbnailIconPreview.lua | 91 +++ .../Preview/ThumbnailIconPreview.spec.lua | 27 + .../Components/Preview/TreeViewButton.lua | 143 +++++ .../Preview/TreeViewButton.spec.lua | 26 + .../Components/Preview/VideoPreview.lua | 271 ++++++++ .../Components/Preview/VideoPreview.spec.lua | 26 + .../_internal/Components/Preview/Vote.lua | 272 +++++++++ .../Components/Preview/Vote.spec.lua | 32 + .../Components/Preview/wrapDraggableMedia.lua | 67 ++ .../Preview/wrapDraggableMedia.spec.lua | 38 ++ .../Components/Preview/wrapMedia.lua | 116 ++++ .../Components/Preview/wrapMedia.spec.lua | 49 ++ .../_internal/Components/RadioButtons.lua | 140 +++++ .../Components/RadioButtons.spec.lua | 47 ++ .../_internal/Components/RoundFrame.lua | 107 ++++ .../_internal/Components/RoundFrame.spec.lua | 79 +++ .../_internal/Components/RoundTextBox.lua | 194 ++++++ .../Components/RoundTextBox.spec.lua | 71 +++ .../_internal/Components/RoundTextButton.lua | 118 ++++ .../Components/RoundTextButton.spec.lua | 35 ++ .../_internal/Components/SearchBar.lua | 483 +++++++++++++++ .../_internal/Components/SearchBar.spec.lua | 59 ++ .../_internal/Components/Separator.lua | 51 ++ .../_internal/Components/Separator.spec.lua | 30 + .../_internal/Components/StyledDialog.lua | 105 ++++ .../Components/StyledDialog.spec.lua | 94 +++ .../_internal/Components/StyledDropdown.lua | 281 +++++++++ .../Components/StyledDropdown.spec.lua | 34 ++ .../Components/StyledScrollingFrame.lua | 98 +++ .../Components/StyledScrollingFrame.spec.lua | 62 ++ .../_internal/Components/StyledTooltip.lua | 196 ++++++ .../Components/StyledTooltip.spec.lua | 30 + .../_internal/Components/TextEntry.lua | 137 +++++ .../_internal/Components/TextEntry.spec.lua | 44 ++ .../Components/Timeline/Keyframe.lua | 74 +++ .../Components/Timeline/Keyframe.spec.lua | 31 + .../Components/Timeline/Scrubber.lua | 66 ++ .../Components/Timeline/Scrubber.spec.lua | 54 ++ .../_internal/Components/TitledFrame.lua | 57 ++ .../_internal/Components/TitledFrame.spec.lua | 34 ++ .../_internal/Components/ToggleButton.lua | 61 ++ .../Components/ToggleButton.spec.lua | 30 + .../_internal/Components/Tooltip.lua | 193 ++++++ .../_internal/Components/Tooltip.spec.lua | 30 + .../_internal/Components/TreeView.lua | 524 ++++++++++++++++ .../_internal/Components/TreeView.spec.lua | 355 +++++++++++ .../Components/createFitToContent.lua | 70 +++ .../Components/createFitToContent.spec.lua | 44 ++ .../Packages/UILibrary/_internal/Focus.lua | 181 ++++++ .../UILibrary/_internal/Focus.spec.lua | 118 ++++ .../UILibrary/_internal/Localizing.lua | 98 +++ .../UILibrary/_internal/Localizing.spec.lua | 157 +++++ .../UILibrary/_internal/MockWrapper.lua | 44 ++ .../Packages/UILibrary/_internal/Plugin.lua | 28 + .../UILibrary/_internal/Studio/Analytics.lua | 123 ++++ .../_internal/Studio/ContextMenus.lua | 42 ++ .../UILibrary/_internal/Studio/Hyperlink.lua | 60 ++ .../_internal/Studio/Internal/Mouse.lua | 19 + .../_internal/Studio/Localization.lua | 284 +++++++++ .../_internal/Studio/Localization.spec.lua | 228 +++++++ .../_internal/Studio/PartialHyperLink.lua | 39 ++ .../_internal/Studio/PluginMenus.lua | 131 ++++ .../_internal/Studio/StudioStyle.lua | 54 ++ .../_internal/Studio/StudioStyle.spec.lua | 35 ++ .../_internal/Studio/StudioTheme.lua | 133 ++++ .../_internal/Studio/StudioTheme.spec.lua | 102 ++++ .../UILibrary/_internal/StyleDefaults.lua | 71 +++ .../Packages/UILibrary/_internal/Theming.lua | 63 ++ .../UILibrary/_internal/UILibraryWrapper.lua | 69 +++ .../_internal/UILibraryWrapper.spec.lua | 86 +++ .../UILibrary/_internal/Utils/AssetType.lua | 126 ++++ .../_internal/Utils/AssetType.spec.lua | 24 + .../_internal/Utils/GetClassIcon.lua | 31 + .../_internal/Utils/GetClassIcon.spec.lua | 43 ++ .../UILibrary/_internal/Utils/GetTextSize.lua | 20 + .../UILibrary/_internal/Utils/Immutable.lua | 141 +++++ .../_internal/Utils/Immutable.spec.lua | 284 +++++++++ .../_internal/Utils/InsertToolEvent.lua | 51 ++ .../_internal/Utils/LayoutOrderIterator.lua | 37 ++ .../Utils/LayoutOrderIterator.spec.lua | 30 + .../UILibrary/_internal/Utils/MathUtils.lua | 15 + .../_internal/Utils/MathUtils.spec.lua | 40 ++ .../UILibrary/_internal/Utils/Signal.lua | 63 ++ .../UILibrary/_internal/Utils/Signal.spec.lua | 114 ++++ .../UILibrary/_internal/Utils/Spritesheet.lua | 86 +++ .../_internal/Utils/Spritesheet.spec.lua | 115 ++++ .../UILibrary/_internal/Utils/Symbol.lua | 44 ++ .../UILibrary/_internal/Utils/Symbol.spec.lua | 45 ++ .../UILibrary/_internal/Utils/Urls.lua | 43 ++ .../_internal/Utils/getTimeString.lua | 11 + .../_internal/Utils/getTimeString.spec.lua | 51 ++ .../UILibrary/_internal/createTheme.lua | 578 ++++++++++++++++++ .../Packages/UILibrary/_internal/deepJoin.lua | 31 + .../UILibrary/_internal/deepJoin.spec.lua | 76 +++ .../Packages/UILibrary/_internal/join.lua | 15 + .../Thunks/UploadConvertToPackageRequest.lua | 26 +- .../UILibrary/_internal/Utils/Symbol.spec.lua | 4 +- .../Framework/ContextServices/Theme.spec.lua | 7 + .../ContextServices/UILibraryWrapper.spec.lua | 35 +- .../StudioUI/TitledFrame/test.spec.lua | 2 +- .../Framework/StudioUI/createPluginWidget.lua | 19 +- .../Packages/Framework/Style/Stylizer.lua | 4 +- .../Framework/Style/getRawComponentStyle.lua | 1 + .../TestHelpers/provideMockContext.lua | 2 +- .../Packages/Framework/UI/Box/test.spec.lua | 2 +- .../Framework/UI/Button/test.spec.lua | 2 +- .../Framework/UI/Container/test.spec.lua | 2 +- .../Packages/Framework/UI/DropdownMenu.lua | 3 +- .../Framework/UI/FakeLoadingBar/test.spec.lua | 2 +- .../Packages/Framework/UI/Image/test.spec.lua | 2 +- .../UI/InstanceTreeView/InstanceTreeRow.lua | 11 +- .../Framework/UI/LinkText/test.spec.lua | 2 +- .../Framework/UI/LoadingBar/test.spec.lua | 2 +- .../Framework/UI/RadioButton/test.spec.lua | 2 +- .../Framework/UI/RoundBox/test.spec.lua | 2 +- .../Framework/UI/TextLabel/test.spec.lua | 2 +- .../Framework/UI/ToggleButton/test.spec.lua | 2 +- .../Framework/UI/Tooltip/test.spec.lua | 2 +- .../Framework/UI/TreeView/test.spec.lua | 2 +- .../EventEmulator/Packages/Framework/Util.lua | 1 + .../Util/CrossPluginCommunication.lua | 6 +- .../Util/CrossPluginCommunication.spec.lua | 8 +- .../Packages/Framework/Util/Palette.spec.lua | 13 +- .../Packages/Framework/Util/Typecheck/t.lua | 2 + .../Framework/Util/getTestVariation.lua | 30 + .../Framework/Util/getTestVariation.spec.lua | 41 ++ .../FrameworkCompanion/Bin/main.server.lua | 36 +- .../Bin/runTests.server.lua | 43 +- .../ContextServices/Theme.spec.lua | 7 + .../ContextServices/UILibraryWrapper.spec.lua | 35 +- .../StudioUI/TitledFrame/test.spec.lua | 2 +- .../StudioUI/createPluginWidget.lua | 19 +- .../DEPRECATED_Framework/Style/Stylizer.lua | 4 +- .../Style/getRawComponentStyle.lua | 1 + .../TestHelpers/provideMockContext.lua | 2 +- .../DEPRECATED_Framework/UI/Box/test.spec.lua | 2 +- .../UI/Button/test.spec.lua | 2 +- .../UI/Container/test.spec.lua | 2 +- .../DEPRECATED_Framework/UI/DropdownMenu.lua | 3 +- .../UI/FakeLoadingBar/test.spec.lua | 2 +- .../UI/Image/test.spec.lua | 2 +- .../UI/InstanceTreeView/InstanceTreeRow.lua | 11 +- .../UI/LinkText/test.spec.lua | 2 +- .../UI/LoadingBar/test.spec.lua | 2 +- .../UI/RadioButton/test.spec.lua | 2 +- .../UI/RoundBox/test.spec.lua | 2 +- .../UI/TextLabel/test.spec.lua | 2 +- .../UI/ToggleButton/test.spec.lua | 2 +- .../UI/Tooltip/test.spec.lua | 2 +- .../UI/TreeView/test.spec.lua | 2 +- .../Packages/DEPRECATED_Framework/Util.lua | 1 + .../Util/CrossPluginCommunication.lua | 6 +- .../Util/CrossPluginCommunication.spec.lua | 8 +- .../Util/Palette.spec.lua | 13 +- .../DEPRECATED_Framework/Util/Typecheck/t.lua | 2 + .../Util/getTestVariation.lua | 30 + .../Util/getTestVariation.spec.lua | 41 ++ .../Src/Components/StylesList.spec.lua | 4 + .../FrameworkCompanion/Src/MockWrap.lua | 16 +- .../Src/Resources/MakeTheme.lua | 11 +- .../Src/Util/DebugFlags.lua | 20 + .../Src/Util/commonInit.lua | 41 ++ .../Framework/ContextServices/Theme.spec.lua | 7 + .../ContextServices/UILibraryWrapper.spec.lua | 35 +- .../StudioUI/TitledFrame/test.spec.lua | 2 +- .../Framework/StudioUI/createPluginWidget.lua | 19 +- .../GameSettings/Framework/Style/Stylizer.lua | 4 +- .../Framework/Style/getRawComponentStyle.lua | 1 + .../TestHelpers/provideMockContext.lua | 2 +- .../Framework/UI/Box/test.spec.lua | 2 +- .../Framework/UI/Button/test.spec.lua | 2 +- .../Framework/UI/Container/test.spec.lua | 2 +- .../Framework/UI/DropdownMenu.lua | 3 +- .../Framework/UI/FakeLoadingBar/test.spec.lua | 2 +- .../Framework/UI/Image/test.spec.lua | 2 +- .../UI/InstanceTreeView/InstanceTreeRow.lua | 11 +- .../Framework/UI/LinkText/test.spec.lua | 2 +- .../Framework/UI/LoadingBar/test.spec.lua | 2 +- .../Framework/UI/RadioButton/test.spec.lua | 2 +- .../Framework/UI/RoundBox/test.spec.lua | 2 +- .../Framework/UI/TextLabel/test.spec.lua | 2 +- .../Framework/UI/ToggleButton/test.spec.lua | 2 +- .../Framework/UI/Tooltip/test.spec.lua | 2 +- .../Framework/UI/TreeView/test.spec.lua | 2 +- .../GameSettings/Framework/Util.lua | 1 + .../Util/CrossPluginCommunication.lua | 6 +- .../Util/CrossPluginCommunication.spec.lua | 8 +- .../Framework/Util/Palette.spec.lua | 13 +- .../Framework/Util/Typecheck/t.lua | 2 + .../Framework/Util/getTestVariation.lua | 30 + .../Framework/Util/getTestVariation.spec.lua | 41 ++ .../AvatarPage/Components/AssetsPanel.lua | 5 +- .../AvatarPage/Util/StateModelTemplate.lua | 4 +- .../Pages/BasicInfoPage/BasicInfo.lua | 6 +- .../Pages/OptionsPage/Options.lua | 4 +- .../Pages/SecurityPage/Security.lua | 4 +- .../GameSettings/Pages/WorldPage/World.lua | 4 +- BuiltInPlugins/GameSettings/Promise.lua | 2 +- .../RoactStudioWidgets/Internal/Theme.lua | 4 +- .../RoactStudioWidgets/RadioButtonSet.lua | 6 +- .../GameSettings/RoactStudioWidgets/Text.lua | 5 +- .../RoactStudioWidgets/TitledFrame.lua | 3 +- .../GameSettings/Src/Components/Dropdown.lua | 4 +- .../Components/SettingsPages/SettingsPage.lua | 4 +- .../Controllers/GroupMetadataController.lua | 9 +- .../GameSettings/Src/Util/AssetOverrides.lua | 4 +- .../UILibrary/_internal/Utils/Symbol.spec.lua | 4 +- .../LocalizationTools/Packages/Framework.lua | 1 + .../Packages/Framework/ContextServices.lua | 4 +- .../Framework/ContextServices/ContextItem.lua | 15 +- .../Framework/ContextServices/FastFlags.lua | 2 +- .../ContextServices/FastFlags.spec.lua | 3 +- .../ContextServices/Localization.lua | 11 +- .../Framework/ContextServices/Mouse.spec.lua | 4 +- .../ContextServices/PluginActions.spec.lua | 6 +- .../Framework/ContextServices/Theme.spec.lua | 7 + .../ContextServices/UILibraryWrapper.lua | 25 +- .../ContextServices/UILibraryWrapper.spec.lua | 35 +- .../Framework/ContextServices/mapToProps.lua | 22 +- .../Backtrace/BacktraceReport.lua | 9 +- .../Backtrace/BacktraceReporter.spec.lua | 27 +- .../Framework/ErrorReporter/ErrorQueue.lua | 1 - .../ErrorReporter/ErrorQueue.spec.lua | 1 - .../StudioPluginErrorReporter.spec.lua | 16 +- .../Packages/Framework/Examples/General.lua | 55 +- .../Framework/Examples/General/stylizer.lua | 96 +++ .../Examples/General/stylizer/Application.lua | 67 ++ .../Examples/General/stylizer/Box.lua | 38 ++ .../Examples/General/stylizer/Button.lua | 32 + .../Examples/General/stylizer/Dialog.lua | 47 ++ .../Packages/Framework/Http/Networking.lua | 15 +- .../Framework/Http/Networking.spec.lua | 51 +- .../Packages/Framework/RobloxAPI.lua | 9 +- .../Games/assetsGenerationRequest.lua | 6 +- .../ToolboxService/V1/Items/details.lua | 51 ++ .../Packages/Framework/RobloxAPI/Url.lua | 5 +- .../Packages/Framework/RobloxAPI/Url.spec.lua | 12 +- .../RobloxAPI/WWW/Develop/library.lua | 4 +- .../Framework/RobloxAPI/init.spec.lua | 4 +- .../StudioUI/PluginButton/test.spec.lua | 21 +- .../Framework/StudioUI/PluginToolbar.lua | 2 + .../StudioUI/PluginToolbar/test.spec.lua | 11 +- .../Packages/Framework/StudioUI/SearchBar.lua | 24 +- .../Framework/StudioUI/SearchBar/style.lua | 115 +++- .../StudioUI/StudioFrameworkStyles.lua | 9 + .../StudioUI/StudioFrameworkStyles.spec.lua | 14 +- .../StudioUI/StudioFrameworkStyles/Common.lua | 107 ++-- .../Framework/StudioUI/StyledDialog.lua | 24 +- .../StudioUI/StyledDialog/example.lua | 35 +- .../StudioUI/StyledDialog/renderExample.lua | 16 +- .../Framework/StudioUI/StyledDialog/style.lua | 72 ++- .../StudioUI/StyledDialog/test.spec.lua | 3 +- .../Framework/StudioUI/TitledFrame.lua | 16 +- .../StudioUI/TitledFrame/renderExample.lua | 8 +- .../Framework/StudioUI/TitledFrame/style.lua | 35 +- .../StudioUI/TitledFrame/test.spec.lua | 22 +- .../Framework/StudioUI/createPluginWidget.lua | 27 +- .../Packages/Framework/Style.lua | 17 + .../Packages/Framework/Style/Colors.lua | 22 + .../Framework/Style/ComponentSymbols.lua | 26 + .../Framework/Style/ComponentSymbols.spec.lua | 43 ++ .../Packages/Framework/Style/StyleKey.lua | 19 + .../Framework/Style/StyleKey.spec.lua | 34 ++ .../Packages/Framework/Style/Stylizer.lua | 323 ++++++++++ .../Framework/Style/Stylizer.spec.lua | 539 ++++++++++++++++ .../Framework/Style/Themes/BaseTheme.lua | 48 ++ .../Framework/Style/Themes/DarkTheme.lua | 54 ++ .../Framework/Style/Themes/LightTheme.lua | 54 ++ .../Framework/Style/Themes/StudioTheme.lua | 49 ++ .../Style/Themes/StudioTheme.spec.lua | 8 + .../Framework/Style/createDefaultTheme.lua | 17 + .../Style/createDefaultTheme.spec.lua | 30 + .../Framework/Style/getRawComponentStyle.lua | 17 + .../Style/getRawComponentStyle.spec.lua | 22 + .../Packages/Framework/TestHelpers.lua | 7 +- .../Framework/TestHelpers/Instances.lua | 10 + .../Instances/MockAnalyticsService.lua | 41 ++ .../TestHelpers/Instances/MockMouse.lua | 23 + .../TestHelpers/Instances/MockPlugin.lua | 133 ++++ .../TestHelpers/Instances/MockPlugin.spec.lua | 78 +++ .../Instances/MockPluginToolbar.lua | 63 ++ .../Instances/MockPluginToolbar.spec.lua | 38 ++ .../Instances/MockPluginToolbarButton.lua | 42 ++ .../MockPluginToolbarButton.spec.lua | 22 + .../Instances/MockSelectionService.lua | 28 + .../Instances/MockStudioService.lua | 19 + .../TestHelpers/makeSettableValue.lua | 21 + .../TestHelpers/makeSettableValue.spec.lua | 40 ++ .../TestHelpers/provideMockContext.lua | 57 +- .../TestHelpers/provideMockContext.spec.lua | 15 +- .../Framework/TestHelpers/setEquals.lua | 16 + .../TestHelpers/testImmutability.lua | 74 +++ .../TestHelpers/testImmutability.spec.lua | 169 +++++ .../Packages/Framework/UI/Box.lua | 23 +- .../Packages/Framework/UI/Box/style.lua | 33 +- .../Packages/Framework/UI/Box/test.spec.lua | 35 +- .../Packages/Framework/UI/BulletList.lua | 38 +- .../Framework/UI/BulletList/style.lua | 27 +- .../Framework/UI/BulletList/test.spec.lua | 6 +- .../Packages/Framework/UI/Button.lua | 17 +- .../Packages/Framework/UI/Button/example.lua | 109 ++-- .../Packages/Framework/UI/Button/style.lua | 138 +++-- .../Framework/UI/Button/test.spec.lua | 22 +- .../Framework/UI/Container/example.lua | 86 ++- .../Framework/UI/Container/test.spec.lua | 22 +- .../Packages/Framework/UI/DropShadow.lua | 23 +- .../Framework/UI/DropShadow/style.lua | 35 +- .../Packages/Framework/UI/DropdownMenu.lua | 31 +- .../Framework/UI/DropdownMenu/style.lua | 49 +- .../Framework/UI/FakeLoadingBar/test.spec.lua | 22 +- .../Framework/UI/FrameworkStyles.spec.lua | 4 +- .../Packages/Framework/UI/Image.lua | 20 +- .../Packages/Framework/UI/Image/style.lua | 22 +- .../Packages/Framework/UI/Image/test.spec.lua | 22 +- .../Framework/UI/InfiniteScrollingFrame.lua | 26 +- .../UI/InfiniteScrollingFrame/style.lua | 34 +- .../Framework/UI/InstanceTreeView.lua | 25 +- .../UI/InstanceTreeView/InstanceTreeRow.lua | 11 +- .../Framework/UI/InstanceTreeView/style.lua | 87 ++- .../Packages/Framework/UI/LinkText.lua | 17 +- .../Framework/UI/LinkText/example.lua | 24 +- .../Packages/Framework/UI/LinkText/style.lua | 29 +- .../Framework/UI/LinkText/test.spec.lua | 22 +- .../Packages/Framework/UI/LoadingBar.lua | 22 +- .../Framework/UI/LoadingBar/example.lua | 31 +- .../Framework/UI/LoadingBar/style.lua | 47 +- .../Framework/UI/LoadingBar/test.spec.lua | 22 +- .../Framework/UI/LoadingIndicator.lua | 23 +- .../Framework/UI/LoadingIndicator/style.lua | 29 +- .../Packages/Framework/UI/RadioButton.lua | 19 +- .../Framework/UI/RadioButton/example.lua | 22 +- .../Framework/UI/RadioButton/style.lua | 65 +- .../Framework/UI/RadioButton/test.spec.lua | 22 +- .../Packages/Framework/UI/RadioButtonList.lua | 17 +- .../Framework/UI/RadioButtonList/example.lua | 22 +- .../Framework/UI/RadioButtonList/style.lua | 25 +- .../Packages/Framework/UI/RangeSlider.lua | 17 +- .../Framework/UI/RangeSlider/example.lua | 25 +- .../Framework/UI/RangeSlider/style.lua | 116 ++-- .../Packages/Framework/UI/RoundBox.lua | 24 +- .../Packages/Framework/UI/RoundBox/style.lua | 35 +- .../Framework/UI/RoundBox/test.spec.lua | 22 +- .../Packages/Framework/UI/ScrollingFrame.lua | 58 +- .../Framework/UI/ScrollingFrame/example.lua | 28 +- .../Framework/UI/ScrollingFrame/style.lua | 32 +- .../Packages/Framework/UI/SelectInput.lua | 19 +- .../Framework/UI/SelectInput/style.lua | 89 ++- .../Packages/Framework/UI/Separator.lua | 24 +- .../Packages/Framework/UI/Separator/style.lua | 31 +- .../Framework/UI/Slider/test.spec.lua | 4 +- .../Packages/Framework/UI/TextInput.lua | 25 +- .../Packages/Framework/UI/TextInput/style.lua | 75 ++- .../Packages/Framework/UI/TextLabel.lua | 26 +- .../Packages/Framework/UI/TextLabel/style.lua | 29 +- .../Framework/UI/TextLabel/test.spec.lua | 22 +- .../Packages/Framework/UI/ToggleButton.lua | 18 +- .../Framework/UI/ToggleButton/example.lua | 24 +- .../Framework/UI/ToggleButton/style.lua | 119 ++-- .../Framework/UI/ToggleButton/test.spec.lua | 22 +- .../Packages/Framework/UI/Tooltip.lua | 32 +- .../Packages/Framework/UI/Tooltip/style.lua | 39 +- .../Framework/UI/Tooltip/test.spec.lua | 26 +- .../Packages/Framework/UI/TreeView.lua | 18 +- .../Packages/Framework/UI/TreeView/style.lua | 35 +- .../Framework/UI/TreeView/test.spec.lua | 22 +- .../Packages/Framework/Util.lua | 6 +- .../Util/CrossPluginCommunication.lua | 6 +- .../Util/CrossPluginCommunication.spec.lua | 8 +- .../Packages/Framework/Util/Flags.lua | 4 +- .../Packages/Framework/Util/Palette.spec.lua | 13 +- .../Packages/Framework/Util/Promise.lua | 78 ++- .../Packages/Framework/Util/Promise.spec.lua | 10 - .../Packages/Framework/Util/StyleModifier.lua | 45 +- .../Packages/Framework/Util/Symbol.lua | 2 - .../Packages/Framework/Util/Symbol.spec.lua | 4 +- .../Framework/Util/Typecheck/DocParser.lua | 1 + .../Util/Typecheck/FrameworkTypes.lua | 21 +- .../Packages/Framework/Util/Typecheck/t.lua | 2 + .../Framework/Util/Typecheck/wrap.lua | 12 +- .../Framework/Util/Typecheck/wrap.spec.lua | 43 +- .../Packages/Framework/Util/deepCopy.lua | 22 + .../Packages/Framework/Util/deepCopy.spec.lua | 37 ++ .../Packages/Framework/Util/deepJoin.lua | 31 + .../Packages/Framework/Util/deepJoin.spec.lua | 76 +++ .../Framework/Util/getTestVariation.lua | 30 + .../Framework/Util/getTestVariation.spec.lua | 41 ++ .../Packages/Framework/Util/tableCache.lua | 31 + .../Framework/Util/tableCache.spec.lua | 40 ++ .../LocalizationTools/Packages/UILibrary.lua | 21 +- .../Packages/UILibrary/_internal/Camera.lua | 29 + .../_internal/Components/BaseDialog.lua | 128 ++++ .../_internal/Components/BaseDialog.spec.lua | 121 ++++ .../_internal/Components/BulletPoint.lua | 93 +++ .../_internal/Components/BulletPoint.spec.lua | 36 ++ .../UILibrary/_internal/Components/Button.lua | 142 +++++ .../_internal/Components/Button.spec.lua | 79 +++ .../_internal/Components/CheckBox.lua | 96 +++ .../_internal/Components/CheckBox.spec.lua | 64 ++ .../_internal/Components/DetailedDropdown.lua | 353 +++++++++++ .../Components/DetailedDropdown.spec.lua | 34 ++ .../_internal/Components/DragTarget.lua | 58 ++ .../_internal/Components/DragTarget.spec.lua | 38 ++ .../_internal/Components/DropShadow.lua | 58 ++ .../_internal/Components/DropShadow.spec.lua | 30 + .../_internal/Components/DropdownMenu.lua | 241 ++++++++ .../Components/DropdownMenu.spec.lua | 228 +++++++ .../_internal/Components/ExpandableList.lua | 108 ++++ .../Components/ExpandableList.spec.lua | 143 +++++ .../Components/InfiniteScrollingFrame.lua | 136 +++++ .../InfiniteScrollingFrame.spec.lua | 69 +++ .../_internal/Components/LoadingBar.lua | 115 ++++ .../_internal/Components/LoadingIndicator.lua | 139 +++++ .../Components/LoadingIndicator.spec.lua | 16 + .../Components/MultilineTextEntry.lua | 143 +++++ .../Components/MultilineTextEntry.spec.lua | 47 ++ .../Components/PluginWidget/Dialog.lua | 93 +++ .../Components/Preview/ActionBar.lua | 192 ++++++ .../Components/Preview/ActionBar.spec.lua | 26 + .../Components/Preview/AssetDescription.lua | 98 +++ .../Preview/AssetDescription.spec.lua | 28 + .../Components/Preview/AssetPreview.lua | 512 ++++++++++++++++ .../Components/Preview/AssetPreview.spec.lua | 71 +++ .../Components/Preview/AudioControl.lua | 137 +++++ .../Components/Preview/AudioControl.spec.lua | 41 ++ .../Components/Preview/AudioPreview.lua | 356 +++++++++++ .../Components/Preview/AudioPreview.spec.lua | 25 + .../Components/Preview/Favorites.lua | 122 ++++ .../Components/Preview/Favorites.spec.lua | 78 +++ .../Components/Preview/ImagePreview.lua | 57 ++ .../Components/Preview/ImagePreview.spec.lua | 27 + .../Preview/InstanceTreeViewItem.lua | 161 +++++ .../Preview/InstanceTreeViewItem.spec.lua | 44 ++ .../Components/Preview/MediaControl.lua | 128 ++++ .../Components/Preview/MediaControl.spec.lua | 34 ++ .../Components/Preview/MediaProgressBar.lua | 179 ++++++ .../Preview/MediaProgressBar.spec.lua | 30 + .../Components/Preview/ModelPreview.lua | 216 +++++++ .../Components/Preview/ModelPreview.spec.lua | 28 + .../Components/Preview/PreviewController.lua | 350 +++++++++++ .../Preview/PreviewController.spec.lua | 36 ++ .../Components/Preview/SearchLinkText.lua | 141 +++++ .../Preview/SearchLinkText.spec.lua | 51 ++ .../Preview/ThumbnailIconPreview.lua | 91 +++ .../Preview/ThumbnailIconPreview.spec.lua | 27 + .../Components/Preview/TreeViewButton.lua | 143 +++++ .../Preview/TreeViewButton.spec.lua | 26 + .../Components/Preview/VideoPreview.lua | 271 ++++++++ .../Components/Preview/VideoPreview.spec.lua | 26 + .../_internal/Components/Preview/Vote.lua | 272 +++++++++ .../Components/Preview/Vote.spec.lua | 32 + .../Components/Preview/wrapDraggableMedia.lua | 67 ++ .../Preview/wrapDraggableMedia.spec.lua | 38 ++ .../Components/Preview/wrapMedia.lua | 116 ++++ .../Components/Preview/wrapMedia.spec.lua | 49 ++ .../_internal/Components/RadioButtons.lua | 140 +++++ .../Components/RadioButtons.spec.lua | 47 ++ .../_internal/Components/RoundFrame.lua | 107 ++++ .../_internal/Components/RoundFrame.spec.lua | 79 +++ .../_internal/Components/RoundTextBox.lua | 194 ++++++ .../Components/RoundTextBox.spec.lua | 71 +++ .../_internal/Components/RoundTextButton.lua | 118 ++++ .../Components/RoundTextButton.spec.lua | 35 ++ .../_internal/Components/SearchBar.lua | 483 +++++++++++++++ .../_internal/Components/SearchBar.spec.lua | 59 ++ .../_internal/Components/Separator.lua | 51 ++ .../_internal/Components/Separator.spec.lua | 30 + .../_internal/Components/StyledDialog.lua | 105 ++++ .../Components/StyledDialog.spec.lua | 94 +++ .../_internal/Components/StyledDropdown.lua | 281 +++++++++ .../Components/StyledDropdown.spec.lua | 34 ++ .../Components/StyledScrollingFrame.lua | 98 +++ .../Components/StyledScrollingFrame.spec.lua | 62 ++ .../_internal/Components/StyledTooltip.lua | 196 ++++++ .../Components/StyledTooltip.spec.lua | 30 + .../_internal/Components/TextEntry.lua | 137 +++++ .../_internal/Components/TextEntry.spec.lua | 44 ++ .../Components/Timeline/Keyframe.lua | 74 +++ .../Components/Timeline/Keyframe.spec.lua | 31 + .../Components/Timeline/Scrubber.lua | 66 ++ .../Components/Timeline/Scrubber.spec.lua | 54 ++ .../_internal/Components/TitledFrame.lua | 57 ++ .../_internal/Components/TitledFrame.spec.lua | 34 ++ .../_internal/Components/ToggleButton.lua | 61 ++ .../Components/ToggleButton.spec.lua | 30 + .../_internal/Components/Tooltip.lua | 193 ++++++ .../_internal/Components/Tooltip.spec.lua | 30 + .../_internal/Components/TreeView.lua | 524 ++++++++++++++++ .../_internal/Components/TreeView.spec.lua | 355 +++++++++++ .../Components/createFitToContent.lua | 70 +++ .../Components/createFitToContent.spec.lua | 44 ++ .../Packages/UILibrary/_internal/Focus.lua | 181 ++++++ .../UILibrary/_internal/Focus.spec.lua | 118 ++++ .../UILibrary/_internal/Localizing.lua | 98 +++ .../UILibrary/_internal/Localizing.spec.lua | 157 +++++ .../UILibrary/_internal/MockWrapper.lua | 44 ++ .../Packages/UILibrary/_internal/Plugin.lua | 28 + .../UILibrary/_internal/Studio/Analytics.lua | 123 ++++ .../_internal/Studio/ContextMenus.lua | 42 ++ .../UILibrary/_internal/Studio/Hyperlink.lua | 60 ++ .../_internal/Studio/Internal/Mouse.lua | 19 + .../_internal/Studio/Localization.lua | 284 +++++++++ .../_internal/Studio/Localization.spec.lua | 228 +++++++ .../_internal/Studio/PartialHyperLink.lua | 39 ++ .../_internal/Studio/PluginMenus.lua | 131 ++++ .../_internal/Studio/StudioStyle.lua | 54 ++ .../_internal/Studio/StudioStyle.spec.lua | 35 ++ .../_internal/Studio/StudioTheme.lua | 133 ++++ .../_internal/Studio/StudioTheme.spec.lua | 102 ++++ .../UILibrary/_internal/StyleDefaults.lua | 71 +++ .../Packages/UILibrary/_internal/Theming.lua | 63 ++ .../UILibrary/_internal/UILibraryWrapper.lua | 69 +++ .../_internal/UILibraryWrapper.spec.lua | 86 +++ .../UILibrary/_internal/Utils/AssetType.lua | 126 ++++ .../_internal/Utils/AssetType.spec.lua | 24 + .../_internal/Utils/GetClassIcon.lua | 31 + .../_internal/Utils/GetClassIcon.spec.lua | 43 ++ .../UILibrary/_internal/Utils/GetTextSize.lua | 20 + .../UILibrary/_internal/Utils/Immutable.lua | 141 +++++ .../_internal/Utils/Immutable.spec.lua | 284 +++++++++ .../_internal/Utils/InsertToolEvent.lua | 51 ++ .../_internal/Utils/LayoutOrderIterator.lua | 37 ++ .../Utils/LayoutOrderIterator.spec.lua | 30 + .../UILibrary/_internal/Utils/MathUtils.lua | 15 + .../_internal/Utils/MathUtils.spec.lua | 40 ++ .../UILibrary/_internal/Utils/Signal.lua | 63 ++ .../UILibrary/_internal/Utils/Signal.spec.lua | 114 ++++ .../UILibrary/_internal/Utils/Spritesheet.lua | 86 +++ .../_internal/Utils/Spritesheet.spec.lua | 115 ++++ .../UILibrary/_internal/Utils/Symbol.lua | 44 ++ .../UILibrary/_internal/Utils/Symbol.spec.lua | 45 ++ .../UILibrary/_internal/Utils/Urls.lua | 43 ++ .../_internal/Utils/getTimeString.lua | 11 + .../_internal/Utils/getTimeString.spec.lua | 51 ++ .../UILibrary/_internal/createTheme.lua | 578 ++++++++++++++++++ .../Packages/UILibrary/_internal/deepJoin.lua | 31 + .../UILibrary/_internal/deepJoin.spec.lua | 76 +++ .../Packages/UILibrary/_internal/join.lua | 15 + .../Src/Components/UploadDialogContent.lua | 5 +- .../PlayerEmulator/Packages/Framework.lua | 1 + .../Packages/Framework/ContextServices.lua | 4 +- .../Framework/ContextServices/ContextItem.lua | 15 +- .../Framework/ContextServices/FastFlags.lua | 2 +- .../ContextServices/FastFlags.spec.lua | 3 +- .../ContextServices/Localization.lua | 11 +- .../Framework/ContextServices/Mouse.spec.lua | 4 +- .../ContextServices/PluginActions.spec.lua | 6 +- .../Framework/ContextServices/Theme.spec.lua | 7 + .../ContextServices/UILibraryWrapper.lua | 25 +- .../ContextServices/UILibraryWrapper.spec.lua | 35 +- .../Framework/ContextServices/mapToProps.lua | 22 +- .../Backtrace/BacktraceReport.lua | 9 +- .../Backtrace/BacktraceReporter.spec.lua | 27 +- .../Framework/ErrorReporter/ErrorQueue.lua | 1 - .../ErrorReporter/ErrorQueue.spec.lua | 1 - .../StudioPluginErrorReporter.spec.lua | 16 +- .../Packages/Framework/Examples/General.lua | 55 +- .../Framework/Examples/General/stylizer.lua | 96 +++ .../Examples/General/stylizer/Application.lua | 67 ++ .../Examples/General/stylizer/Box.lua | 38 ++ .../Examples/General/stylizer/Button.lua | 32 + .../Examples/General/stylizer/Dialog.lua | 47 ++ .../Packages/Framework/Http/Networking.lua | 15 +- .../Framework/Http/Networking.spec.lua | 51 +- .../Packages/Framework/RobloxAPI.lua | 19 +- .../Games/assetsGenerationRequest.lua | 6 +- .../ToolboxService/V1/Items/details.lua | 51 ++ .../Packages/Framework/RobloxAPI/Url.lua | 5 +- .../Packages/Framework/RobloxAPI/Url.spec.lua | 12 +- .../RobloxAPI/WWW/Develop/library.lua | 4 +- .../Framework/RobloxAPI/init.spec.lua | 4 +- .../StudioUI/PluginButton/test.spec.lua | 21 +- .../Framework/StudioUI/PluginToolbar.lua | 2 + .../StudioUI/PluginToolbar/test.spec.lua | 11 +- .../Packages/Framework/StudioUI/SearchBar.lua | 24 +- .../Framework/StudioUI/SearchBar/style.lua | 115 +++- .../StudioUI/StudioFrameworkStyles.lua | 9 + .../StudioUI/StudioFrameworkStyles.spec.lua | 14 +- .../StudioUI/StudioFrameworkStyles/Common.lua | 107 ++-- .../Framework/StudioUI/StyledDialog.lua | 24 +- .../StudioUI/StyledDialog/example.lua | 35 +- .../StudioUI/StyledDialog/renderExample.lua | 16 +- .../Framework/StudioUI/StyledDialog/style.lua | 72 ++- .../StudioUI/StyledDialog/test.spec.lua | 3 +- .../Framework/StudioUI/TitledFrame.lua | 16 +- .../StudioUI/TitledFrame/renderExample.lua | 8 +- .../Framework/StudioUI/TitledFrame/style.lua | 35 +- .../StudioUI/TitledFrame/test.spec.lua | 22 +- .../Framework/StudioUI/createPluginWidget.lua | 27 +- .../Packages/Framework/Style.lua | 17 + .../Packages/Framework/Style/Colors.lua | 22 + .../Framework/Style/ComponentSymbols.lua | 26 + .../Framework/Style/ComponentSymbols.spec.lua | 43 ++ .../Packages/Framework/Style/StyleKey.lua | 19 + .../Framework/Style/StyleKey.spec.lua | 34 ++ .../Packages/Framework/Style/Stylizer.lua | 323 ++++++++++ .../Framework/Style/Stylizer.spec.lua | 539 ++++++++++++++++ .../Framework/Style/Themes/BaseTheme.lua | 48 ++ .../Framework/Style/Themes/DarkTheme.lua | 54 ++ .../Framework/Style/Themes/LightTheme.lua | 54 ++ .../Framework/Style/Themes/StudioTheme.lua | 49 ++ .../Style/Themes/StudioTheme.spec.lua | 8 + .../Framework/Style/createDefaultTheme.lua | 17 + .../Style/createDefaultTheme.spec.lua | 30 + .../Framework/Style/getRawComponentStyle.lua | 17 + .../Style/getRawComponentStyle.spec.lua | 22 + .../Packages/Framework/TestHelpers.lua | 7 +- .../Framework/TestHelpers/Instances.lua | 10 + .../Instances/MockAnalyticsService.lua | 41 ++ .../TestHelpers/Instances/MockMouse.lua | 23 + .../TestHelpers/Instances/MockPlugin.lua | 133 ++++ .../TestHelpers/Instances/MockPlugin.spec.lua | 78 +++ .../Instances/MockPluginToolbar.lua | 63 ++ .../Instances/MockPluginToolbar.spec.lua | 38 ++ .../Instances/MockPluginToolbarButton.lua | 42 ++ .../MockPluginToolbarButton.spec.lua | 22 + .../Instances/MockSelectionService.lua | 28 + .../Instances/MockStudioService.lua | 19 + .../TestHelpers/makeSettableValue.lua | 21 + .../TestHelpers/makeSettableValue.spec.lua | 40 ++ .../TestHelpers/provideMockContext.lua | 57 +- .../TestHelpers/provideMockContext.spec.lua | 15 +- .../Framework/TestHelpers/setEquals.lua | 16 + .../TestHelpers/testImmutability.lua | 74 +++ .../TestHelpers/testImmutability.spec.lua | 169 +++++ .../Packages/Framework/UI/Box.lua | 23 +- .../Packages/Framework/UI/Box/style.lua | 33 +- .../Packages/Framework/UI/Box/test.spec.lua | 35 +- .../Packages/Framework/UI/BulletList.lua | 38 +- .../Framework/UI/BulletList/style.lua | 27 +- .../Framework/UI/BulletList/test.spec.lua | 6 +- .../Packages/Framework/UI/Button.lua | 17 +- .../Packages/Framework/UI/Button/example.lua | 109 ++-- .../Packages/Framework/UI/Button/style.lua | 138 +++-- .../Framework/UI/Button/test.spec.lua | 22 +- .../Framework/UI/Container/example.lua | 86 ++- .../Framework/UI/Container/test.spec.lua | 22 +- .../Packages/Framework/UI/DropShadow.lua | 23 +- .../Framework/UI/DropShadow/style.lua | 35 +- .../Packages/Framework/UI/DropdownMenu.lua | 31 +- .../Framework/UI/DropdownMenu/style.lua | 49 +- .../Framework/UI/FakeLoadingBar/test.spec.lua | 22 +- .../Framework/UI/FrameworkStyles.spec.lua | 4 +- .../Packages/Framework/UI/Image.lua | 20 +- .../Packages/Framework/UI/Image/style.lua | 22 +- .../Packages/Framework/UI/Image/test.spec.lua | 22 +- .../Framework/UI/InfiniteScrollingFrame.lua | 26 +- .../UI/InfiniteScrollingFrame/style.lua | 34 +- .../Framework/UI/InstanceTreeView.lua | 25 +- .../UI/InstanceTreeView/InstanceTreeRow.lua | 11 +- .../Framework/UI/InstanceTreeView/style.lua | 87 ++- .../Packages/Framework/UI/LinkText.lua | 17 +- .../Framework/UI/LinkText/example.lua | 24 +- .../Packages/Framework/UI/LinkText/style.lua | 29 +- .../Framework/UI/LinkText/test.spec.lua | 22 +- .../Packages/Framework/UI/LoadingBar.lua | 22 +- .../Framework/UI/LoadingBar/example.lua | 31 +- .../Framework/UI/LoadingBar/style.lua | 47 +- .../Framework/UI/LoadingBar/test.spec.lua | 22 +- .../Framework/UI/LoadingIndicator.lua | 23 +- .../Framework/UI/LoadingIndicator/style.lua | 29 +- .../Packages/Framework/UI/RadioButton.lua | 19 +- .../Framework/UI/RadioButton/example.lua | 22 +- .../Framework/UI/RadioButton/style.lua | 65 +- .../Framework/UI/RadioButton/test.spec.lua | 22 +- .../Packages/Framework/UI/RadioButtonList.lua | 17 +- .../Framework/UI/RadioButtonList/example.lua | 22 +- .../Framework/UI/RadioButtonList/style.lua | 25 +- .../Packages/Framework/UI/RangeSlider.lua | 17 +- .../Framework/UI/RangeSlider/example.lua | 25 +- .../Framework/UI/RangeSlider/style.lua | 116 ++-- .../Packages/Framework/UI/RoundBox.lua | 24 +- .../Packages/Framework/UI/RoundBox/style.lua | 35 +- .../Framework/UI/RoundBox/test.spec.lua | 22 +- .../Packages/Framework/UI/ScrollingFrame.lua | 58 +- .../Framework/UI/ScrollingFrame/example.lua | 28 +- .../Framework/UI/ScrollingFrame/style.lua | 32 +- .../Packages/Framework/UI/SelectInput.lua | 19 +- .../Framework/UI/SelectInput/style.lua | 89 ++- .../Packages/Framework/UI/Separator.lua | 24 +- .../Packages/Framework/UI/Separator/style.lua | 31 +- .../Framework/UI/Slider/test.spec.lua | 4 +- .../Packages/Framework/UI/TextInput.lua | 25 +- .../Packages/Framework/UI/TextInput/style.lua | 75 ++- .../Packages/Framework/UI/TextLabel.lua | 26 +- .../Packages/Framework/UI/TextLabel/style.lua | 29 +- .../Framework/UI/TextLabel/test.spec.lua | 22 +- .../Packages/Framework/UI/ToggleButton.lua | 18 +- .../Framework/UI/ToggleButton/example.lua | 24 +- .../Framework/UI/ToggleButton/style.lua | 119 ++-- .../Framework/UI/ToggleButton/test.spec.lua | 22 +- .../Packages/Framework/UI/Tooltip.lua | 41 +- .../Packages/Framework/UI/Tooltip/style.lua | 41 +- .../Framework/UI/Tooltip/test.spec.lua | 26 +- .../Packages/Framework/UI/TreeView.lua | 18 +- .../Packages/Framework/UI/TreeView/style.lua | 35 +- .../Framework/UI/TreeView/test.spec.lua | 22 +- .../Packages/Framework/Util.lua | 6 +- .../Util/CrossPluginCommunication.lua | 6 +- .../Util/CrossPluginCommunication.spec.lua | 8 +- .../Packages/Framework/Util/Flags.lua | 4 +- .../Packages/Framework/Util/Palette.spec.lua | 13 +- .../Packages/Framework/Util/Promise.lua | 78 ++- .../Packages/Framework/Util/Promise.spec.lua | 10 - .../Packages/Framework/Util/StyleModifier.lua | 45 +- .../Packages/Framework/Util/Symbol.lua | 2 - .../Packages/Framework/Util/Symbol.spec.lua | 4 +- .../Framework/Util/Typecheck/DocParser.lua | 1 + .../Util/Typecheck/FrameworkTypes.lua | 21 +- .../Packages/Framework/Util/Typecheck/t.lua | 2 + .../Framework/Util/Typecheck/wrap.lua | 12 +- .../Framework/Util/Typecheck/wrap.spec.lua | 43 +- .../Packages/Framework/Util/deepCopy.lua | 22 + .../Packages/Framework/Util/deepCopy.spec.lua | 37 ++ .../Packages/Framework/Util/deepJoin.lua | 31 + .../Packages/Framework/Util/deepJoin.spec.lua | 76 +++ .../Framework/Util/getTestVariation.lua | 30 + .../Framework/Util/getTestVariation.spec.lua | 41 ++ .../Packages/Framework/Util/tableCache.lua | 31 + .../Framework/Util/tableCache.spec.lua | 40 ++ .../PlayerEmulator/Packages/UILibrary.lua | 21 +- .../Packages/UILibrary/_internal/Camera.lua | 29 + .../_internal/Components/BaseDialog.lua | 128 ++++ .../_internal/Components/BaseDialog.spec.lua | 121 ++++ .../_internal/Components/BulletPoint.lua | 93 +++ .../_internal/Components/BulletPoint.spec.lua | 36 ++ .../UILibrary/_internal/Components/Button.lua | 142 +++++ .../_internal/Components/Button.spec.lua | 79 +++ .../_internal/Components/CheckBox.lua | 96 +++ .../_internal/Components/CheckBox.spec.lua | 64 ++ .../_internal/Components/DetailedDropdown.lua | 353 +++++++++++ .../Components/DetailedDropdown.spec.lua | 34 ++ .../_internal/Components/DragTarget.lua | 58 ++ .../_internal/Components/DragTarget.spec.lua | 38 ++ .../_internal/Components/DropShadow.lua | 58 ++ .../_internal/Components/DropShadow.spec.lua | 30 + .../_internal/Components/DropdownMenu.lua | 241 ++++++++ .../Components/DropdownMenu.spec.lua | 228 +++++++ .../_internal/Components/ExpandableList.lua | 108 ++++ .../Components/ExpandableList.spec.lua | 143 +++++ .../Components/InfiniteScrollingFrame.lua | 136 +++++ .../InfiniteScrollingFrame.spec.lua | 69 +++ .../_internal/Components/LoadingBar.lua | 115 ++++ .../_internal/Components/LoadingIndicator.lua | 139 +++++ .../Components/LoadingIndicator.spec.lua | 16 + .../Components/MultilineTextEntry.lua | 143 +++++ .../Components/MultilineTextEntry.spec.lua | 47 ++ .../Components/PluginWidget/Dialog.lua | 93 +++ .../Components/Preview/ActionBar.lua | 192 ++++++ .../Components/Preview/ActionBar.spec.lua | 26 + .../Components/Preview/AssetDescription.lua | 98 +++ .../Preview/AssetDescription.spec.lua | 28 + .../Components/Preview/AssetPreview.lua | 512 ++++++++++++++++ .../Components/Preview/AssetPreview.spec.lua | 71 +++ .../Components/Preview/AudioControl.lua | 137 +++++ .../Components/Preview/AudioControl.spec.lua | 41 ++ .../Components/Preview/AudioPreview.lua | 356 +++++++++++ .../Components/Preview/AudioPreview.spec.lua | 25 + .../Components/Preview/Favorites.lua | 122 ++++ .../Components/Preview/Favorites.spec.lua | 78 +++ .../Components/Preview/ImagePreview.lua | 57 ++ .../Components/Preview/ImagePreview.spec.lua | 27 + .../Preview/InstanceTreeViewItem.lua | 161 +++++ .../Preview/InstanceTreeViewItem.spec.lua | 44 ++ .../Components/Preview/MediaControl.lua | 128 ++++ .../Components/Preview/MediaControl.spec.lua | 34 ++ .../Components/Preview/MediaProgressBar.lua | 179 ++++++ .../Preview/MediaProgressBar.spec.lua | 30 + .../Components/Preview/ModelPreview.lua | 216 +++++++ .../Components/Preview/ModelPreview.spec.lua | 28 + .../Components/Preview/PreviewController.lua | 350 +++++++++++ .../Preview/PreviewController.spec.lua | 36 ++ .../Components/Preview/SearchLinkText.lua | 141 +++++ .../Preview/SearchLinkText.spec.lua | 51 ++ .../Preview/ThumbnailIconPreview.lua | 91 +++ .../Preview/ThumbnailIconPreview.spec.lua | 27 + .../Components/Preview/TreeViewButton.lua | 143 +++++ .../Preview/TreeViewButton.spec.lua | 26 + .../Components/Preview/VideoPreview.lua | 271 ++++++++ .../Components/Preview/VideoPreview.spec.lua | 26 + .../_internal/Components/Preview/Vote.lua | 272 +++++++++ .../Components/Preview/Vote.spec.lua | 32 + .../Components/Preview/wrapDraggableMedia.lua | 67 ++ .../Preview/wrapDraggableMedia.spec.lua | 38 ++ .../Components/Preview/wrapMedia.lua | 116 ++++ .../Components/Preview/wrapMedia.spec.lua | 49 ++ .../_internal/Components/RadioButtons.lua | 140 +++++ .../Components/RadioButtons.spec.lua | 47 ++ .../_internal/Components/RoundFrame.lua | 107 ++++ .../_internal/Components/RoundFrame.spec.lua | 79 +++ .../_internal/Components/RoundTextBox.lua | 194 ++++++ .../Components/RoundTextBox.spec.lua | 71 +++ .../_internal/Components/RoundTextButton.lua | 118 ++++ .../Components/RoundTextButton.spec.lua | 35 ++ .../_internal/Components/SearchBar.lua | 483 +++++++++++++++ .../_internal/Components/SearchBar.spec.lua | 59 ++ .../_internal/Components/Separator.lua | 51 ++ .../_internal/Components/Separator.spec.lua | 30 + .../_internal/Components/StyledDialog.lua | 105 ++++ .../Components/StyledDialog.spec.lua | 94 +++ .../_internal/Components/StyledDropdown.lua | 281 +++++++++ .../Components/StyledDropdown.spec.lua | 34 ++ .../Components/StyledScrollingFrame.lua | 98 +++ .../Components/StyledScrollingFrame.spec.lua | 62 ++ .../_internal/Components/StyledTooltip.lua | 196 ++++++ .../Components/StyledTooltip.spec.lua | 30 + .../_internal/Components/TextEntry.lua | 137 +++++ .../_internal/Components/TextEntry.spec.lua | 44 ++ .../Components/Timeline/Keyframe.lua | 74 +++ .../Components/Timeline/Keyframe.spec.lua | 31 + .../Components/Timeline/Scrubber.lua | 66 ++ .../Components/Timeline/Scrubber.spec.lua | 54 ++ .../_internal/Components/TitledFrame.lua | 57 ++ .../_internal/Components/TitledFrame.spec.lua | 34 ++ .../_internal/Components/ToggleButton.lua | 61 ++ .../Components/ToggleButton.spec.lua | 30 + .../_internal/Components/Tooltip.lua | 193 ++++++ .../_internal/Components/Tooltip.spec.lua | 30 + .../_internal/Components/TreeView.lua | 524 ++++++++++++++++ .../_internal/Components/TreeView.spec.lua | 355 +++++++++++ .../Components/createFitToContent.lua | 70 +++ .../Components/createFitToContent.spec.lua | 44 ++ .../Packages/UILibrary/_internal/Focus.lua | 181 ++++++ .../UILibrary/_internal/Focus.spec.lua | 118 ++++ .../UILibrary/_internal/Localizing.lua | 98 +++ .../UILibrary/_internal/Localizing.spec.lua | 157 +++++ .../UILibrary/_internal/MockWrapper.lua | 44 ++ .../Packages/UILibrary/_internal/Plugin.lua | 28 + .../UILibrary/_internal/Studio/Analytics.lua | 123 ++++ .../_internal/Studio/ContextMenus.lua | 42 ++ .../UILibrary/_internal/Studio/Hyperlink.lua | 60 ++ .../_internal/Studio/Internal/Mouse.lua | 19 + .../_internal/Studio/Localization.lua | 284 +++++++++ .../_internal/Studio/Localization.spec.lua | 228 +++++++ .../_internal/Studio/PartialHyperLink.lua | 39 ++ .../_internal/Studio/PluginMenus.lua | 131 ++++ .../_internal/Studio/StudioStyle.lua | 54 ++ .../_internal/Studio/StudioStyle.spec.lua | 35 ++ .../_internal/Studio/StudioTheme.lua | 133 ++++ .../_internal/Studio/StudioTheme.spec.lua | 102 ++++ .../UILibrary/_internal/StyleDefaults.lua | 71 +++ .../Packages/UILibrary/_internal/Theming.lua | 63 ++ .../UILibrary/_internal/UILibraryWrapper.lua | 69 +++ .../_internal/UILibraryWrapper.spec.lua | 86 +++ .../UILibrary/_internal/Utils/AssetType.lua | 126 ++++ .../_internal/Utils/AssetType.spec.lua | 24 + .../_internal/Utils/GetClassIcon.lua | 31 + .../_internal/Utils/GetClassIcon.spec.lua | 43 ++ .../UILibrary/_internal/Utils/GetTextSize.lua | 20 + .../UILibrary/_internal/Utils/Immutable.lua | 141 +++++ .../_internal/Utils/Immutable.spec.lua | 284 +++++++++ .../_internal/Utils/InsertToolEvent.lua | 51 ++ .../_internal/Utils/LayoutOrderIterator.lua | 37 ++ .../Utils/LayoutOrderIterator.spec.lua | 30 + .../UILibrary/_internal/Utils/MathUtils.lua | 15 + .../_internal/Utils/MathUtils.spec.lua | 40 ++ .../UILibrary/_internal/Utils/Signal.lua | 63 ++ .../UILibrary/_internal/Utils/Signal.spec.lua | 114 ++++ .../UILibrary/_internal/Utils/Spritesheet.lua | 86 +++ .../_internal/Utils/Spritesheet.spec.lua | 115 ++++ .../UILibrary/_internal/Utils/Symbol.lua | 44 ++ .../UILibrary/_internal/Utils/Symbol.spec.lua | 45 ++ .../UILibrary/_internal/Utils/Urls.lua | 43 ++ .../_internal/Utils/getTimeString.lua | 11 + .../_internal/Utils/getTimeString.spec.lua | 51 ++ .../UILibrary/_internal/createTheme.lua | 578 ++++++++++++++++++ .../Packages/UILibrary/_internal/deepJoin.lua | 31 + .../UILibrary/_internal/deepJoin.spec.lua | 76 +++ .../Packages/UILibrary/_internal/join.lua | 15 + .../Src/Components/LanguageSection.lua | 5 +- .../UILibrary/_internal/Utils/Symbol.spec.lua | 4 +- .../TerrainToolsV2/Bin/defineLuaFlags.lua | 6 + .../TerrainToolsV2/Bin/main.server.lua | 19 +- .../Framework/ContextServices/Theme.spec.lua | 7 + .../ContextServices/UILibraryWrapper.spec.lua | 35 +- .../StudioUI/TitledFrame/test.spec.lua | 2 +- .../Framework/StudioUI/createPluginWidget.lua | 19 +- .../Packages/Framework/Style/Stylizer.lua | 4 +- .../Framework/Style/getRawComponentStyle.lua | 1 + .../TestHelpers/provideMockContext.lua | 2 +- .../Packages/Framework/UI/Box/test.spec.lua | 2 +- .../Framework/UI/Button/test.spec.lua | 2 +- .../Framework/UI/Container/test.spec.lua | 2 +- .../Packages/Framework/UI/DropdownMenu.lua | 3 +- .../Framework/UI/FakeLoadingBar/test.spec.lua | 2 +- .../Packages/Framework/UI/Image/test.spec.lua | 2 +- .../UI/InstanceTreeView/InstanceTreeRow.lua | 11 +- .../Framework/UI/LinkText/test.spec.lua | 2 +- .../Framework/UI/LoadingBar/test.spec.lua | 2 +- .../Framework/UI/RadioButton/test.spec.lua | 2 +- .../Framework/UI/RoundBox/test.spec.lua | 2 +- .../Framework/UI/TextLabel/test.spec.lua | 2 +- .../Framework/UI/ToggleButton/test.spec.lua | 2 +- .../Framework/UI/Tooltip/test.spec.lua | 2 +- .../Framework/UI/TreeView/test.spec.lua | 2 +- .../Packages/Framework/Util.lua | 1 + .../Util/CrossPluginCommunication.lua | 6 +- .../Util/CrossPluginCommunication.spec.lua | 8 +- .../Packages/Framework/Util/Palette.spec.lua | 13 +- .../Packages/Framework/Util/Typecheck/t.lua | 2 + .../Framework/Util/getTestVariation.lua | 30 + .../Framework/Util/getTestVariation.spec.lua | 41 ++ .../UILibrary/_internal/Utils/Symbol.spec.lua | 4 +- .../Src/Actions/SelectColormap.lua | 7 + .../Src/Actions/SelectHeightmap.lua | 7 + .../Src/Components/TerrainTools.lua | 18 +- .../Src/Components/ToolManager.lua | 3 +- .../Src/Components/ToolRenderer.lua | 2 + .../Src/Components/Tools/Clear.lua | 2 + .../Src/Components/Tools/ImportLocal.lua | 204 +++++++ .../Src/Components/Tools/ImportLocal.spec.lua | 19 + .../Tools/ToolParts/LocalImageSelector.lua | 89 +++ .../ToolParts/LocalImageSelector.spec.lua | 19 + .../Tools/ToolParts/MapSettingsFragment.lua | 5 + .../Tools/ToolParts/MaterialSelector.lua | 4 +- .../ToolParts/PromptSelectorWithPreview.lua | 328 ++++++++++ .../PromptSelectorWithPreview.spec.lua | 19 + .../ToolParts/SingleSelectButtonGroup.lua | 50 +- .../Src/Reducers/ImportLocalTool.lua | 59 ++ .../Src/Reducers/ImportLocalTool.spec.lua | 153 +++++ .../Src/Reducers/MainReducer.lua | 4 + .../Src/Resources/PluginTheme.lua | 27 + .../TerrainImporterInstance.lua | 12 +- .../TerrainToolsV2/Src/Util/Constants.lua | 2 + .../TerrainToolsV2/Src/Util/TerrainEnums.lua | 3 + .../Toolbox/Core/Components/ToolboxPlugin.lua | 23 +- .../Requests/ChangeMarketplaceTab.lua | 3 +- .../Toolbox/Core/Types/Category.lua | 12 +- .../Toolbox/Core/Util/InsertAsset.lua | 2 +- .../Framework/ContextServices/Theme.spec.lua | 7 + .../ContextServices/UILibraryWrapper.spec.lua | 35 +- .../StudioUI/TitledFrame/test.spec.lua | 2 +- .../Framework/StudioUI/createPluginWidget.lua | 19 +- .../Toolbox/Libs/Framework/Style/Stylizer.lua | 4 +- .../Framework/Style/getRawComponentStyle.lua | 1 + .../TestHelpers/provideMockContext.lua | 2 +- .../Libs/Framework/UI/Box/test.spec.lua | 2 +- .../Libs/Framework/UI/Button/test.spec.lua | 2 +- .../Libs/Framework/UI/Container/test.spec.lua | 2 +- .../Libs/Framework/UI/DropdownMenu.lua | 3 +- .../Framework/UI/FakeLoadingBar/test.spec.lua | 2 +- .../Libs/Framework/UI/Image/test.spec.lua | 2 +- .../UI/InstanceTreeView/InstanceTreeRow.lua | 11 +- .../Libs/Framework/UI/LinkText/test.spec.lua | 2 +- .../Framework/UI/LoadingBar/test.spec.lua | 2 +- .../Framework/UI/RadioButton/test.spec.lua | 2 +- .../Libs/Framework/UI/RoundBox/test.spec.lua | 2 +- .../Libs/Framework/UI/TextLabel/test.spec.lua | 2 +- .../Framework/UI/ToggleButton/test.spec.lua | 2 +- .../Libs/Framework/UI/Tooltip/test.spec.lua | 2 +- .../Libs/Framework/UI/TreeView/test.spec.lua | 2 +- .../Toolbox/Libs/Framework/Util.lua | 1 + .../Util/CrossPluginCommunication.lua | 6 +- .../Util/CrossPluginCommunication.spec.lua | 8 +- .../Libs/Framework/Util/Palette.spec.lua | 13 +- .../Libs/Framework/Util/Typecheck/t.lua | 2 + .../Libs/Framework/Util/getTestVariation.lua | 30 + .../Framework/Util/getTestVariation.spec.lua | 41 ++ .../UILibrary/_internal/Utils/Symbol.spec.lua | 4 +- .../UILibrary/_internal/Utils/Symbol.spec.lua | 4 +- .../Framework/ContextServices/Theme.spec.lua | 7 + .../ContextServices/UILibraryWrapper.spec.lua | 35 +- .../StudioUI/TitledFrame/test.spec.lua | 2 +- .../Framework/StudioUI/createPluginWidget.lua | 19 +- .../Packages/Framework/Style/Stylizer.lua | 4 +- .../Framework/Style/getRawComponentStyle.lua | 1 + .../TestHelpers/provideMockContext.lua | 2 +- .../Packages/Framework/UI/Box/test.spec.lua | 2 +- .../Framework/UI/Button/test.spec.lua | 2 +- .../Framework/UI/Container/test.spec.lua | 2 +- .../Packages/Framework/UI/DropdownMenu.lua | 3 +- .../Framework/UI/FakeLoadingBar/test.spec.lua | 2 +- .../Packages/Framework/UI/Image/test.spec.lua | 2 +- .../UI/InstanceTreeView/InstanceTreeRow.lua | 11 +- .../Framework/UI/LinkText/test.spec.lua | 2 +- .../Framework/UI/LoadingBar/test.spec.lua | 2 +- .../Framework/UI/RadioButton/test.spec.lua | 2 +- .../Framework/UI/RoundBox/test.spec.lua | 2 +- .../Framework/UI/TextLabel/test.spec.lua | 2 +- .../Framework/UI/ToggleButton/test.spec.lua | 2 +- .../Framework/UI/Tooltip/test.spec.lua | 2 +- .../Framework/UI/TreeView/test.spec.lua | 2 +- .../Packages/Framework/Util.lua | 1 + .../Util/CrossPluginCommunication.lua | 6 +- .../Util/CrossPluginCommunication.spec.lua | 8 +- .../Packages/Framework/Util/Palette.spec.lua | 13 +- .../Packages/Framework/Util/Typecheck/t.lua | 2 + .../Framework/Util/getTestVariation.lua | 30 + .../Framework/Util/getTestVariation.spec.lua | 41 ++ .../UILibrary/_internal/Utils/Symbol.spec.lua | 4 +- .../TransformDragger/Precision.server.lua | 14 +- .../Standalone/main.server.lua | 13 + .../AppTempCommon/LuaApp/Style/Colors.lua | 1 + .../LuaApp/Style/Themes/DarkTheme.lua | 5 + .../LuaApp/Style/Themes/LightTheme.lua | 5 + .../Regulations/ScreenTime/Constants.lua | 6 + .../ScreenTime/GetFFlagScreenTime.lua | 5 + .../Regulations/ScreenTime/HttpRequests.lua | 183 ++++++ .../ScreenTime/HttpRequests.spec.lua | 250 ++++++++ ...tFFlagLuaAppUseNewUIBloxRoundedCorners.lua | 5 + .../GetFFlagLuaAppUseUIBloxToasts.lua | 5 - LuaPackages/UIBloxUniversalAppConfig.lua | 5 +- .../CoreScripts/CoreScripts/InGameChat.lua | 2 +- .../CoreScripts/NotificationScript2.lua | 11 +- .../CoreScripts/ScreenTimeInGame.lua | 63 +- .../Actions/CloseOpenPrompt.lua | 7 + .../Actions/GameNameFetched.lua | 9 + .../Actions/OpenPrompt.lua | 10 + .../Actions/ScreenSizeUpdated.lua | 9 + .../Components/AvatarEditorPromptsApp.lua | 81 +++ .../AvatarEditorServiceConnector.lua | 79 +++ .../Connection/ContextActionsBinder.lua | 106 ++++ .../Components/Connection/init.lua | 17 + .../Components/HumanoidViewport.lua | 220 +++++++ .../Components/ItemsList.lua | 99 +++ .../AllowInventoryReadAccessPrompt.lua | 78 +++ .../AllowInventoryReadAccessPrompt.spec.lua | 46 ++ .../Components/Prompts/CreateOutfitPrompt.lua | 201 ++++++ .../Prompts/CreateOutfitPrompt.spec.lua | 51 ++ .../Components/Prompts/SaveAvatarPrompt.lua | 162 +++++ .../Prompts/SaveAvatarPrompt.spec.lua | 55 ++ .../Components/Prompts/SetFavoritePrompt.lua | 132 ++++ .../Prompts/SetFavoritePrompt.spec.lua | 52 ++ .../GetAssetNamesFromDescription.lua | 75 +++ .../AvatarEditorPrompts/PromptType.lua | 9 + .../AvatarEditorPrompts/Reducer/GameName.lua | 19 + .../Reducer/PromptInfo.lua | 57 ++ .../Reducer/PromptInfo.spec.lua | 190 ++++++ .../Reducer/ScreenSize.lua | 16 + .../Reducer/ScreenSize.spec.lua | 21 + .../AvatarEditorPrompts/Reducer/init.lua | 15 + .../AvatarEditorPrompts/RoactGlobalConfig.lua | 4 + .../Thunks/CloseOpenPrompt.lua | 22 + .../Thunks/GetGameName.lua | 40 ++ .../Thunks/OpenSaveAvatarPrompt.lua | 28 + .../Thunks/OpenSetFavoritePrompt.lua | 45 ++ .../Thunks/PerformCreateOutfit.lua | 12 + .../Thunks/PerformSaveAvatar.lua | 10 + .../Thunks/PerformSetFavorite.lua | 10 + .../Thunks/SetAllowInventoryReadAccess.lua | 12 + .../SignalCreateOutfitPermissionDenied.lua | 10 + .../SignalSaveAvatarPermissionDenied.lua | 10 + .../SignalSetFavoritePermissionDenied.lua | 10 + .../Modules/AvatarEditorPrompts/init.lua | 62 ++ .../Modules/AvatarEditorPrompts/init.spec.lua | 6 + .../Modules/Flags/GetFixGraphicsQuality.lua | 5 + .../InGameChat/BubbleChat/ChatSettings.lua | 28 +- .../Components/BubbleChatBillboard.lua | 44 +- .../Components/BubbleChatBillboards.lua | 24 +- .../BubbleChat/Components/BubbleChatList.lua | 33 +- .../BubbleChat/Components/ChatBubble.lua | 42 +- .../Components/ChatBubbleDistant.lua | 55 +- .../__stories__/ChatBubble.story.lua | 10 +- .../Components/__stories__/Themes.story.lua | 10 +- .../InGameChat/BubbleChat/Constants.lua | 6 - .../Modules/InGameChat/BubbleChat/Types.lua | 12 + .../GameSettingsPage/GraphicsQualityEntry.lua | 164 ++++- .../GraphicsQualityEntry.spec.lua | 40 +- .../ClientChat/BubbleChat/BubbleChat.lua | 14 +- .../Modules/Settings/Pages/GameSettings.lua | 324 ++++++++-- .../ShareGame/Components/ConversationList.lua | 14 +- .../ShareGame/isSelectionGroupEnabled.lua | 4 +- scripts/CoreScripts/StarterScript.lua | 12 +- .../CameraModule/CameraInput.lua | 6 +- .../CameraModule/LegacyCamera.lua | 2 +- 1189 files changed, 63370 insertions(+), 3121 deletions(-) create mode 100644 BuiltInPlugins/AnimationClipEditor/LuaFlags/GetFFlagHideLoadToastIfAnimationClipped.lua create mode 100644 BuiltInPlugins/AssetConfiguration/Libs/Framework/Util/getTestVariation.lua create mode 100644 BuiltInPlugins/AssetConfiguration/Libs/Framework/Util/getTestVariation.spec.lua create mode 100644 BuiltInPlugins/AssetManager/Packages/Framework/Util/getTestVariation.lua create mode 100644 BuiltInPlugins/AssetManager/Packages/Framework/Util/getTestVariation.spec.lua create mode 100644 BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Camera.lua create mode 100644 BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Components/BaseDialog.lua create mode 100644 BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Components/BaseDialog.spec.lua create mode 100644 BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Components/BulletPoint.lua create mode 100644 BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Components/BulletPoint.spec.lua create mode 100644 BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Components/Button.lua create mode 100644 BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Components/Button.spec.lua create mode 100644 BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Components/CheckBox.lua create mode 100644 BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Components/CheckBox.spec.lua create mode 100644 BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Components/DetailedDropdown.lua create mode 100644 BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Components/DetailedDropdown.spec.lua create mode 100644 BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Components/DragTarget.lua create mode 100644 BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Components/DragTarget.spec.lua create mode 100644 BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Components/DropShadow.lua create mode 100644 BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Components/DropShadow.spec.lua create mode 100644 BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Components/DropdownMenu.lua create mode 100644 BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Components/DropdownMenu.spec.lua create mode 100644 BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Components/ExpandableList.lua create mode 100644 BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Components/ExpandableList.spec.lua create mode 100644 BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Components/InfiniteScrollingFrame.lua create mode 100644 BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Components/InfiniteScrollingFrame.spec.lua create mode 100644 BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Components/LoadingBar.lua create mode 100644 BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Components/LoadingIndicator.lua create mode 100644 BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Components/LoadingIndicator.spec.lua create mode 100644 BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Components/MultilineTextEntry.lua create mode 100644 BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Components/MultilineTextEntry.spec.lua create mode 100644 BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Components/PluginWidget/Dialog.lua create mode 100644 BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Components/Preview/ActionBar.lua create mode 100644 BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Components/Preview/ActionBar.spec.lua create mode 100644 BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Components/Preview/AssetDescription.lua create mode 100644 BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Components/Preview/AssetDescription.spec.lua create mode 100644 BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Components/Preview/AssetPreview.lua create mode 100644 BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Components/Preview/AssetPreview.spec.lua create mode 100644 BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Components/Preview/AudioControl.lua create mode 100644 BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Components/Preview/AudioControl.spec.lua create mode 100644 BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Components/Preview/AudioPreview.lua create mode 100644 BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Components/Preview/AudioPreview.spec.lua create mode 100644 BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Components/Preview/Favorites.lua create mode 100644 BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Components/Preview/Favorites.spec.lua create mode 100644 BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Components/Preview/ImagePreview.lua create mode 100644 BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Components/Preview/ImagePreview.spec.lua create mode 100644 BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Components/Preview/InstanceTreeViewItem.lua create mode 100644 BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Components/Preview/InstanceTreeViewItem.spec.lua create mode 100644 BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Components/Preview/MediaControl.lua create mode 100644 BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Components/Preview/MediaControl.spec.lua create mode 100644 BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Components/Preview/MediaProgressBar.lua create mode 100644 BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Components/Preview/MediaProgressBar.spec.lua create mode 100644 BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Components/Preview/ModelPreview.lua create mode 100644 BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Components/Preview/ModelPreview.spec.lua create mode 100644 BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Components/Preview/PreviewController.lua create mode 100644 BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Components/Preview/PreviewController.spec.lua create mode 100644 BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Components/Preview/SearchLinkText.lua create mode 100644 BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Components/Preview/SearchLinkText.spec.lua create mode 100644 BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Components/Preview/ThumbnailIconPreview.lua create mode 100644 BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Components/Preview/ThumbnailIconPreview.spec.lua create mode 100644 BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Components/Preview/TreeViewButton.lua create mode 100644 BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Components/Preview/TreeViewButton.spec.lua create mode 100644 BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Components/Preview/VideoPreview.lua create mode 100644 BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Components/Preview/VideoPreview.spec.lua create mode 100644 BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Components/Preview/Vote.lua create mode 100644 BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Components/Preview/Vote.spec.lua create mode 100644 BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Components/Preview/wrapDraggableMedia.lua create mode 100644 BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Components/Preview/wrapDraggableMedia.spec.lua create mode 100644 BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Components/Preview/wrapMedia.lua create mode 100644 BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Components/Preview/wrapMedia.spec.lua create mode 100644 BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Components/RadioButtons.lua create mode 100644 BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Components/RadioButtons.spec.lua create mode 100644 BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Components/RoundFrame.lua create mode 100644 BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Components/RoundFrame.spec.lua create mode 100644 BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Components/RoundTextBox.lua create mode 100644 BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Components/RoundTextBox.spec.lua create mode 100644 BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Components/RoundTextButton.lua create mode 100644 BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Components/RoundTextButton.spec.lua create mode 100644 BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Components/SearchBar.lua create mode 100644 BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Components/SearchBar.spec.lua create mode 100644 BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Components/Separator.lua create mode 100644 BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Components/Separator.spec.lua create mode 100644 BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Components/StyledDialog.lua create mode 100644 BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Components/StyledDialog.spec.lua create mode 100644 BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Components/StyledDropdown.lua create mode 100644 BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Components/StyledDropdown.spec.lua create mode 100644 BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Components/StyledScrollingFrame.lua create mode 100644 BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Components/StyledScrollingFrame.spec.lua create mode 100644 BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Components/StyledTooltip.lua create mode 100644 BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Components/StyledTooltip.spec.lua create mode 100644 BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Components/TextEntry.lua create mode 100644 BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Components/TextEntry.spec.lua create mode 100644 BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Components/Timeline/Keyframe.lua create mode 100644 BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Components/Timeline/Keyframe.spec.lua create mode 100644 BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Components/Timeline/Scrubber.lua create mode 100644 BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Components/Timeline/Scrubber.spec.lua create mode 100644 BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Components/TitledFrame.lua create mode 100644 BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Components/TitledFrame.spec.lua create mode 100644 BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Components/ToggleButton.lua create mode 100644 BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Components/ToggleButton.spec.lua create mode 100644 BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Components/Tooltip.lua create mode 100644 BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Components/Tooltip.spec.lua create mode 100644 BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Components/TreeView.lua create mode 100644 BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Components/TreeView.spec.lua create mode 100644 BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Components/createFitToContent.lua create mode 100644 BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Components/createFitToContent.spec.lua create mode 100644 BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Focus.lua create mode 100644 BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Focus.spec.lua create mode 100644 BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Localizing.lua create mode 100644 BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Localizing.spec.lua create mode 100644 BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/MockWrapper.lua create mode 100644 BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Plugin.lua create mode 100644 BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Studio/Analytics.lua create mode 100644 BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Studio/ContextMenus.lua create mode 100644 BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Studio/Hyperlink.lua create mode 100644 BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Studio/Internal/Mouse.lua create mode 100644 BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Studio/Localization.lua create mode 100644 BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Studio/Localization.spec.lua create mode 100644 BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Studio/PartialHyperLink.lua create mode 100644 BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Studio/PluginMenus.lua create mode 100644 BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Studio/StudioStyle.lua create mode 100644 BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Studio/StudioStyle.spec.lua create mode 100644 BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Studio/StudioTheme.lua create mode 100644 BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Studio/StudioTheme.spec.lua create mode 100644 BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/StyleDefaults.lua create mode 100644 BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Theming.lua create mode 100644 BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/UILibraryWrapper.lua create mode 100644 BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/UILibraryWrapper.spec.lua create mode 100644 BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Utils/AssetType.lua create mode 100644 BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Utils/AssetType.spec.lua create mode 100644 BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Utils/GetClassIcon.lua create mode 100644 BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Utils/GetClassIcon.spec.lua create mode 100644 BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Utils/GetTextSize.lua create mode 100644 BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Utils/Immutable.lua create mode 100644 BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Utils/Immutable.spec.lua create mode 100644 BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Utils/InsertToolEvent.lua create mode 100644 BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Utils/LayoutOrderIterator.lua create mode 100644 BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Utils/LayoutOrderIterator.spec.lua create mode 100644 BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Utils/MathUtils.lua create mode 100644 BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Utils/MathUtils.spec.lua create mode 100644 BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Utils/Signal.lua create mode 100644 BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Utils/Signal.spec.lua create mode 100644 BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Utils/Spritesheet.lua create mode 100644 BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Utils/Spritesheet.spec.lua create mode 100644 BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Utils/Symbol.lua create mode 100644 BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Utils/Symbol.spec.lua create mode 100644 BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Utils/Urls.lua create mode 100644 BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Utils/getTimeString.lua create mode 100644 BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Utils/getTimeString.spec.lua create mode 100644 BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/createTheme.lua create mode 100644 BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/deepJoin.lua create mode 100644 BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/deepJoin.spec.lua create mode 100644 BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/join.lua create mode 100644 BuiltInPlugins/EventEmulator/Packages/Framework/Util/getTestVariation.lua create mode 100644 BuiltInPlugins/EventEmulator/Packages/Framework/Util/getTestVariation.spec.lua create mode 100644 BuiltInPlugins/FrameworkCompanion/Packages/DEPRECATED_Framework/Util/getTestVariation.lua create mode 100644 BuiltInPlugins/FrameworkCompanion/Packages/DEPRECATED_Framework/Util/getTestVariation.spec.lua create mode 100644 BuiltInPlugins/FrameworkCompanion/Src/Util/DebugFlags.lua create mode 100644 BuiltInPlugins/FrameworkCompanion/Src/Util/commonInit.lua create mode 100644 BuiltInPlugins/GameSettings/Framework/Util/getTestVariation.lua create mode 100644 BuiltInPlugins/GameSettings/Framework/Util/getTestVariation.spec.lua create mode 100644 BuiltInPlugins/LocalizationTools/Packages/Framework/Examples/General/stylizer.lua create mode 100644 BuiltInPlugins/LocalizationTools/Packages/Framework/Examples/General/stylizer/Application.lua create mode 100644 BuiltInPlugins/LocalizationTools/Packages/Framework/Examples/General/stylizer/Box.lua create mode 100644 BuiltInPlugins/LocalizationTools/Packages/Framework/Examples/General/stylizer/Button.lua create mode 100644 BuiltInPlugins/LocalizationTools/Packages/Framework/Examples/General/stylizer/Dialog.lua create mode 100644 BuiltInPlugins/LocalizationTools/Packages/Framework/RobloxAPI/ToolboxService/V1/Items/details.lua create mode 100644 BuiltInPlugins/LocalizationTools/Packages/Framework/Style.lua create mode 100644 BuiltInPlugins/LocalizationTools/Packages/Framework/Style/Colors.lua create mode 100644 BuiltInPlugins/LocalizationTools/Packages/Framework/Style/ComponentSymbols.lua create mode 100644 BuiltInPlugins/LocalizationTools/Packages/Framework/Style/ComponentSymbols.spec.lua create mode 100644 BuiltInPlugins/LocalizationTools/Packages/Framework/Style/StyleKey.lua create mode 100644 BuiltInPlugins/LocalizationTools/Packages/Framework/Style/StyleKey.spec.lua create mode 100644 BuiltInPlugins/LocalizationTools/Packages/Framework/Style/Stylizer.lua create mode 100644 BuiltInPlugins/LocalizationTools/Packages/Framework/Style/Stylizer.spec.lua create mode 100644 BuiltInPlugins/LocalizationTools/Packages/Framework/Style/Themes/BaseTheme.lua create mode 100644 BuiltInPlugins/LocalizationTools/Packages/Framework/Style/Themes/DarkTheme.lua create mode 100644 BuiltInPlugins/LocalizationTools/Packages/Framework/Style/Themes/LightTheme.lua create mode 100644 BuiltInPlugins/LocalizationTools/Packages/Framework/Style/Themes/StudioTheme.lua create mode 100644 BuiltInPlugins/LocalizationTools/Packages/Framework/Style/Themes/StudioTheme.spec.lua create mode 100644 BuiltInPlugins/LocalizationTools/Packages/Framework/Style/createDefaultTheme.lua create mode 100644 BuiltInPlugins/LocalizationTools/Packages/Framework/Style/createDefaultTheme.spec.lua create mode 100644 BuiltInPlugins/LocalizationTools/Packages/Framework/Style/getRawComponentStyle.lua create mode 100644 BuiltInPlugins/LocalizationTools/Packages/Framework/Style/getRawComponentStyle.spec.lua create mode 100644 BuiltInPlugins/LocalizationTools/Packages/Framework/TestHelpers/Instances.lua create mode 100644 BuiltInPlugins/LocalizationTools/Packages/Framework/TestHelpers/Instances/MockAnalyticsService.lua create mode 100644 BuiltInPlugins/LocalizationTools/Packages/Framework/TestHelpers/Instances/MockMouse.lua create mode 100644 BuiltInPlugins/LocalizationTools/Packages/Framework/TestHelpers/Instances/MockPlugin.lua create mode 100644 BuiltInPlugins/LocalizationTools/Packages/Framework/TestHelpers/Instances/MockPlugin.spec.lua create mode 100644 BuiltInPlugins/LocalizationTools/Packages/Framework/TestHelpers/Instances/MockPluginToolbar.lua create mode 100644 BuiltInPlugins/LocalizationTools/Packages/Framework/TestHelpers/Instances/MockPluginToolbar.spec.lua create mode 100644 BuiltInPlugins/LocalizationTools/Packages/Framework/TestHelpers/Instances/MockPluginToolbarButton.lua create mode 100644 BuiltInPlugins/LocalizationTools/Packages/Framework/TestHelpers/Instances/MockPluginToolbarButton.spec.lua create mode 100644 BuiltInPlugins/LocalizationTools/Packages/Framework/TestHelpers/Instances/MockSelectionService.lua create mode 100644 BuiltInPlugins/LocalizationTools/Packages/Framework/TestHelpers/Instances/MockStudioService.lua create mode 100644 BuiltInPlugins/LocalizationTools/Packages/Framework/TestHelpers/makeSettableValue.lua create mode 100644 BuiltInPlugins/LocalizationTools/Packages/Framework/TestHelpers/makeSettableValue.spec.lua create mode 100644 BuiltInPlugins/LocalizationTools/Packages/Framework/TestHelpers/setEquals.lua create mode 100644 BuiltInPlugins/LocalizationTools/Packages/Framework/TestHelpers/testImmutability.lua create mode 100644 BuiltInPlugins/LocalizationTools/Packages/Framework/TestHelpers/testImmutability.spec.lua create mode 100644 BuiltInPlugins/LocalizationTools/Packages/Framework/Util/deepCopy.lua create mode 100644 BuiltInPlugins/LocalizationTools/Packages/Framework/Util/deepCopy.spec.lua create mode 100644 BuiltInPlugins/LocalizationTools/Packages/Framework/Util/deepJoin.lua create mode 100644 BuiltInPlugins/LocalizationTools/Packages/Framework/Util/deepJoin.spec.lua create mode 100644 BuiltInPlugins/LocalizationTools/Packages/Framework/Util/getTestVariation.lua create mode 100644 BuiltInPlugins/LocalizationTools/Packages/Framework/Util/getTestVariation.spec.lua create mode 100644 BuiltInPlugins/LocalizationTools/Packages/Framework/Util/tableCache.lua create mode 100644 BuiltInPlugins/LocalizationTools/Packages/Framework/Util/tableCache.spec.lua create mode 100644 BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Camera.lua create mode 100644 BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Components/BaseDialog.lua create mode 100644 BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Components/BaseDialog.spec.lua create mode 100644 BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Components/BulletPoint.lua create mode 100644 BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Components/BulletPoint.spec.lua create mode 100644 BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Components/Button.lua create mode 100644 BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Components/Button.spec.lua create mode 100644 BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Components/CheckBox.lua create mode 100644 BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Components/CheckBox.spec.lua create mode 100644 BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Components/DetailedDropdown.lua create mode 100644 BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Components/DetailedDropdown.spec.lua create mode 100644 BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Components/DragTarget.lua create mode 100644 BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Components/DragTarget.spec.lua create mode 100644 BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Components/DropShadow.lua create mode 100644 BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Components/DropShadow.spec.lua create mode 100644 BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Components/DropdownMenu.lua create mode 100644 BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Components/DropdownMenu.spec.lua create mode 100644 BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Components/ExpandableList.lua create mode 100644 BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Components/ExpandableList.spec.lua create mode 100644 BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Components/InfiniteScrollingFrame.lua create mode 100644 BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Components/InfiniteScrollingFrame.spec.lua create mode 100644 BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Components/LoadingBar.lua create mode 100644 BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Components/LoadingIndicator.lua create mode 100644 BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Components/LoadingIndicator.spec.lua create mode 100644 BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Components/MultilineTextEntry.lua create mode 100644 BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Components/MultilineTextEntry.spec.lua create mode 100644 BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Components/PluginWidget/Dialog.lua create mode 100644 BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Components/Preview/ActionBar.lua create mode 100644 BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Components/Preview/ActionBar.spec.lua create mode 100644 BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Components/Preview/AssetDescription.lua create mode 100644 BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Components/Preview/AssetDescription.spec.lua create mode 100644 BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Components/Preview/AssetPreview.lua create mode 100644 BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Components/Preview/AssetPreview.spec.lua create mode 100644 BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Components/Preview/AudioControl.lua create mode 100644 BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Components/Preview/AudioControl.spec.lua create mode 100644 BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Components/Preview/AudioPreview.lua create mode 100644 BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Components/Preview/AudioPreview.spec.lua create mode 100644 BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Components/Preview/Favorites.lua create mode 100644 BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Components/Preview/Favorites.spec.lua create mode 100644 BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Components/Preview/ImagePreview.lua create mode 100644 BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Components/Preview/ImagePreview.spec.lua create mode 100644 BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Components/Preview/InstanceTreeViewItem.lua create mode 100644 BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Components/Preview/InstanceTreeViewItem.spec.lua create mode 100644 BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Components/Preview/MediaControl.lua create mode 100644 BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Components/Preview/MediaControl.spec.lua create mode 100644 BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Components/Preview/MediaProgressBar.lua create mode 100644 BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Components/Preview/MediaProgressBar.spec.lua create mode 100644 BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Components/Preview/ModelPreview.lua create mode 100644 BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Components/Preview/ModelPreview.spec.lua create mode 100644 BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Components/Preview/PreviewController.lua create mode 100644 BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Components/Preview/PreviewController.spec.lua create mode 100644 BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Components/Preview/SearchLinkText.lua create mode 100644 BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Components/Preview/SearchLinkText.spec.lua create mode 100644 BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Components/Preview/ThumbnailIconPreview.lua create mode 100644 BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Components/Preview/ThumbnailIconPreview.spec.lua create mode 100644 BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Components/Preview/TreeViewButton.lua create mode 100644 BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Components/Preview/TreeViewButton.spec.lua create mode 100644 BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Components/Preview/VideoPreview.lua create mode 100644 BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Components/Preview/VideoPreview.spec.lua create mode 100644 BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Components/Preview/Vote.lua create mode 100644 BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Components/Preview/Vote.spec.lua create mode 100644 BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Components/Preview/wrapDraggableMedia.lua create mode 100644 BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Components/Preview/wrapDraggableMedia.spec.lua create mode 100644 BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Components/Preview/wrapMedia.lua create mode 100644 BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Components/Preview/wrapMedia.spec.lua create mode 100644 BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Components/RadioButtons.lua create mode 100644 BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Components/RadioButtons.spec.lua create mode 100644 BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Components/RoundFrame.lua create mode 100644 BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Components/RoundFrame.spec.lua create mode 100644 BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Components/RoundTextBox.lua create mode 100644 BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Components/RoundTextBox.spec.lua create mode 100644 BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Components/RoundTextButton.lua create mode 100644 BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Components/RoundTextButton.spec.lua create mode 100644 BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Components/SearchBar.lua create mode 100644 BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Components/SearchBar.spec.lua create mode 100644 BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Components/Separator.lua create mode 100644 BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Components/Separator.spec.lua create mode 100644 BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Components/StyledDialog.lua create mode 100644 BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Components/StyledDialog.spec.lua create mode 100644 BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Components/StyledDropdown.lua create mode 100644 BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Components/StyledDropdown.spec.lua create mode 100644 BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Components/StyledScrollingFrame.lua create mode 100644 BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Components/StyledScrollingFrame.spec.lua create mode 100644 BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Components/StyledTooltip.lua create mode 100644 BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Components/StyledTooltip.spec.lua create mode 100644 BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Components/TextEntry.lua create mode 100644 BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Components/TextEntry.spec.lua create mode 100644 BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Components/Timeline/Keyframe.lua create mode 100644 BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Components/Timeline/Keyframe.spec.lua create mode 100644 BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Components/Timeline/Scrubber.lua create mode 100644 BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Components/Timeline/Scrubber.spec.lua create mode 100644 BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Components/TitledFrame.lua create mode 100644 BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Components/TitledFrame.spec.lua create mode 100644 BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Components/ToggleButton.lua create mode 100644 BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Components/ToggleButton.spec.lua create mode 100644 BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Components/Tooltip.lua create mode 100644 BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Components/Tooltip.spec.lua create mode 100644 BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Components/TreeView.lua create mode 100644 BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Components/TreeView.spec.lua create mode 100644 BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Components/createFitToContent.lua create mode 100644 BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Components/createFitToContent.spec.lua create mode 100644 BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Focus.lua create mode 100644 BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Focus.spec.lua create mode 100644 BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Localizing.lua create mode 100644 BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Localizing.spec.lua create mode 100644 BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/MockWrapper.lua create mode 100644 BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Plugin.lua create mode 100644 BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Studio/Analytics.lua create mode 100644 BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Studio/ContextMenus.lua create mode 100644 BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Studio/Hyperlink.lua create mode 100644 BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Studio/Internal/Mouse.lua create mode 100644 BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Studio/Localization.lua create mode 100644 BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Studio/Localization.spec.lua create mode 100644 BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Studio/PartialHyperLink.lua create mode 100644 BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Studio/PluginMenus.lua create mode 100644 BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Studio/StudioStyle.lua create mode 100644 BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Studio/StudioStyle.spec.lua create mode 100644 BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Studio/StudioTheme.lua create mode 100644 BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Studio/StudioTheme.spec.lua create mode 100644 BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/StyleDefaults.lua create mode 100644 BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Theming.lua create mode 100644 BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/UILibraryWrapper.lua create mode 100644 BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/UILibraryWrapper.spec.lua create mode 100644 BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Utils/AssetType.lua create mode 100644 BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Utils/AssetType.spec.lua create mode 100644 BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Utils/GetClassIcon.lua create mode 100644 BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Utils/GetClassIcon.spec.lua create mode 100644 BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Utils/GetTextSize.lua create mode 100644 BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Utils/Immutable.lua create mode 100644 BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Utils/Immutable.spec.lua create mode 100644 BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Utils/InsertToolEvent.lua create mode 100644 BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Utils/LayoutOrderIterator.lua create mode 100644 BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Utils/LayoutOrderIterator.spec.lua create mode 100644 BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Utils/MathUtils.lua create mode 100644 BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Utils/MathUtils.spec.lua create mode 100644 BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Utils/Signal.lua create mode 100644 BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Utils/Signal.spec.lua create mode 100644 BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Utils/Spritesheet.lua create mode 100644 BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Utils/Spritesheet.spec.lua create mode 100644 BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Utils/Symbol.lua create mode 100644 BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Utils/Symbol.spec.lua create mode 100644 BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Utils/Urls.lua create mode 100644 BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Utils/getTimeString.lua create mode 100644 BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Utils/getTimeString.spec.lua create mode 100644 BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/createTheme.lua create mode 100644 BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/deepJoin.lua create mode 100644 BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/deepJoin.spec.lua create mode 100644 BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/join.lua create mode 100644 BuiltInPlugins/PlayerEmulator/Packages/Framework/Examples/General/stylizer.lua create mode 100644 BuiltInPlugins/PlayerEmulator/Packages/Framework/Examples/General/stylizer/Application.lua create mode 100644 BuiltInPlugins/PlayerEmulator/Packages/Framework/Examples/General/stylizer/Box.lua create mode 100644 BuiltInPlugins/PlayerEmulator/Packages/Framework/Examples/General/stylizer/Button.lua create mode 100644 BuiltInPlugins/PlayerEmulator/Packages/Framework/Examples/General/stylizer/Dialog.lua create mode 100644 BuiltInPlugins/PlayerEmulator/Packages/Framework/RobloxAPI/ToolboxService/V1/Items/details.lua create mode 100644 BuiltInPlugins/PlayerEmulator/Packages/Framework/Style.lua create mode 100644 BuiltInPlugins/PlayerEmulator/Packages/Framework/Style/Colors.lua create mode 100644 BuiltInPlugins/PlayerEmulator/Packages/Framework/Style/ComponentSymbols.lua create mode 100644 BuiltInPlugins/PlayerEmulator/Packages/Framework/Style/ComponentSymbols.spec.lua create mode 100644 BuiltInPlugins/PlayerEmulator/Packages/Framework/Style/StyleKey.lua create mode 100644 BuiltInPlugins/PlayerEmulator/Packages/Framework/Style/StyleKey.spec.lua create mode 100644 BuiltInPlugins/PlayerEmulator/Packages/Framework/Style/Stylizer.lua create mode 100644 BuiltInPlugins/PlayerEmulator/Packages/Framework/Style/Stylizer.spec.lua create mode 100644 BuiltInPlugins/PlayerEmulator/Packages/Framework/Style/Themes/BaseTheme.lua create mode 100644 BuiltInPlugins/PlayerEmulator/Packages/Framework/Style/Themes/DarkTheme.lua create mode 100644 BuiltInPlugins/PlayerEmulator/Packages/Framework/Style/Themes/LightTheme.lua create mode 100644 BuiltInPlugins/PlayerEmulator/Packages/Framework/Style/Themes/StudioTheme.lua create mode 100644 BuiltInPlugins/PlayerEmulator/Packages/Framework/Style/Themes/StudioTheme.spec.lua create mode 100644 BuiltInPlugins/PlayerEmulator/Packages/Framework/Style/createDefaultTheme.lua create mode 100644 BuiltInPlugins/PlayerEmulator/Packages/Framework/Style/createDefaultTheme.spec.lua create mode 100644 BuiltInPlugins/PlayerEmulator/Packages/Framework/Style/getRawComponentStyle.lua create mode 100644 BuiltInPlugins/PlayerEmulator/Packages/Framework/Style/getRawComponentStyle.spec.lua create mode 100644 BuiltInPlugins/PlayerEmulator/Packages/Framework/TestHelpers/Instances.lua create mode 100644 BuiltInPlugins/PlayerEmulator/Packages/Framework/TestHelpers/Instances/MockAnalyticsService.lua create mode 100644 BuiltInPlugins/PlayerEmulator/Packages/Framework/TestHelpers/Instances/MockMouse.lua create mode 100644 BuiltInPlugins/PlayerEmulator/Packages/Framework/TestHelpers/Instances/MockPlugin.lua create mode 100644 BuiltInPlugins/PlayerEmulator/Packages/Framework/TestHelpers/Instances/MockPlugin.spec.lua create mode 100644 BuiltInPlugins/PlayerEmulator/Packages/Framework/TestHelpers/Instances/MockPluginToolbar.lua create mode 100644 BuiltInPlugins/PlayerEmulator/Packages/Framework/TestHelpers/Instances/MockPluginToolbar.spec.lua create mode 100644 BuiltInPlugins/PlayerEmulator/Packages/Framework/TestHelpers/Instances/MockPluginToolbarButton.lua create mode 100644 BuiltInPlugins/PlayerEmulator/Packages/Framework/TestHelpers/Instances/MockPluginToolbarButton.spec.lua create mode 100644 BuiltInPlugins/PlayerEmulator/Packages/Framework/TestHelpers/Instances/MockSelectionService.lua create mode 100644 BuiltInPlugins/PlayerEmulator/Packages/Framework/TestHelpers/Instances/MockStudioService.lua create mode 100644 BuiltInPlugins/PlayerEmulator/Packages/Framework/TestHelpers/makeSettableValue.lua create mode 100644 BuiltInPlugins/PlayerEmulator/Packages/Framework/TestHelpers/makeSettableValue.spec.lua create mode 100644 BuiltInPlugins/PlayerEmulator/Packages/Framework/TestHelpers/setEquals.lua create mode 100644 BuiltInPlugins/PlayerEmulator/Packages/Framework/TestHelpers/testImmutability.lua create mode 100644 BuiltInPlugins/PlayerEmulator/Packages/Framework/TestHelpers/testImmutability.spec.lua create mode 100644 BuiltInPlugins/PlayerEmulator/Packages/Framework/Util/deepCopy.lua create mode 100644 BuiltInPlugins/PlayerEmulator/Packages/Framework/Util/deepCopy.spec.lua create mode 100644 BuiltInPlugins/PlayerEmulator/Packages/Framework/Util/deepJoin.lua create mode 100644 BuiltInPlugins/PlayerEmulator/Packages/Framework/Util/deepJoin.spec.lua create mode 100644 BuiltInPlugins/PlayerEmulator/Packages/Framework/Util/getTestVariation.lua create mode 100644 BuiltInPlugins/PlayerEmulator/Packages/Framework/Util/getTestVariation.spec.lua create mode 100644 BuiltInPlugins/PlayerEmulator/Packages/Framework/Util/tableCache.lua create mode 100644 BuiltInPlugins/PlayerEmulator/Packages/Framework/Util/tableCache.spec.lua create mode 100644 BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Camera.lua create mode 100644 BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Components/BaseDialog.lua create mode 100644 BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Components/BaseDialog.spec.lua create mode 100644 BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Components/BulletPoint.lua create mode 100644 BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Components/BulletPoint.spec.lua create mode 100644 BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Components/Button.lua create mode 100644 BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Components/Button.spec.lua create mode 100644 BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Components/CheckBox.lua create mode 100644 BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Components/CheckBox.spec.lua create mode 100644 BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Components/DetailedDropdown.lua create mode 100644 BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Components/DetailedDropdown.spec.lua create mode 100644 BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Components/DragTarget.lua create mode 100644 BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Components/DragTarget.spec.lua create mode 100644 BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Components/DropShadow.lua create mode 100644 BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Components/DropShadow.spec.lua create mode 100644 BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Components/DropdownMenu.lua create mode 100644 BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Components/DropdownMenu.spec.lua create mode 100644 BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Components/ExpandableList.lua create mode 100644 BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Components/ExpandableList.spec.lua create mode 100644 BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Components/InfiniteScrollingFrame.lua create mode 100644 BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Components/InfiniteScrollingFrame.spec.lua create mode 100644 BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Components/LoadingBar.lua create mode 100644 BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Components/LoadingIndicator.lua create mode 100644 BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Components/LoadingIndicator.spec.lua create mode 100644 BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Components/MultilineTextEntry.lua create mode 100644 BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Components/MultilineTextEntry.spec.lua create mode 100644 BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Components/PluginWidget/Dialog.lua create mode 100644 BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Components/Preview/ActionBar.lua create mode 100644 BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Components/Preview/ActionBar.spec.lua create mode 100644 BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Components/Preview/AssetDescription.lua create mode 100644 BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Components/Preview/AssetDescription.spec.lua create mode 100644 BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Components/Preview/AssetPreview.lua create mode 100644 BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Components/Preview/AssetPreview.spec.lua create mode 100644 BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Components/Preview/AudioControl.lua create mode 100644 BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Components/Preview/AudioControl.spec.lua create mode 100644 BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Components/Preview/AudioPreview.lua create mode 100644 BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Components/Preview/AudioPreview.spec.lua create mode 100644 BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Components/Preview/Favorites.lua create mode 100644 BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Components/Preview/Favorites.spec.lua create mode 100644 BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Components/Preview/ImagePreview.lua create mode 100644 BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Components/Preview/ImagePreview.spec.lua create mode 100644 BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Components/Preview/InstanceTreeViewItem.lua create mode 100644 BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Components/Preview/InstanceTreeViewItem.spec.lua create mode 100644 BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Components/Preview/MediaControl.lua create mode 100644 BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Components/Preview/MediaControl.spec.lua create mode 100644 BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Components/Preview/MediaProgressBar.lua create mode 100644 BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Components/Preview/MediaProgressBar.spec.lua create mode 100644 BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Components/Preview/ModelPreview.lua create mode 100644 BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Components/Preview/ModelPreview.spec.lua create mode 100644 BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Components/Preview/PreviewController.lua create mode 100644 BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Components/Preview/PreviewController.spec.lua create mode 100644 BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Components/Preview/SearchLinkText.lua create mode 100644 BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Components/Preview/SearchLinkText.spec.lua create mode 100644 BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Components/Preview/ThumbnailIconPreview.lua create mode 100644 BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Components/Preview/ThumbnailIconPreview.spec.lua create mode 100644 BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Components/Preview/TreeViewButton.lua create mode 100644 BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Components/Preview/TreeViewButton.spec.lua create mode 100644 BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Components/Preview/VideoPreview.lua create mode 100644 BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Components/Preview/VideoPreview.spec.lua create mode 100644 BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Components/Preview/Vote.lua create mode 100644 BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Components/Preview/Vote.spec.lua create mode 100644 BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Components/Preview/wrapDraggableMedia.lua create mode 100644 BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Components/Preview/wrapDraggableMedia.spec.lua create mode 100644 BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Components/Preview/wrapMedia.lua create mode 100644 BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Components/Preview/wrapMedia.spec.lua create mode 100644 BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Components/RadioButtons.lua create mode 100644 BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Components/RadioButtons.spec.lua create mode 100644 BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Components/RoundFrame.lua create mode 100644 BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Components/RoundFrame.spec.lua create mode 100644 BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Components/RoundTextBox.lua create mode 100644 BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Components/RoundTextBox.spec.lua create mode 100644 BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Components/RoundTextButton.lua create mode 100644 BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Components/RoundTextButton.spec.lua create mode 100644 BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Components/SearchBar.lua create mode 100644 BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Components/SearchBar.spec.lua create mode 100644 BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Components/Separator.lua create mode 100644 BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Components/Separator.spec.lua create mode 100644 BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Components/StyledDialog.lua create mode 100644 BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Components/StyledDialog.spec.lua create mode 100644 BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Components/StyledDropdown.lua create mode 100644 BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Components/StyledDropdown.spec.lua create mode 100644 BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Components/StyledScrollingFrame.lua create mode 100644 BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Components/StyledScrollingFrame.spec.lua create mode 100644 BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Components/StyledTooltip.lua create mode 100644 BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Components/StyledTooltip.spec.lua create mode 100644 BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Components/TextEntry.lua create mode 100644 BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Components/TextEntry.spec.lua create mode 100644 BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Components/Timeline/Keyframe.lua create mode 100644 BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Components/Timeline/Keyframe.spec.lua create mode 100644 BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Components/Timeline/Scrubber.lua create mode 100644 BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Components/Timeline/Scrubber.spec.lua create mode 100644 BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Components/TitledFrame.lua create mode 100644 BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Components/TitledFrame.spec.lua create mode 100644 BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Components/ToggleButton.lua create mode 100644 BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Components/ToggleButton.spec.lua create mode 100644 BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Components/Tooltip.lua create mode 100644 BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Components/Tooltip.spec.lua create mode 100644 BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Components/TreeView.lua create mode 100644 BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Components/TreeView.spec.lua create mode 100644 BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Components/createFitToContent.lua create mode 100644 BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Components/createFitToContent.spec.lua create mode 100644 BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Focus.lua create mode 100644 BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Focus.spec.lua create mode 100644 BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Localizing.lua create mode 100644 BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Localizing.spec.lua create mode 100644 BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/MockWrapper.lua create mode 100644 BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Plugin.lua create mode 100644 BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Studio/Analytics.lua create mode 100644 BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Studio/ContextMenus.lua create mode 100644 BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Studio/Hyperlink.lua create mode 100644 BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Studio/Internal/Mouse.lua create mode 100644 BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Studio/Localization.lua create mode 100644 BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Studio/Localization.spec.lua create mode 100644 BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Studio/PartialHyperLink.lua create mode 100644 BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Studio/PluginMenus.lua create mode 100644 BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Studio/StudioStyle.lua create mode 100644 BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Studio/StudioStyle.spec.lua create mode 100644 BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Studio/StudioTheme.lua create mode 100644 BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Studio/StudioTheme.spec.lua create mode 100644 BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/StyleDefaults.lua create mode 100644 BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Theming.lua create mode 100644 BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/UILibraryWrapper.lua create mode 100644 BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/UILibraryWrapper.spec.lua create mode 100644 BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Utils/AssetType.lua create mode 100644 BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Utils/AssetType.spec.lua create mode 100644 BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Utils/GetClassIcon.lua create mode 100644 BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Utils/GetClassIcon.spec.lua create mode 100644 BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Utils/GetTextSize.lua create mode 100644 BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Utils/Immutable.lua create mode 100644 BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Utils/Immutable.spec.lua create mode 100644 BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Utils/InsertToolEvent.lua create mode 100644 BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Utils/LayoutOrderIterator.lua create mode 100644 BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Utils/LayoutOrderIterator.spec.lua create mode 100644 BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Utils/MathUtils.lua create mode 100644 BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Utils/MathUtils.spec.lua create mode 100644 BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Utils/Signal.lua create mode 100644 BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Utils/Signal.spec.lua create mode 100644 BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Utils/Spritesheet.lua create mode 100644 BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Utils/Spritesheet.spec.lua create mode 100644 BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Utils/Symbol.lua create mode 100644 BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Utils/Symbol.spec.lua create mode 100644 BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Utils/Urls.lua create mode 100644 BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Utils/getTimeString.lua create mode 100644 BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Utils/getTimeString.spec.lua create mode 100644 BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/createTheme.lua create mode 100644 BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/deepJoin.lua create mode 100644 BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/deepJoin.spec.lua create mode 100644 BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/join.lua create mode 100644 BuiltInPlugins/TerrainToolsV2/Packages/Framework/Util/getTestVariation.lua create mode 100644 BuiltInPlugins/TerrainToolsV2/Packages/Framework/Util/getTestVariation.spec.lua create mode 100644 BuiltInPlugins/TerrainToolsV2/Src/Actions/SelectColormap.lua create mode 100644 BuiltInPlugins/TerrainToolsV2/Src/Actions/SelectHeightmap.lua create mode 100644 BuiltInPlugins/TerrainToolsV2/Src/Components/Tools/ImportLocal.lua create mode 100644 BuiltInPlugins/TerrainToolsV2/Src/Components/Tools/ImportLocal.spec.lua create mode 100644 BuiltInPlugins/TerrainToolsV2/Src/Components/Tools/ToolParts/LocalImageSelector.lua create mode 100644 BuiltInPlugins/TerrainToolsV2/Src/Components/Tools/ToolParts/LocalImageSelector.spec.lua create mode 100644 BuiltInPlugins/TerrainToolsV2/Src/Components/Tools/ToolParts/PromptSelectorWithPreview.lua create mode 100644 BuiltInPlugins/TerrainToolsV2/Src/Components/Tools/ToolParts/PromptSelectorWithPreview.spec.lua create mode 100644 BuiltInPlugins/TerrainToolsV2/Src/Reducers/ImportLocalTool.lua create mode 100644 BuiltInPlugins/TerrainToolsV2/Src/Reducers/ImportLocalTool.spec.lua create mode 100644 BuiltInPlugins/Toolbox/Libs/Framework/Util/getTestVariation.lua create mode 100644 BuiltInPlugins/Toolbox/Libs/Framework/Util/getTestVariation.spec.lua create mode 100644 BuiltInStandalonePlugins/PluginManagement/Packages/Framework/Util/getTestVariation.lua create mode 100644 BuiltInStandalonePlugins/PluginManagement/Packages/Framework/Util/getTestVariation.spec.lua create mode 100644 LuaPackages/Regulations/ScreenTime/Constants.lua create mode 100644 LuaPackages/Regulations/ScreenTime/GetFFlagScreenTime.lua create mode 100644 LuaPackages/Regulations/ScreenTime/HttpRequests.lua create mode 100644 LuaPackages/Regulations/ScreenTime/HttpRequests.spec.lua create mode 100644 LuaPackages/UIBloxFlags/GetFFlagLuaAppUseNewUIBloxRoundedCorners.lua delete mode 100644 LuaPackages/UIBloxFlags/GetFFlagLuaAppUseUIBloxToasts.lua create mode 100644 scripts/CoreScripts/Modules/AvatarEditorPrompts/Actions/CloseOpenPrompt.lua create mode 100644 scripts/CoreScripts/Modules/AvatarEditorPrompts/Actions/GameNameFetched.lua create mode 100644 scripts/CoreScripts/Modules/AvatarEditorPrompts/Actions/OpenPrompt.lua create mode 100644 scripts/CoreScripts/Modules/AvatarEditorPrompts/Actions/ScreenSizeUpdated.lua create mode 100644 scripts/CoreScripts/Modules/AvatarEditorPrompts/Components/AvatarEditorPromptsApp.lua create mode 100644 scripts/CoreScripts/Modules/AvatarEditorPrompts/Components/Connection/AvatarEditorServiceConnector.lua create mode 100644 scripts/CoreScripts/Modules/AvatarEditorPrompts/Components/Connection/ContextActionsBinder.lua create mode 100644 scripts/CoreScripts/Modules/AvatarEditorPrompts/Components/Connection/init.lua create mode 100644 scripts/CoreScripts/Modules/AvatarEditorPrompts/Components/HumanoidViewport.lua create mode 100644 scripts/CoreScripts/Modules/AvatarEditorPrompts/Components/ItemsList.lua create mode 100644 scripts/CoreScripts/Modules/AvatarEditorPrompts/Components/Prompts/AllowInventoryReadAccessPrompt.lua create mode 100644 scripts/CoreScripts/Modules/AvatarEditorPrompts/Components/Prompts/AllowInventoryReadAccessPrompt.spec.lua create mode 100644 scripts/CoreScripts/Modules/AvatarEditorPrompts/Components/Prompts/CreateOutfitPrompt.lua create mode 100644 scripts/CoreScripts/Modules/AvatarEditorPrompts/Components/Prompts/CreateOutfitPrompt.spec.lua create mode 100644 scripts/CoreScripts/Modules/AvatarEditorPrompts/Components/Prompts/SaveAvatarPrompt.lua create mode 100644 scripts/CoreScripts/Modules/AvatarEditorPrompts/Components/Prompts/SaveAvatarPrompt.spec.lua create mode 100644 scripts/CoreScripts/Modules/AvatarEditorPrompts/Components/Prompts/SetFavoritePrompt.lua create mode 100644 scripts/CoreScripts/Modules/AvatarEditorPrompts/Components/Prompts/SetFavoritePrompt.spec.lua create mode 100644 scripts/CoreScripts/Modules/AvatarEditorPrompts/GetAssetNamesFromDescription.lua create mode 100644 scripts/CoreScripts/Modules/AvatarEditorPrompts/PromptType.lua create mode 100644 scripts/CoreScripts/Modules/AvatarEditorPrompts/Reducer/GameName.lua create mode 100644 scripts/CoreScripts/Modules/AvatarEditorPrompts/Reducer/PromptInfo.lua create mode 100644 scripts/CoreScripts/Modules/AvatarEditorPrompts/Reducer/PromptInfo.spec.lua create mode 100644 scripts/CoreScripts/Modules/AvatarEditorPrompts/Reducer/ScreenSize.lua create mode 100644 scripts/CoreScripts/Modules/AvatarEditorPrompts/Reducer/ScreenSize.spec.lua create mode 100644 scripts/CoreScripts/Modules/AvatarEditorPrompts/Reducer/init.lua create mode 100644 scripts/CoreScripts/Modules/AvatarEditorPrompts/RoactGlobalConfig.lua create mode 100644 scripts/CoreScripts/Modules/AvatarEditorPrompts/Thunks/CloseOpenPrompt.lua create mode 100644 scripts/CoreScripts/Modules/AvatarEditorPrompts/Thunks/GetGameName.lua create mode 100644 scripts/CoreScripts/Modules/AvatarEditorPrompts/Thunks/OpenSaveAvatarPrompt.lua create mode 100644 scripts/CoreScripts/Modules/AvatarEditorPrompts/Thunks/OpenSetFavoritePrompt.lua create mode 100644 scripts/CoreScripts/Modules/AvatarEditorPrompts/Thunks/PerformCreateOutfit.lua create mode 100644 scripts/CoreScripts/Modules/AvatarEditorPrompts/Thunks/PerformSaveAvatar.lua create mode 100644 scripts/CoreScripts/Modules/AvatarEditorPrompts/Thunks/PerformSetFavorite.lua create mode 100644 scripts/CoreScripts/Modules/AvatarEditorPrompts/Thunks/SetAllowInventoryReadAccess.lua create mode 100644 scripts/CoreScripts/Modules/AvatarEditorPrompts/Thunks/SignalCreateOutfitPermissionDenied.lua create mode 100644 scripts/CoreScripts/Modules/AvatarEditorPrompts/Thunks/SignalSaveAvatarPermissionDenied.lua create mode 100644 scripts/CoreScripts/Modules/AvatarEditorPrompts/Thunks/SignalSetFavoritePermissionDenied.lua create mode 100644 scripts/CoreScripts/Modules/AvatarEditorPrompts/init.lua create mode 100644 scripts/CoreScripts/Modules/AvatarEditorPrompts/init.spec.lua create mode 100644 scripts/CoreScripts/Modules/Flags/GetFixGraphicsQuality.lua diff --git a/BuiltInPlugins/AnimationClipEditor/LuaFlags/GetFFlagHideLoadToastIfAnimationClipped.lua b/BuiltInPlugins/AnimationClipEditor/LuaFlags/GetFFlagHideLoadToastIfAnimationClipped.lua new file mode 100644 index 0000000000..c39b3321b0 --- /dev/null +++ b/BuiltInPlugins/AnimationClipEditor/LuaFlags/GetFFlagHideLoadToastIfAnimationClipped.lua @@ -0,0 +1,5 @@ +game:DefineFastFlag("HideLoadToastIfAnimationClipped", false) + +return function() + return game:GetFastFlag("HideLoadToastIfAnimationClipped") +end \ No newline at end of file diff --git a/BuiltInPlugins/AnimationClipEditor/Src/Components/DopeSheetController.lua b/BuiltInPlugins/AnimationClipEditor/Src/Components/DopeSheetController.lua index 16220e4676..2043e99c87 100644 --- a/BuiltInPlugins/AnimationClipEditor/Src/Components/DopeSheetController.lua +++ b/BuiltInPlugins/AnimationClipEditor/Src/Components/DopeSheetController.lua @@ -59,6 +59,7 @@ local SetIsPlaying = require(Plugin.Src.Actions.SetIsPlaying) local GetFFlagEnforceMaxAnimLength = require(Plugin.LuaFlags.GetFFlagEnforceMaxAnimLength) local UseCustomFPS = require(Plugin.LuaFlags.GetFFlagAnimEditorUseCustomFPS) local GetFFlagAddImportFailureToast = require(Plugin.LuaFlags.GetFFlagAddImportFailureToast) +local GetFFlagHideLoadToastIfAnimationClipped = require(Plugin.LuaFlags.GetFFlagHideLoadToastIfAnimationClipped) local DopeSheetController = Roact.Component:extend("DopeSheetController") @@ -489,6 +490,7 @@ function DopeSheetController:render() local savedAnimName = props.Saved local showClippedWarning = GetFFlagEnforceMaxAnimLength() and props.ClippedWarning local showInvalidIdWarning = GetFFlagAddImportFailureToast() and props.InvalidIdWarning + local showLoadToast = not GetFFlagHideLoadToastIfAnimationClipped() or (GetFFlagHideLoadToastIfAnimationClipped() and not (GetFFlagEnforceMaxAnimLength() and showClippedWarning)) local size = props.Size local position = props.Position @@ -692,7 +694,7 @@ function DopeSheetController:render() OnClose = props.CloseSavedToast, }), - LoadedToast = loadedAnimName and Roact.createElement(NoticeToast, { + LoadedToast = showLoadToast and loadedAnimName and Roact.createElement(NoticeToast, { Text = localization:getText("Toast", "Loaded", loadedAnimName), OnClose = props.CloseLoadedToast, }), diff --git a/BuiltInPlugins/AnimationClipEditor/UILibrary/_internal/Utils/Symbol.spec.lua b/BuiltInPlugins/AnimationClipEditor/UILibrary/_internal/Utils/Symbol.spec.lua index cde9be065b..f3312055c9 100644 --- a/BuiltInPlugins/AnimationClipEditor/UILibrary/_internal/Utils/Symbol.spec.lua +++ b/BuiltInPlugins/AnimationClipEditor/UILibrary/_internal/Utils/Symbol.spec.lua @@ -11,7 +11,7 @@ return function() it("should coerce to the given name", function() local symbol = Symbol.named("foo") - expect(tostring(symbol):find("foo")).to.be.ok() + expect(tostring(symbol):match("foo")).to.be.ok() end) it("should be unique when constructed", function() @@ -42,4 +42,4 @@ return function() expect(symbolA).never.to.equal(symbolB) end) end) -end \ No newline at end of file +end diff --git a/BuiltInPlugins/AssetConfiguration/Core/Components/ToolboxPlugin.lua b/BuiltInPlugins/AssetConfiguration/Core/Components/ToolboxPlugin.lua index 388db5ed5d..dff65cdd83 100644 --- a/BuiltInPlugins/AssetConfiguration/Core/Components/ToolboxPlugin.lua +++ b/BuiltInPlugins/AssetConfiguration/Core/Components/ToolboxPlugin.lua @@ -18,25 +18,27 @@ local makeTheme = require(Util.makeTheme) local ContextServices = require(Libs.Framework.ContextServices) local UILibraryWrapper = ContextServices.UILibraryWrapper +local FrameworkUtil = require(Libs.Framework.Util) +local getTestVariation = FrameworkUtil.getTestVariation local Analytics = require(Util.Analytics.Analytics) local FFlagEnableToolboxImpressionAnalytics = game:GetFastFlag("EnableToolboxImpressionAnalytics") local FFlagBootstrapperTryAsset = game:GetFastFlag("BootstrapperTryAsset") +-- Be sure to turn off ToolboxShowHideABTest before turning on StudioShowHideABTestV2 local FFlagToolboxShowHideABTest = game:GetFastFlag("ToolboxShowHideABTest") +local FFlagStudioShowHideABTestV2 = game:GetFastFlag("StudioShowHideABTestV2") -local AB_TEST_GROUP_CONTROL = "Control" - --- ShowHideToolbox : AB Test where Toolbox shows on startup for users not in the Control group --- Control : Toolbox appears on startup --- All Variations : Toolbox hidden on startup local ShowHideABTestName = "AllUsers.RobloxStudio.ShowHideToolbox" +local ABTEST_SHOWHIDEV2_NAME = "AllUsers.RobloxStudio.ShowHideV2" local function shouldSeeTestBehavior(abTestName) + -- REMOVE THIS WITH FFlagShowHideABTest + -- helper function for showing a behavior so long as the result is not "Control" -- further specificity can be used if the exact variation is required local variation = ABTestService:GetVariant(abTestName) - local shouldShowBehavior = variation ~= AB_TEST_GROUP_CONTROL + local shouldShowBehavior = variation ~= "Control" return shouldShowBehavior, variation end @@ -151,8 +153,15 @@ function ToolboxPlugin:render() local isToolboxHidden = shouldSeeTestBehavior(ShowHideABTestName) if isToolboxHidden then initialEnabled = false - else + end + elseif FFlagStudioShowHideABTestV2 then + local variation = getTestVariation(ABTEST_SHOWHIDEV2_NAME) + if variation == 0 or variation == 2 then + -- Even though 0 is supposed to be the Control group and preserve existing behaviors, + -- Toolbox should be enabled by default. The fact that it isn't is a bug. initialEnabled = true + elseif variation == 1 then + initialEnabled = false end end diff --git a/BuiltInPlugins/AssetConfiguration/Core/Networking/Requests/ChangeMarketplaceTab.lua b/BuiltInPlugins/AssetConfiguration/Core/Networking/Requests/ChangeMarketplaceTab.lua index 484949689d..622b0a7d2b 100644 --- a/BuiltInPlugins/AssetConfiguration/Core/Networking/Requests/ChangeMarketplaceTab.lua +++ b/BuiltInPlugins/AssetConfiguration/Core/Networking/Requests/ChangeMarketplaceTab.lua @@ -3,6 +3,7 @@ local FFlagUseCategoryNameInToolbox = game:GetFastFlag("UseCategoryNameInToolbox local Plugin = script.Parent.Parent.Parent.Parent local Cryo = require(Plugin.Libs.Cryo) +local RobloxAPI = require(Plugin.Libs.Framework).RobloxAPI local RequestReason = require(Plugin.Core.Types.RequestReason) @@ -19,7 +20,7 @@ return function(networkInterface, tabName, newCategories, settings, options) local categories = Category.getCategories(tabName, store:getState().roles) local creator = Cryo.None - if FFlagToolboxShowRobloxCreatedAssetsForLuobu then + if FFlagToolboxShowRobloxCreatedAssetsForLuobu and RobloxAPI:baseURLHasChineseHost() then creator = options.creator or Cryo.None end diff --git a/BuiltInPlugins/AssetConfiguration/Core/Types/Category.lua b/BuiltInPlugins/AssetConfiguration/Core/Types/Category.lua index 0f623b8de0..93f09798b8 100644 --- a/BuiltInPlugins/AssetConfiguration/Core/Types/Category.lua +++ b/BuiltInPlugins/AssetConfiguration/Core/Types/Category.lua @@ -281,12 +281,10 @@ Category.RECENT_KEY = "Recent" Category.CREATIONS_KEY = "Creations" table.insert(Category.INVENTORY, Category.MY_PLUGINS) -if not FFlagToolboxShowRobloxCreatedAssetsForLuobu then - if FFlagOnlyWhitelistedPluginsInStudio then - table.insert(Category.MARKETPLACE, Category.WHITELISTED_PLUGINS) - else - table.insert(Category.MARKETPLACE, Category.FREE_PLUGINS) - end +if FFlagOnlyWhitelistedPluginsInStudio then + table.insert(Category.MARKETPLACE, Category.WHITELISTED_PLUGINS) +else + table.insert(Category.MARKETPLACE, Category.FREE_PLUGINS) end local insertIndex = Cryo.List.find(Category.INVENTORY_WITH_GROUPS, Category.MY_PACKAGES) + 1 @@ -294,7 +292,7 @@ table.insert(Category.INVENTORY_WITH_GROUPS, insertIndex, Category.MY_PLUGINS) local insertIndex2 = Cryo.List.find(Category.INVENTORY_WITH_GROUPS, Category.GROUP_AUDIO) + 1 table.insert(Category.INVENTORY_WITH_GROUPS, insertIndex2, Category.GROUP_PLUGINS) -if FFlagToolboxShowRobloxCreatedAssetsForLuobu then +if FFlagToolboxShowRobloxCreatedAssetsForLuobu and RobloxAPI:baseURLHasChineseHost() then local disabledCategories = string.split(FStringLuobuMarketplaceDisabledCategories, ";") for _, categoryName in pairs(disabledCategories) do diff --git a/BuiltInPlugins/AssetConfiguration/Core/Util/InsertAsset.lua b/BuiltInPlugins/AssetConfiguration/Core/Util/InsertAsset.lua index 72c170a615..8522e29153 100644 --- a/BuiltInPlugins/AssetConfiguration/Core/Util/InsertAsset.lua +++ b/BuiltInPlugins/AssetConfiguration/Core/Util/InsertAsset.lua @@ -186,7 +186,7 @@ local function insertDecal(plugin, assetId, assetName) decal.Name = assetName if FFlagMarketplaceSourceAssetIds then - decal.SourceAssetId = tbl[1] + decal.SourceAssetId = assetId end if FFlagToolboxFixDecalInsert then diff --git a/BuiltInPlugins/AssetConfiguration/Libs/Framework/ContextServices/Theme.spec.lua b/BuiltInPlugins/AssetConfiguration/Libs/Framework/ContextServices/Theme.spec.lua index 22377dc00b..1547fb3c11 100644 --- a/BuiltInPlugins/AssetConfiguration/Libs/Framework/ContextServices/Theme.spec.lua +++ b/BuiltInPlugins/AssetConfiguration/Libs/Framework/ContextServices/Theme.spec.lua @@ -11,6 +11,13 @@ return function() local StyleTable = Util.StyleTable local StyleModifier = Util.StyleModifier local Signal = Util.Signal + local FlagsList = Util.Flags.new({ + FFlagRefactorDevFrameworkTheme = {"RefactorDevFrameworkTheme"}, + }) + + if FlagsList:get("FFlagRefactorDevFrameworkTheme") then + return + end local function createTestThemedComponent(render) local TestThemedComponent = Roact.PureComponent:extend("TestThemedComponent") diff --git a/BuiltInPlugins/AssetConfiguration/Libs/Framework/ContextServices/UILibraryWrapper.spec.lua b/BuiltInPlugins/AssetConfiguration/Libs/Framework/ContextServices/UILibraryWrapper.spec.lua index 0823cb2280..d4bbb673db 100644 --- a/BuiltInPlugins/AssetConfiguration/Libs/Framework/ContextServices/UILibraryWrapper.spec.lua +++ b/BuiltInPlugins/AssetConfiguration/Libs/Framework/ContextServices/UILibraryWrapper.spec.lua @@ -2,8 +2,10 @@ return function() local Framework = script.Parent.Parent local Util = require(Framework.Util) + local Signal = Util.Signal local FlagsList = Util.Flags.new({ FFlagStudioDevFrameworkPackage = {"StudioDevFrameworkPackage"}, + FFlagRefactorDevFrameworkTheme = {"RefactorDevFrameworkTheme"}, }) local isUsedAsPackage = require(Framework.Util.isUsedAsPackage) @@ -18,6 +20,7 @@ return function() local Focus = require(Framework.ContextServices.Focus) local Theme = require(Framework.ContextServices.Theme) local UILibraryWrapper = require(script.Parent.UILibraryWrapper) + local StudioTheme = require(Framework.Style.Themes.StudioTheme) local WrapperStub = Roact.PureComponent:extend("UILibraryWrapper") @@ -29,12 +32,26 @@ return function() Wrapper = WrapperStub } + local function createThemeMock() + + if FlagsList:get("FFlagRefactorDevFrameworkTheme") then + return StudioTheme.mock() + else + local createStyles = function() + return {} + end + local getTheme = function() + return "Light" + end + local themeChanged = Signal.new() + return Theme.mock(createStyles, getTheme, themeChanged) + end + end + it("should expect a Plugin ContextItem provided above", function() local focus = Focus.new(Instance.new("ScreenGui")) local wrapper = UILibraryWrapper.new(UILibraryStub) - local theme = Theme.new(function() - return {} - end) + local theme = createThemeMock() function theme:getUILibraryTheme() return {} end @@ -55,9 +72,7 @@ return function() it("should expect a Focus ContextItem provided above", function() local plugin = Plugin.new({}) local wrapper = UILibraryWrapper.new(UILibraryStub) - local theme = Theme.new(function() - return {} - end) + local theme = createThemeMock() function theme:getUILibraryTheme() return {} end @@ -97,9 +112,7 @@ return function() local plugin = Plugin.new({}) local focus = Focus.new(Instance.new("ScreenGui")) local wrapper = UILibraryWrapper.new(UILibraryStub) - local theme = Theme.new(function() - return {} - end) + local theme = createThemeMock() local element = provide({ plugin, @@ -119,9 +132,7 @@ return function() local plugin = Plugin.new({}) local focus = Focus.new(Instance.new("ScreenGui")) local wrapper = UILibraryWrapper.new(UILibraryStub) - local theme = Theme.new(function() - return {} - end) + local theme = createThemeMock() function theme:getUILibraryTheme() return {} end diff --git a/BuiltInPlugins/AssetConfiguration/Libs/Framework/StudioUI/TitledFrame/test.spec.lua b/BuiltInPlugins/AssetConfiguration/Libs/Framework/StudioUI/TitledFrame/test.spec.lua index cff37efea8..f013a391bc 100644 --- a/BuiltInPlugins/AssetConfiguration/Libs/Framework/StudioUI/TitledFrame/test.spec.lua +++ b/BuiltInPlugins/AssetConfiguration/Libs/Framework/StudioUI/TitledFrame/test.spec.lua @@ -17,7 +17,7 @@ return function() local function createTestTitledFrame(children, container) local theme if FlagsList:get("FFlagRefactorDevFrameworkTheme") then - theme = StudioTheme.new() + theme = StudioTheme.mock() else theme = Theme.new(function() return { diff --git a/BuiltInPlugins/AssetConfiguration/Libs/Framework/StudioUI/createPluginWidget.lua b/BuiltInPlugins/AssetConfiguration/Libs/Framework/StudioUI/createPluginWidget.lua index 85620982d3..b9dd6d3759 100644 --- a/BuiltInPlugins/AssetConfiguration/Libs/Framework/StudioUI/createPluginWidget.lua +++ b/BuiltInPlugins/AssetConfiguration/Libs/Framework/StudioUI/createPluginWidget.lua @@ -9,10 +9,12 @@ ]] game:DefineFastFlag("FixDevFrameworkDockWidgetRestore", false) -game:DefineFastFlag("DevFrameworkPluginWidgetEnabledEvent", false) +game:DefineFastFlag("DevFrameworkPluginWidgetEnabledEvent2", false) +game:DefineFastFlag("DevFrameworkPluginWidgetUseSiblingZIndex", false) local FFlagFixDevFrameworkDockWidgetRestore = game:GetFastFlag("FixDevFrameworkDockWidgetRestore") -local FFlagDevFrameworkPluginWidgetEnabledEvent = game:GetFastFlag("DevFrameworkPluginWidgetEnabledEvent") +local FFlagDevFrameworkPluginWidgetEnabledEvent2 = game:GetFastFlag("DevFrameworkPluginWidgetEnabledEvent2") +local FFlagDevFrameworkPluginWidgetUseSiblingZIndex = game:GetFastFlag("DevFrameworkPluginWidgetUseSiblingZIndex") local Framework = script.Parent.Parent local Roact = require(Framework.Parent.Roact) @@ -31,7 +33,11 @@ local function createPluginWidget(componentName, createWidgetFunc) local widget = createWidgetFunc(props) widget.Name = title or "" - widget.ZIndexBehavior = props.ZIndexBehavior or Enum.ZIndexBehavior.Global + if FFlagDevFrameworkPluginWidgetUseSiblingZIndex then + widget.ZIndexBehavior = props.ZIndexBehavior or Enum.ZIndexBehavior.Sibling + else + widget.ZIndexBehavior = props.ZIndexBehavior or Enum.ZIndexBehavior.Global + end if widget:IsA("PluginGui") then widget:BindToClose(onClose) @@ -67,12 +73,13 @@ local function createPluginWidget(componentName, createWidgetFunc) end end - if FFlagDevFrameworkPluginWidgetEnabledEvent then + if FFlagDevFrameworkPluginWidgetEnabledEvent2 then -- Connect to enabled changing *after* restore -- Otherwise users of this will get 2 enabled changes: one from the onRestore, and the same from Roact.Change.Enabled self.widgetEnabledChangedConnection = widget:GetPropertyChangedSignal("Enabled"):Connect(function() - if self.props[Roact.Change.Enabled] then - self.props[Roact.Change.Enabled](self.widget) + local callback = self.props[Roact.Change.Enabled] + if callback and self.widget and self.widget.Enabled ~= self.props.Enabled then + callback(self.widget) end end) end diff --git a/BuiltInPlugins/AssetConfiguration/Libs/Framework/Style/Stylizer.lua b/BuiltInPlugins/AssetConfiguration/Libs/Framework/Style/Stylizer.lua index 47a60c5663..228c4b165a 100644 --- a/BuiltInPlugins/AssetConfiguration/Libs/Framework/Style/Stylizer.lua +++ b/BuiltInPlugins/AssetConfiguration/Libs/Framework/Style/Stylizer.lua @@ -1,3 +1,5 @@ +--!nocheck + --[[ Wraps theme styles and update logic into a ContextItem. @@ -318,4 +320,4 @@ function Stylizer.mock(t, themeProps, callback) return self end -return Stylizer \ No newline at end of file +return Stylizer diff --git a/BuiltInPlugins/AssetConfiguration/Libs/Framework/Style/getRawComponentStyle.lua b/BuiltInPlugins/AssetConfiguration/Libs/Framework/Style/getRawComponentStyle.lua index 7e659ca0c0..5c76de757c 100644 --- a/BuiltInPlugins/AssetConfiguration/Libs/Framework/Style/getRawComponentStyle.lua +++ b/BuiltInPlugins/AssetConfiguration/Libs/Framework/Style/getRawComponentStyle.lua @@ -9,6 +9,7 @@ local UIFolderData = require(Framework.UI.UIFolderData) return function(componentName) local componentData = UIFolderData[componentName] or StudioUIFolderData[componentName] local result + if componentData.style then result = require(componentData.style) end diff --git a/BuiltInPlugins/AssetConfiguration/Libs/Framework/TestHelpers/provideMockContext.lua b/BuiltInPlugins/AssetConfiguration/Libs/Framework/TestHelpers/provideMockContext.lua index 04cc85664e..10633a0dc7 100644 --- a/BuiltInPlugins/AssetConfiguration/Libs/Framework/TestHelpers/provideMockContext.lua +++ b/BuiltInPlugins/AssetConfiguration/Libs/Framework/TestHelpers/provideMockContext.lua @@ -91,7 +91,7 @@ return function(contextItemsList, children) -- Theme local theme if FlagsList:get("FFlagRefactorDevFrameworkTheme") then - theme = StudioTheme.new() + theme = StudioTheme.mock() else theme = ContextServices.Theme.mock(function(theme, getColor) return { diff --git a/BuiltInPlugins/AssetConfiguration/Libs/Framework/UI/Box/test.spec.lua b/BuiltInPlugins/AssetConfiguration/Libs/Framework/UI/Box/test.spec.lua index 27baa93e01..edc3b7c984 100644 --- a/BuiltInPlugins/AssetConfiguration/Libs/Framework/UI/Box/test.spec.lua +++ b/BuiltInPlugins/AssetConfiguration/Libs/Framework/UI/Box/test.spec.lua @@ -23,7 +23,7 @@ return function() else local theme if FlagsList:get("FFlagRefactorDevFrameworkTheme") then - theme = StudioTheme.new() + theme = StudioTheme.mock() else theme = Theme.new(function() return { diff --git a/BuiltInPlugins/AssetConfiguration/Libs/Framework/UI/Button/test.spec.lua b/BuiltInPlugins/AssetConfiguration/Libs/Framework/UI/Button/test.spec.lua index 3ffc122f23..cafabfaa40 100644 --- a/BuiltInPlugins/AssetConfiguration/Libs/Framework/UI/Button/test.spec.lua +++ b/BuiltInPlugins/AssetConfiguration/Libs/Framework/UI/Button/test.spec.lua @@ -18,7 +18,7 @@ return function() local function createTestButton(props, children) local theme if FlagsList:get("FFlagRefactorDevFrameworkTheme") then - theme = StudioTheme.new() + theme = StudioTheme.mock() else theme = Theme.new(function() return { diff --git a/BuiltInPlugins/AssetConfiguration/Libs/Framework/UI/Container/test.spec.lua b/BuiltInPlugins/AssetConfiguration/Libs/Framework/UI/Container/test.spec.lua index 21e7c8874a..1417100db0 100644 --- a/BuiltInPlugins/AssetConfiguration/Libs/Framework/UI/Container/test.spec.lua +++ b/BuiltInPlugins/AssetConfiguration/Libs/Framework/UI/Container/test.spec.lua @@ -18,7 +18,7 @@ return function() local function createTestContainer(props, children) local theme if FlagsList:get("FFlagRefactorDevFrameworkTheme") then - theme = StudioTheme.new() + theme = StudioTheme.mock() else theme = Theme.new(function() return { diff --git a/BuiltInPlugins/AssetConfiguration/Libs/Framework/UI/DropdownMenu.lua b/BuiltInPlugins/AssetConfiguration/Libs/Framework/UI/DropdownMenu.lua index 60f9fadd0c..b1e6b3c757 100644 --- a/BuiltInPlugins/AssetConfiguration/Libs/Framework/UI/DropdownMenu.lua +++ b/BuiltInPlugins/AssetConfiguration/Libs/Framework/UI/DropdownMenu.lua @@ -44,6 +44,7 @@ local TextLabel = require(UI.TextLabel) local FlagsList = Util.Flags.new({ FFlagRefactorDevFrameworkTheme = {"RefactorDevFrameworkTheme"}, + FFlagRefactorDevFrameworkContextItems = {"RefactorDevFrameworkContextItems"}, }) local DropdownMenu = Roact.PureComponent:extend("DropdownMenu") @@ -201,7 +202,7 @@ function DropdownMenu:renderMenu() local width = style.Width local offset = prioritize(style.Offset, Vector2.new(0, 0)) - local pluginGui = props.Focus:getTarget() + local pluginGui = FlagsList:get("FFlagRefactorDevFrameworkContextItems") and props.Focus:get() or props.Focus:getTarget() local menuPositionAndSize = self.getPositionAndSize(pluginGui, width, offset) local x = menuPositionAndSize.X diff --git a/BuiltInPlugins/AssetConfiguration/Libs/Framework/UI/FakeLoadingBar/test.spec.lua b/BuiltInPlugins/AssetConfiguration/Libs/Framework/UI/FakeLoadingBar/test.spec.lua index 4533d1b2a4..1306596adc 100644 --- a/BuiltInPlugins/AssetConfiguration/Libs/Framework/UI/FakeLoadingBar/test.spec.lua +++ b/BuiltInPlugins/AssetConfiguration/Libs/Framework/UI/FakeLoadingBar/test.spec.lua @@ -17,7 +17,7 @@ return function() local function createTestFakeLoadingBar(props, children) local theme if FlagsList:get("FFlagRefactorDevFrameworkTheme") then - theme = StudioTheme.new() + theme = StudioTheme.mock() else theme = Theme.new(function() return { diff --git a/BuiltInPlugins/AssetConfiguration/Libs/Framework/UI/Image/test.spec.lua b/BuiltInPlugins/AssetConfiguration/Libs/Framework/UI/Image/test.spec.lua index c5aa507a3a..6860be79dd 100644 --- a/BuiltInPlugins/AssetConfiguration/Libs/Framework/UI/Image/test.spec.lua +++ b/BuiltInPlugins/AssetConfiguration/Libs/Framework/UI/Image/test.spec.lua @@ -17,7 +17,7 @@ return function() local function createTestImageDecoration() local theme if FlagsList:get("FFlagRefactorDevFrameworkTheme") then - theme = StudioTheme.new() + theme = StudioTheme.mock() else theme = Theme.new(function() return { diff --git a/BuiltInPlugins/AssetConfiguration/Libs/Framework/UI/InstanceTreeView/InstanceTreeRow.lua b/BuiltInPlugins/AssetConfiguration/Libs/Framework/UI/InstanceTreeView/InstanceTreeRow.lua index 8000e57785..6110490a54 100644 --- a/BuiltInPlugins/AssetConfiguration/Libs/Framework/UI/InstanceTreeView/InstanceTreeRow.lua +++ b/BuiltInPlugins/AssetConfiguration/Libs/Framework/UI/InstanceTreeView/InstanceTreeRow.lua @@ -25,7 +25,7 @@ local TextLabel = require(UI.TextLabel) Typecheck.wrap(InstanceTreeRow, script) local function getClassIcon(instance) - local StudioService = game:GetService("StudioService") + local StudioService = game:GetService("StudioService") local className = instance.ClassName if instance.IsA then if instance:IsA("JointInstance") and className == "ManualWeld" or className == "ManualGlue" then @@ -73,9 +73,10 @@ function InstanceTreeRow:render() local arrowSize = style.Arrow.Size local padding = style.IconPadding local iconInfo = getClassIcon(item) - local iconSize = iconInfo.ImageRectSize.X + -- Default iconSize to (0, 0) as ImageRectSize is unavailable in Roblox CLI + local iconSize = iconInfo.ImageRectSize or Vector2.new() local labelOffset = indent + arrowSize + 2 * padding - local textOffset = iconSize + 3 * padding + local textOffset = iconSize.X + 3 * padding return Roact.createElement(Container, { Size = UDim2.new(1, -indent, 0, style.RowHeight), @@ -105,10 +106,10 @@ function InstanceTreeRow:render() Size = UDim2.new(1, -arrowSize, 1, 0), }, { Icon = Roact.createElement("ImageLabel", { - Size = UDim2.fromOffset(iconSize, iconInfo.ImageRectSize.Y), + Size = UDim2.fromOffset(iconSize.X, iconSize.Y), BackgroundTransparency = 1, Image = iconInfo.Image, - ImageRectSize = iconInfo.ImageRectSize, + ImageRectSize = iconSize, ImageRectOffset = iconInfo.ImageRectOffset, Position = UDim2.new(0, padding, 0.5, 0), AnchorPoint = Vector2.new(0, 0.5) diff --git a/BuiltInPlugins/AssetConfiguration/Libs/Framework/UI/LinkText/test.spec.lua b/BuiltInPlugins/AssetConfiguration/Libs/Framework/UI/LinkText/test.spec.lua index d72d172041..37cecf30b1 100644 --- a/BuiltInPlugins/AssetConfiguration/Libs/Framework/UI/LinkText/test.spec.lua +++ b/BuiltInPlugins/AssetConfiguration/Libs/Framework/UI/LinkText/test.spec.lua @@ -19,7 +19,7 @@ return function() local mouse = Mouse.new({}) local theme if FlagsList:get("FFlagRefactorDevFrameworkTheme") then - theme = StudioTheme.new() + theme = StudioTheme.mock() else theme = Theme.new(function() return { diff --git a/BuiltInPlugins/AssetConfiguration/Libs/Framework/UI/LoadingBar/test.spec.lua b/BuiltInPlugins/AssetConfiguration/Libs/Framework/UI/LoadingBar/test.spec.lua index ea437b136c..c898cde0f4 100644 --- a/BuiltInPlugins/AssetConfiguration/Libs/Framework/UI/LoadingBar/test.spec.lua +++ b/BuiltInPlugins/AssetConfiguration/Libs/Framework/UI/LoadingBar/test.spec.lua @@ -18,7 +18,7 @@ return function() local function createTestLoadingBar(props, children) local theme if FlagsList:get("FFlagRefactorDevFrameworkTheme") then - theme = StudioTheme.new() + theme = StudioTheme.mock() else theme = Theme.new(function() return { diff --git a/BuiltInPlugins/AssetConfiguration/Libs/Framework/UI/RadioButton/test.spec.lua b/BuiltInPlugins/AssetConfiguration/Libs/Framework/UI/RadioButton/test.spec.lua index 31e2c12c77..4e578229a9 100644 --- a/BuiltInPlugins/AssetConfiguration/Libs/Framework/UI/RadioButton/test.spec.lua +++ b/BuiltInPlugins/AssetConfiguration/Libs/Framework/UI/RadioButton/test.spec.lua @@ -23,7 +23,7 @@ return function() local function createTestRadioButton(props, children) local theme if FlagsList:get("FFlagRefactorDevFrameworkTheme") then - theme = StudioTheme.new() + theme = StudioTheme.mock() else theme = Theme.new(function() return { diff --git a/BuiltInPlugins/AssetConfiguration/Libs/Framework/UI/RoundBox/test.spec.lua b/BuiltInPlugins/AssetConfiguration/Libs/Framework/UI/RoundBox/test.spec.lua index 10b16f79a0..d9715cf163 100644 --- a/BuiltInPlugins/AssetConfiguration/Libs/Framework/UI/RoundBox/test.spec.lua +++ b/BuiltInPlugins/AssetConfiguration/Libs/Framework/UI/RoundBox/test.spec.lua @@ -17,7 +17,7 @@ return function() local function createTestRoundBoxDecoration() local theme if FlagsList:get("FFlagRefactorDevFrameworkTheme") then - theme = StudioTheme.new() + theme = StudioTheme.mock() else theme = Theme.new(function() return { diff --git a/BuiltInPlugins/AssetConfiguration/Libs/Framework/UI/TextLabel/test.spec.lua b/BuiltInPlugins/AssetConfiguration/Libs/Framework/UI/TextLabel/test.spec.lua index a403d8b7d3..5f739cb55f 100644 --- a/BuiltInPlugins/AssetConfiguration/Libs/Framework/UI/TextLabel/test.spec.lua +++ b/BuiltInPlugins/AssetConfiguration/Libs/Framework/UI/TextLabel/test.spec.lua @@ -17,7 +17,7 @@ return function() local function createTestTextLabelDecoration() local theme if FlagsList:get("FFlagRefactorDevFrameworkTheme") then - theme = StudioTheme.new() + theme = StudioTheme.mock() else theme = Theme.new(function() return { diff --git a/BuiltInPlugins/AssetConfiguration/Libs/Framework/UI/ToggleButton/test.spec.lua b/BuiltInPlugins/AssetConfiguration/Libs/Framework/UI/ToggleButton/test.spec.lua index 0d712bdf45..c4db80d801 100644 --- a/BuiltInPlugins/AssetConfiguration/Libs/Framework/UI/ToggleButton/test.spec.lua +++ b/BuiltInPlugins/AssetConfiguration/Libs/Framework/UI/ToggleButton/test.spec.lua @@ -27,7 +27,7 @@ return function() local mouse = Mouse.new({}) local theme if FlagsList:get("FFlagRefactorDevFrameworkTheme") then - theme = StudioTheme.new() + theme = StudioTheme.mock() else theme = Theme.new(function() return { diff --git a/BuiltInPlugins/AssetConfiguration/Libs/Framework/UI/Tooltip/test.spec.lua b/BuiltInPlugins/AssetConfiguration/Libs/Framework/UI/Tooltip/test.spec.lua index 984329d7eb..8f7a0a6fc9 100644 --- a/BuiltInPlugins/AssetConfiguration/Libs/Framework/UI/Tooltip/test.spec.lua +++ b/BuiltInPlugins/AssetConfiguration/Libs/Framework/UI/Tooltip/test.spec.lua @@ -25,7 +25,7 @@ return function() local focus = Focus.new(target) local theme if FlagsList:get("FFlagRefactorDevFrameworkTheme") then - theme = StudioTheme.new() + theme = StudioTheme.mock() else theme = Theme.new(function() return { diff --git a/BuiltInPlugins/AssetConfiguration/Libs/Framework/UI/TreeView/test.spec.lua b/BuiltInPlugins/AssetConfiguration/Libs/Framework/UI/TreeView/test.spec.lua index 953aef3483..8d06ee8a86 100644 --- a/BuiltInPlugins/AssetConfiguration/Libs/Framework/UI/TreeView/test.spec.lua +++ b/BuiltInPlugins/AssetConfiguration/Libs/Framework/UI/TreeView/test.spec.lua @@ -63,7 +63,7 @@ return function() local function createTreeView() local theme if FlagsList:get("FFlagRefactorDevFrameworkTheme") then - theme = StudioTheme.new() + theme = StudioTheme.mock() else theme = Theme.new(function() return { diff --git a/BuiltInPlugins/AssetConfiguration/Libs/Framework/Util.lua b/BuiltInPlugins/AssetConfiguration/Libs/Framework/Util.lua index 5454c404f1..6a3e12c089 100644 --- a/BuiltInPlugins/AssetConfiguration/Libs/Framework/Util.lua +++ b/BuiltInPlugins/AssetConfiguration/Libs/Framework/Util.lua @@ -19,6 +19,7 @@ return strict({ FitFrame = require(script.FitFrame), Flags = require(script.Flags), + getTestVariation = require(script.getTestVariation), Immutable = require(script.Immutable), LayoutOrderIterator = require(script.LayoutOrderIterator), Math = require(script.Math), diff --git a/BuiltInPlugins/AssetConfiguration/Libs/Framework/Util/CrossPluginCommunication.lua b/BuiltInPlugins/AssetConfiguration/Libs/Framework/Util/CrossPluginCommunication.lua index 37caa1bece..d3b82f5783 100644 --- a/BuiltInPlugins/AssetConfiguration/Libs/Framework/Util/CrossPluginCommunication.lua +++ b/BuiltInPlugins/AssetConfiguration/Libs/Framework/Util/CrossPluginCommunication.lua @@ -22,10 +22,12 @@ CrossPluginCommunication.__index = CrossPluginCommunication CrossPluginCommunication.BASE_FOLDER_NAME = "CrossPluginCommunication" -function CrossPluginCommunication.new(pluginNamespace) +function CrossPluginCommunication.new(pluginNamespace, hostService) assert(t.string(pluginNamespace), "pluginNamespace must be a string") + assert(t.optional(t.instance(pluginNamespace)), "hostService must be an instance if defined") local self = { + hostService = hostService or RobloxPluginGuiService, pluginNamespace = pluginNamespace } @@ -49,7 +51,7 @@ function CrossPluginCommunication:ensureFolderExists(parent, name) end function CrossPluginCommunication:getNamespaceFolder() - local base = self:ensureFolderExists(RobloxPluginGuiService, self.BASE_FOLDER_NAME) + local base = self:ensureFolderExists(self.hostService, self.BASE_FOLDER_NAME) return self:ensureFolderExists(base, self.pluginNamespace) end diff --git a/BuiltInPlugins/AssetConfiguration/Libs/Framework/Util/CrossPluginCommunication.spec.lua b/BuiltInPlugins/AssetConfiguration/Libs/Framework/Util/CrossPluginCommunication.spec.lua index 60338f2b0c..f3c1035f50 100644 --- a/BuiltInPlugins/AssetConfiguration/Libs/Framework/Util/CrossPluginCommunication.spec.lua +++ b/BuiltInPlugins/AssetConfiguration/Libs/Framework/Util/CrossPluginCommunication.spec.lua @@ -1,5 +1,3 @@ -local RobloxPluginGuiService = game:GetService("RobloxPluginGuiService") - local Framework = script.Parent.Parent local CrossPluginCommunication = require(Framework.Util.CrossPluginCommunication) @@ -8,8 +6,10 @@ return function() -- TODO DEVTOOLS-4397: Move cleanup to afterEach hook after the dependency structure is fixed + local hostService = workspace + local function createTestComms() - return CrossPluginCommunication.new(TEST_NAMESPACE) + return CrossPluginCommunication.new(TEST_NAMESPACE, hostService) end describe("Invoke", function() @@ -32,7 +32,7 @@ return function() comms:OnInvoke("blah", function() end) - local namespace = RobloxPluginGuiService:FindFirstChild(CrossPluginCommunication.BASE_FOLDER_NAME):FindFirstChild(TEST_NAMESPACE) + local namespace = hostService:FindFirstChild(CrossPluginCommunication.BASE_FOLDER_NAME):FindFirstChild(TEST_NAMESPACE) assert(namespace) diff --git a/BuiltInPlugins/AssetConfiguration/Libs/Framework/Util/Palette.spec.lua b/BuiltInPlugins/AssetConfiguration/Libs/Framework/Util/Palette.spec.lua index 8794c80717..02911b17c7 100644 --- a/BuiltInPlugins/AssetConfiguration/Libs/Framework/Util/Palette.spec.lua +++ b/BuiltInPlugins/AssetConfiguration/Libs/Framework/Util/Palette.spec.lua @@ -1,5 +1,16 @@ return function() - local Palette = require(script.Parent.Palette) + + local Util = script.Parent + local Flags = require(Util.Flags) + local FlagsList = Flags.new({ + FFlagRefactorDevFrameworkTheme = {"RefactorDevFrameworkTheme"}, + }) + + if FlagsList:get("FFlagRefactorDevFrameworkTheme") then + return + end + + local Palette = require(Util.Palette) it("should be a table of color StyleValues", function() expect(Palette).to.be.ok() diff --git a/BuiltInPlugins/AssetConfiguration/Libs/Framework/Util/Typecheck/t.lua b/BuiltInPlugins/AssetConfiguration/Libs/Framework/Util/Typecheck/t.lua index 44c8d50af8..045d7ac0dd 100644 --- a/BuiltInPlugins/AssetConfiguration/Libs/Framework/Util/Typecheck/t.lua +++ b/BuiltInPlugins/AssetConfiguration/Libs/Framework/Util/Typecheck/t.lua @@ -1,3 +1,5 @@ +--!nocheck + --[[ DeveloperFramework uses t from Osyris https://github.com/osyrisrblx/t diff --git a/BuiltInPlugins/AssetConfiguration/Libs/Framework/Util/getTestVariation.lua b/BuiltInPlugins/AssetConfiguration/Libs/Framework/Util/getTestVariation.lua new file mode 100644 index 0000000000..d09873db1d --- /dev/null +++ b/BuiltInPlugins/AssetConfiguration/Libs/Framework/Util/getTestVariation.lua @@ -0,0 +1,30 @@ +-- This function hides ABTestService away and provides a quick interface for getting a test's variation. +-- Should the requested test not exist, it will return the Control group. + +local ABTestService = game:GetService("ABTestService") + +local AB_TEST_GROUP_CONTROL = "Control" + +-- abTestName : (string) the name of test. ex) "AllUsers.Studio.ExampleTestName" +-- abTestService : (table, optional) an optional override for ABTestService +-- RETURNS : (int) the variation that the user is in. 0 = Control +return function(abTestName, abTestService) + assert(type(abTestName) == "string", "Expected abTestName to be a string") + if abTestService then + assert(type(abTestService.GetVariant) == "function", "Expected the abTestService object to have a GetVariant member function") + else + abTestService = ABTestService + end + + local variation = abTestService:GetVariant(abTestName) + if variation == AB_TEST_GROUP_CONTROL then + return 0 + else + local _, _, variationNumber = string.find(variation, "Variation(%d+)") + if not variationNumber then + return 0 + end + + return tonumber(variationNumber) + end +end diff --git a/BuiltInPlugins/AssetConfiguration/Libs/Framework/Util/getTestVariation.spec.lua b/BuiltInPlugins/AssetConfiguration/Libs/Framework/Util/getTestVariation.spec.lua new file mode 100644 index 0000000000..5febceee91 --- /dev/null +++ b/BuiltInPlugins/AssetConfiguration/Libs/Framework/Util/getTestVariation.spec.lua @@ -0,0 +1,41 @@ +return function() + local getTestVariation = require(script.Parent.getTestVariation) + local VARIATION_NAME_CONTROL = "Control" + + local function createTestService(result) + local DebugABService = {} + + function DebugABService:GetVariant(testName) + return result + end + + return DebugABService + end + + it("should expect some test name", function() + expect(function() + getTestVariation() + end).to.throw() + end) + + it("should return zero when in the control group", function() + local debugABTestService = createTestService(VARIATION_NAME_CONTROL) + local variation = getTestVariation("AllUsers.Studio.ShouldShowZero", debugABTestService) + expect(variation).to.equal(0) + end) + + it("should return the number of the variation when not in the control group", function() + local debugABTestService = createTestService("Variation1") + local variation = getTestVariation("AllUsers.Studio.ShouldShowNumber", debugABTestService) + expect(variation).to.equal(1) + + -- there aren't any variations past 8, but it should work with any number + debugABTestService = createTestService("Variation23") + variation = getTestVariation("AllUsers.Studio.ShouldShowNumber", debugABTestService) + expect(variation).to.equal(23) + + debugABTestService = createTestService("Variation456") + variation = getTestVariation("AllUsers.Studio.ShouldShowNumber", debugABTestService) + expect(variation).to.equal(456) + end) +end \ No newline at end of file diff --git a/BuiltInPlugins/AssetConfiguration/Libs/UILibrary/_internal/Utils/Symbol.spec.lua b/BuiltInPlugins/AssetConfiguration/Libs/UILibrary/_internal/Utils/Symbol.spec.lua index cde9be065b..f3312055c9 100644 --- a/BuiltInPlugins/AssetConfiguration/Libs/UILibrary/_internal/Utils/Symbol.spec.lua +++ b/BuiltInPlugins/AssetConfiguration/Libs/UILibrary/_internal/Utils/Symbol.spec.lua @@ -11,7 +11,7 @@ return function() it("should coerce to the given name", function() local symbol = Symbol.named("foo") - expect(tostring(symbol):find("foo")).to.be.ok() + expect(tostring(symbol):match("foo")).to.be.ok() end) it("should be unique when constructed", function() @@ -42,4 +42,4 @@ return function() expect(symbolA).never.to.equal(symbolB) end) end) -end \ No newline at end of file +end diff --git a/BuiltInPlugins/AssetManager/Bin/main.server.lua b/BuiltInPlugins/AssetManager/Bin/main.server.lua index a1253515bc..5918cd187c 100644 --- a/BuiltInPlugins/AssetManager/Bin/main.server.lua +++ b/BuiltInPlugins/AssetManager/Bin/main.server.lua @@ -8,6 +8,7 @@ local OverrideLocaleId = settings():GetFVariable("StudioForceLocale") local FFlagAssetManagerLuaPlugin = settings():GetFFlag("AssetManagerLuaPlugin") local FFlagAssetManagerAddAnalytics = game:DefineFastFlag("AssetManagerAddAnalytics", false) local FFlagStudioAssetManagerAddRecentlyImportedView = game:GetFastFlag("StudioAssetManagerAddRecentlyImportedView") +local FFlagStudioShowHideABTestV2 = game:GetFastFlag("StudioShowHideABTestV2") if not FFlagAssetManagerLuaPlugin then return @@ -21,9 +22,10 @@ local Plugin = script.Parent.Parent local Roact = require(Plugin.Packages.Roact) local Rodux = require(Plugin.Packages.Rodux) local Cryo = require(Plugin.Packages.Cryo) +local Framework = require(Plugin.Packages.Framework) -- context services -local ContextServices = require(Plugin.Packages.Framework).ContextServices +local ContextServices = Framework.ContextServices local ServiceWrapper = require(Plugin.Src.Components.ServiceWrapper) local UILibraryWrapper = ContextServices.UILibraryWrapper @@ -47,12 +49,15 @@ local MainView = require(Plugin.Src.Components.MainView) local SetBulkImporterRunning = require(Plugin.Src.Actions.SetBulkImporterRunning) local SetRecentAssets = require(Plugin.Src.Actions.SetRecentAssets) +local SetRecentViewToggled = require(Plugin.Src.Actions.SetRecentViewToggled) local PLUGIN_NAME = "AssetManager" local TOOLBAR_NAME = "assetManagerToolbar" local TOOLBAR_BUTTON_NAME = "assetManagerToolButton" local DOCK_WIDGET_PLUGIN_NAME = "AssetManager_PluginGui" +local ABTEST_SHOWHIDEV2_NAME = "AllUsers.RobloxStudio.ShowHideV2" + -- Plugin Specific Globals local store = Rodux.Store.new(MainReducer, {}, MainMiddleware) local theme = PluginTheme:makePluginTheme() @@ -110,9 +115,18 @@ end local function connectBulkImporterSignals() BulkImportService.BulkImportStarted:connect(function() store:dispatch(SetBulkImporterRunning(true)) + if FFlagStudioAssetManagerAddRecentlyImportedView then + store:dispatch(SetRecentAssets({})) + end end) BulkImportService.BulkImportFinished:connect(function(state) store:dispatch(SetBulkImporterRunning(false)) + if FFlagStudioAssetManagerAddRecentlyImportedView then + local state = store:getState() + if #state.AssetManagerReducer.recentAssets > 0 then + store:dispatch(SetRecentViewToggled(true)) + end + end end) if FFlagStudioAssetManagerAddRecentlyImportedView then BulkImportService.AssetImported:connect(function(assetType, name, id) @@ -161,10 +175,18 @@ local function main() toolbarButton:SetActive(pluginGui.Enabled) end + local initiallyEnabled = true + if FFlagStudioShowHideABTestV2 then + local variation = Framework.Util.getTestVariation(ABTEST_SHOWHIDEV2_NAME) + if variation == 2 then + initiallyEnabled = false + end + end + -- create the plugin local widgetInfo = DockWidgetPluginGuiInfo.new( Enum.InitialDockState.Left, -- Widget will be initialized docked to the left - true, -- Widget will be initially enabled + initiallyEnabled, -- Widget will be initially enabled false, -- Don't override the previous enabled state 300, -- Default width of the floating window 600, -- Default height of the floating window diff --git a/BuiltInPlugins/AssetManager/Packages/Framework/ContextServices/Theme.spec.lua b/BuiltInPlugins/AssetManager/Packages/Framework/ContextServices/Theme.spec.lua index 22377dc00b..1547fb3c11 100644 --- a/BuiltInPlugins/AssetManager/Packages/Framework/ContextServices/Theme.spec.lua +++ b/BuiltInPlugins/AssetManager/Packages/Framework/ContextServices/Theme.spec.lua @@ -11,6 +11,13 @@ return function() local StyleTable = Util.StyleTable local StyleModifier = Util.StyleModifier local Signal = Util.Signal + local FlagsList = Util.Flags.new({ + FFlagRefactorDevFrameworkTheme = {"RefactorDevFrameworkTheme"}, + }) + + if FlagsList:get("FFlagRefactorDevFrameworkTheme") then + return + end local function createTestThemedComponent(render) local TestThemedComponent = Roact.PureComponent:extend("TestThemedComponent") diff --git a/BuiltInPlugins/AssetManager/Packages/Framework/ContextServices/UILibraryWrapper.spec.lua b/BuiltInPlugins/AssetManager/Packages/Framework/ContextServices/UILibraryWrapper.spec.lua index 0823cb2280..d4bbb673db 100644 --- a/BuiltInPlugins/AssetManager/Packages/Framework/ContextServices/UILibraryWrapper.spec.lua +++ b/BuiltInPlugins/AssetManager/Packages/Framework/ContextServices/UILibraryWrapper.spec.lua @@ -2,8 +2,10 @@ return function() local Framework = script.Parent.Parent local Util = require(Framework.Util) + local Signal = Util.Signal local FlagsList = Util.Flags.new({ FFlagStudioDevFrameworkPackage = {"StudioDevFrameworkPackage"}, + FFlagRefactorDevFrameworkTheme = {"RefactorDevFrameworkTheme"}, }) local isUsedAsPackage = require(Framework.Util.isUsedAsPackage) @@ -18,6 +20,7 @@ return function() local Focus = require(Framework.ContextServices.Focus) local Theme = require(Framework.ContextServices.Theme) local UILibraryWrapper = require(script.Parent.UILibraryWrapper) + local StudioTheme = require(Framework.Style.Themes.StudioTheme) local WrapperStub = Roact.PureComponent:extend("UILibraryWrapper") @@ -29,12 +32,26 @@ return function() Wrapper = WrapperStub } + local function createThemeMock() + + if FlagsList:get("FFlagRefactorDevFrameworkTheme") then + return StudioTheme.mock() + else + local createStyles = function() + return {} + end + local getTheme = function() + return "Light" + end + local themeChanged = Signal.new() + return Theme.mock(createStyles, getTheme, themeChanged) + end + end + it("should expect a Plugin ContextItem provided above", function() local focus = Focus.new(Instance.new("ScreenGui")) local wrapper = UILibraryWrapper.new(UILibraryStub) - local theme = Theme.new(function() - return {} - end) + local theme = createThemeMock() function theme:getUILibraryTheme() return {} end @@ -55,9 +72,7 @@ return function() it("should expect a Focus ContextItem provided above", function() local plugin = Plugin.new({}) local wrapper = UILibraryWrapper.new(UILibraryStub) - local theme = Theme.new(function() - return {} - end) + local theme = createThemeMock() function theme:getUILibraryTheme() return {} end @@ -97,9 +112,7 @@ return function() local plugin = Plugin.new({}) local focus = Focus.new(Instance.new("ScreenGui")) local wrapper = UILibraryWrapper.new(UILibraryStub) - local theme = Theme.new(function() - return {} - end) + local theme = createThemeMock() local element = provide({ plugin, @@ -119,9 +132,7 @@ return function() local plugin = Plugin.new({}) local focus = Focus.new(Instance.new("ScreenGui")) local wrapper = UILibraryWrapper.new(UILibraryStub) - local theme = Theme.new(function() - return {} - end) + local theme = createThemeMock() function theme:getUILibraryTheme() return {} end diff --git a/BuiltInPlugins/AssetManager/Packages/Framework/StudioUI/TitledFrame/test.spec.lua b/BuiltInPlugins/AssetManager/Packages/Framework/StudioUI/TitledFrame/test.spec.lua index cff37efea8..f013a391bc 100644 --- a/BuiltInPlugins/AssetManager/Packages/Framework/StudioUI/TitledFrame/test.spec.lua +++ b/BuiltInPlugins/AssetManager/Packages/Framework/StudioUI/TitledFrame/test.spec.lua @@ -17,7 +17,7 @@ return function() local function createTestTitledFrame(children, container) local theme if FlagsList:get("FFlagRefactorDevFrameworkTheme") then - theme = StudioTheme.new() + theme = StudioTheme.mock() else theme = Theme.new(function() return { diff --git a/BuiltInPlugins/AssetManager/Packages/Framework/StudioUI/createPluginWidget.lua b/BuiltInPlugins/AssetManager/Packages/Framework/StudioUI/createPluginWidget.lua index 85620982d3..b9dd6d3759 100644 --- a/BuiltInPlugins/AssetManager/Packages/Framework/StudioUI/createPluginWidget.lua +++ b/BuiltInPlugins/AssetManager/Packages/Framework/StudioUI/createPluginWidget.lua @@ -9,10 +9,12 @@ ]] game:DefineFastFlag("FixDevFrameworkDockWidgetRestore", false) -game:DefineFastFlag("DevFrameworkPluginWidgetEnabledEvent", false) +game:DefineFastFlag("DevFrameworkPluginWidgetEnabledEvent2", false) +game:DefineFastFlag("DevFrameworkPluginWidgetUseSiblingZIndex", false) local FFlagFixDevFrameworkDockWidgetRestore = game:GetFastFlag("FixDevFrameworkDockWidgetRestore") -local FFlagDevFrameworkPluginWidgetEnabledEvent = game:GetFastFlag("DevFrameworkPluginWidgetEnabledEvent") +local FFlagDevFrameworkPluginWidgetEnabledEvent2 = game:GetFastFlag("DevFrameworkPluginWidgetEnabledEvent2") +local FFlagDevFrameworkPluginWidgetUseSiblingZIndex = game:GetFastFlag("DevFrameworkPluginWidgetUseSiblingZIndex") local Framework = script.Parent.Parent local Roact = require(Framework.Parent.Roact) @@ -31,7 +33,11 @@ local function createPluginWidget(componentName, createWidgetFunc) local widget = createWidgetFunc(props) widget.Name = title or "" - widget.ZIndexBehavior = props.ZIndexBehavior or Enum.ZIndexBehavior.Global + if FFlagDevFrameworkPluginWidgetUseSiblingZIndex then + widget.ZIndexBehavior = props.ZIndexBehavior or Enum.ZIndexBehavior.Sibling + else + widget.ZIndexBehavior = props.ZIndexBehavior or Enum.ZIndexBehavior.Global + end if widget:IsA("PluginGui") then widget:BindToClose(onClose) @@ -67,12 +73,13 @@ local function createPluginWidget(componentName, createWidgetFunc) end end - if FFlagDevFrameworkPluginWidgetEnabledEvent then + if FFlagDevFrameworkPluginWidgetEnabledEvent2 then -- Connect to enabled changing *after* restore -- Otherwise users of this will get 2 enabled changes: one from the onRestore, and the same from Roact.Change.Enabled self.widgetEnabledChangedConnection = widget:GetPropertyChangedSignal("Enabled"):Connect(function() - if self.props[Roact.Change.Enabled] then - self.props[Roact.Change.Enabled](self.widget) + local callback = self.props[Roact.Change.Enabled] + if callback and self.widget and self.widget.Enabled ~= self.props.Enabled then + callback(self.widget) end end) end diff --git a/BuiltInPlugins/AssetManager/Packages/Framework/Style/Stylizer.lua b/BuiltInPlugins/AssetManager/Packages/Framework/Style/Stylizer.lua index 47a60c5663..228c4b165a 100644 --- a/BuiltInPlugins/AssetManager/Packages/Framework/Style/Stylizer.lua +++ b/BuiltInPlugins/AssetManager/Packages/Framework/Style/Stylizer.lua @@ -1,3 +1,5 @@ +--!nocheck + --[[ Wraps theme styles and update logic into a ContextItem. @@ -318,4 +320,4 @@ function Stylizer.mock(t, themeProps, callback) return self end -return Stylizer \ No newline at end of file +return Stylizer diff --git a/BuiltInPlugins/AssetManager/Packages/Framework/Style/getRawComponentStyle.lua b/BuiltInPlugins/AssetManager/Packages/Framework/Style/getRawComponentStyle.lua index 7e659ca0c0..5c76de757c 100644 --- a/BuiltInPlugins/AssetManager/Packages/Framework/Style/getRawComponentStyle.lua +++ b/BuiltInPlugins/AssetManager/Packages/Framework/Style/getRawComponentStyle.lua @@ -9,6 +9,7 @@ local UIFolderData = require(Framework.UI.UIFolderData) return function(componentName) local componentData = UIFolderData[componentName] or StudioUIFolderData[componentName] local result + if componentData.style then result = require(componentData.style) end diff --git a/BuiltInPlugins/AssetManager/Packages/Framework/TestHelpers/provideMockContext.lua b/BuiltInPlugins/AssetManager/Packages/Framework/TestHelpers/provideMockContext.lua index 04cc85664e..10633a0dc7 100644 --- a/BuiltInPlugins/AssetManager/Packages/Framework/TestHelpers/provideMockContext.lua +++ b/BuiltInPlugins/AssetManager/Packages/Framework/TestHelpers/provideMockContext.lua @@ -91,7 +91,7 @@ return function(contextItemsList, children) -- Theme local theme if FlagsList:get("FFlagRefactorDevFrameworkTheme") then - theme = StudioTheme.new() + theme = StudioTheme.mock() else theme = ContextServices.Theme.mock(function(theme, getColor) return { diff --git a/BuiltInPlugins/AssetManager/Packages/Framework/UI/Box/test.spec.lua b/BuiltInPlugins/AssetManager/Packages/Framework/UI/Box/test.spec.lua index 27baa93e01..edc3b7c984 100644 --- a/BuiltInPlugins/AssetManager/Packages/Framework/UI/Box/test.spec.lua +++ b/BuiltInPlugins/AssetManager/Packages/Framework/UI/Box/test.spec.lua @@ -23,7 +23,7 @@ return function() else local theme if FlagsList:get("FFlagRefactorDevFrameworkTheme") then - theme = StudioTheme.new() + theme = StudioTheme.mock() else theme = Theme.new(function() return { diff --git a/BuiltInPlugins/AssetManager/Packages/Framework/UI/Button/test.spec.lua b/BuiltInPlugins/AssetManager/Packages/Framework/UI/Button/test.spec.lua index 3ffc122f23..cafabfaa40 100644 --- a/BuiltInPlugins/AssetManager/Packages/Framework/UI/Button/test.spec.lua +++ b/BuiltInPlugins/AssetManager/Packages/Framework/UI/Button/test.spec.lua @@ -18,7 +18,7 @@ return function() local function createTestButton(props, children) local theme if FlagsList:get("FFlagRefactorDevFrameworkTheme") then - theme = StudioTheme.new() + theme = StudioTheme.mock() else theme = Theme.new(function() return { diff --git a/BuiltInPlugins/AssetManager/Packages/Framework/UI/Container/test.spec.lua b/BuiltInPlugins/AssetManager/Packages/Framework/UI/Container/test.spec.lua index 21e7c8874a..1417100db0 100644 --- a/BuiltInPlugins/AssetManager/Packages/Framework/UI/Container/test.spec.lua +++ b/BuiltInPlugins/AssetManager/Packages/Framework/UI/Container/test.spec.lua @@ -18,7 +18,7 @@ return function() local function createTestContainer(props, children) local theme if FlagsList:get("FFlagRefactorDevFrameworkTheme") then - theme = StudioTheme.new() + theme = StudioTheme.mock() else theme = Theme.new(function() return { diff --git a/BuiltInPlugins/AssetManager/Packages/Framework/UI/DropdownMenu.lua b/BuiltInPlugins/AssetManager/Packages/Framework/UI/DropdownMenu.lua index 60f9fadd0c..b1e6b3c757 100644 --- a/BuiltInPlugins/AssetManager/Packages/Framework/UI/DropdownMenu.lua +++ b/BuiltInPlugins/AssetManager/Packages/Framework/UI/DropdownMenu.lua @@ -44,6 +44,7 @@ local TextLabel = require(UI.TextLabel) local FlagsList = Util.Flags.new({ FFlagRefactorDevFrameworkTheme = {"RefactorDevFrameworkTheme"}, + FFlagRefactorDevFrameworkContextItems = {"RefactorDevFrameworkContextItems"}, }) local DropdownMenu = Roact.PureComponent:extend("DropdownMenu") @@ -201,7 +202,7 @@ function DropdownMenu:renderMenu() local width = style.Width local offset = prioritize(style.Offset, Vector2.new(0, 0)) - local pluginGui = props.Focus:getTarget() + local pluginGui = FlagsList:get("FFlagRefactorDevFrameworkContextItems") and props.Focus:get() or props.Focus:getTarget() local menuPositionAndSize = self.getPositionAndSize(pluginGui, width, offset) local x = menuPositionAndSize.X diff --git a/BuiltInPlugins/AssetManager/Packages/Framework/UI/FakeLoadingBar/test.spec.lua b/BuiltInPlugins/AssetManager/Packages/Framework/UI/FakeLoadingBar/test.spec.lua index 4533d1b2a4..1306596adc 100644 --- a/BuiltInPlugins/AssetManager/Packages/Framework/UI/FakeLoadingBar/test.spec.lua +++ b/BuiltInPlugins/AssetManager/Packages/Framework/UI/FakeLoadingBar/test.spec.lua @@ -17,7 +17,7 @@ return function() local function createTestFakeLoadingBar(props, children) local theme if FlagsList:get("FFlagRefactorDevFrameworkTheme") then - theme = StudioTheme.new() + theme = StudioTheme.mock() else theme = Theme.new(function() return { diff --git a/BuiltInPlugins/AssetManager/Packages/Framework/UI/Image/test.spec.lua b/BuiltInPlugins/AssetManager/Packages/Framework/UI/Image/test.spec.lua index c5aa507a3a..6860be79dd 100644 --- a/BuiltInPlugins/AssetManager/Packages/Framework/UI/Image/test.spec.lua +++ b/BuiltInPlugins/AssetManager/Packages/Framework/UI/Image/test.spec.lua @@ -17,7 +17,7 @@ return function() local function createTestImageDecoration() local theme if FlagsList:get("FFlagRefactorDevFrameworkTheme") then - theme = StudioTheme.new() + theme = StudioTheme.mock() else theme = Theme.new(function() return { diff --git a/BuiltInPlugins/AssetManager/Packages/Framework/UI/InstanceTreeView/InstanceTreeRow.lua b/BuiltInPlugins/AssetManager/Packages/Framework/UI/InstanceTreeView/InstanceTreeRow.lua index 8000e57785..6110490a54 100644 --- a/BuiltInPlugins/AssetManager/Packages/Framework/UI/InstanceTreeView/InstanceTreeRow.lua +++ b/BuiltInPlugins/AssetManager/Packages/Framework/UI/InstanceTreeView/InstanceTreeRow.lua @@ -25,7 +25,7 @@ local TextLabel = require(UI.TextLabel) Typecheck.wrap(InstanceTreeRow, script) local function getClassIcon(instance) - local StudioService = game:GetService("StudioService") + local StudioService = game:GetService("StudioService") local className = instance.ClassName if instance.IsA then if instance:IsA("JointInstance") and className == "ManualWeld" or className == "ManualGlue" then @@ -73,9 +73,10 @@ function InstanceTreeRow:render() local arrowSize = style.Arrow.Size local padding = style.IconPadding local iconInfo = getClassIcon(item) - local iconSize = iconInfo.ImageRectSize.X + -- Default iconSize to (0, 0) as ImageRectSize is unavailable in Roblox CLI + local iconSize = iconInfo.ImageRectSize or Vector2.new() local labelOffset = indent + arrowSize + 2 * padding - local textOffset = iconSize + 3 * padding + local textOffset = iconSize.X + 3 * padding return Roact.createElement(Container, { Size = UDim2.new(1, -indent, 0, style.RowHeight), @@ -105,10 +106,10 @@ function InstanceTreeRow:render() Size = UDim2.new(1, -arrowSize, 1, 0), }, { Icon = Roact.createElement("ImageLabel", { - Size = UDim2.fromOffset(iconSize, iconInfo.ImageRectSize.Y), + Size = UDim2.fromOffset(iconSize.X, iconSize.Y), BackgroundTransparency = 1, Image = iconInfo.Image, - ImageRectSize = iconInfo.ImageRectSize, + ImageRectSize = iconSize, ImageRectOffset = iconInfo.ImageRectOffset, Position = UDim2.new(0, padding, 0.5, 0), AnchorPoint = Vector2.new(0, 0.5) diff --git a/BuiltInPlugins/AssetManager/Packages/Framework/UI/LinkText/test.spec.lua b/BuiltInPlugins/AssetManager/Packages/Framework/UI/LinkText/test.spec.lua index d72d172041..37cecf30b1 100644 --- a/BuiltInPlugins/AssetManager/Packages/Framework/UI/LinkText/test.spec.lua +++ b/BuiltInPlugins/AssetManager/Packages/Framework/UI/LinkText/test.spec.lua @@ -19,7 +19,7 @@ return function() local mouse = Mouse.new({}) local theme if FlagsList:get("FFlagRefactorDevFrameworkTheme") then - theme = StudioTheme.new() + theme = StudioTheme.mock() else theme = Theme.new(function() return { diff --git a/BuiltInPlugins/AssetManager/Packages/Framework/UI/LoadingBar/test.spec.lua b/BuiltInPlugins/AssetManager/Packages/Framework/UI/LoadingBar/test.spec.lua index ea437b136c..c898cde0f4 100644 --- a/BuiltInPlugins/AssetManager/Packages/Framework/UI/LoadingBar/test.spec.lua +++ b/BuiltInPlugins/AssetManager/Packages/Framework/UI/LoadingBar/test.spec.lua @@ -18,7 +18,7 @@ return function() local function createTestLoadingBar(props, children) local theme if FlagsList:get("FFlagRefactorDevFrameworkTheme") then - theme = StudioTheme.new() + theme = StudioTheme.mock() else theme = Theme.new(function() return { diff --git a/BuiltInPlugins/AssetManager/Packages/Framework/UI/RadioButton/test.spec.lua b/BuiltInPlugins/AssetManager/Packages/Framework/UI/RadioButton/test.spec.lua index 31e2c12c77..4e578229a9 100644 --- a/BuiltInPlugins/AssetManager/Packages/Framework/UI/RadioButton/test.spec.lua +++ b/BuiltInPlugins/AssetManager/Packages/Framework/UI/RadioButton/test.spec.lua @@ -23,7 +23,7 @@ return function() local function createTestRadioButton(props, children) local theme if FlagsList:get("FFlagRefactorDevFrameworkTheme") then - theme = StudioTheme.new() + theme = StudioTheme.mock() else theme = Theme.new(function() return { diff --git a/BuiltInPlugins/AssetManager/Packages/Framework/UI/RoundBox/test.spec.lua b/BuiltInPlugins/AssetManager/Packages/Framework/UI/RoundBox/test.spec.lua index 10b16f79a0..d9715cf163 100644 --- a/BuiltInPlugins/AssetManager/Packages/Framework/UI/RoundBox/test.spec.lua +++ b/BuiltInPlugins/AssetManager/Packages/Framework/UI/RoundBox/test.spec.lua @@ -17,7 +17,7 @@ return function() local function createTestRoundBoxDecoration() local theme if FlagsList:get("FFlagRefactorDevFrameworkTheme") then - theme = StudioTheme.new() + theme = StudioTheme.mock() else theme = Theme.new(function() return { diff --git a/BuiltInPlugins/AssetManager/Packages/Framework/UI/TextLabel/test.spec.lua b/BuiltInPlugins/AssetManager/Packages/Framework/UI/TextLabel/test.spec.lua index a403d8b7d3..5f739cb55f 100644 --- a/BuiltInPlugins/AssetManager/Packages/Framework/UI/TextLabel/test.spec.lua +++ b/BuiltInPlugins/AssetManager/Packages/Framework/UI/TextLabel/test.spec.lua @@ -17,7 +17,7 @@ return function() local function createTestTextLabelDecoration() local theme if FlagsList:get("FFlagRefactorDevFrameworkTheme") then - theme = StudioTheme.new() + theme = StudioTheme.mock() else theme = Theme.new(function() return { diff --git a/BuiltInPlugins/AssetManager/Packages/Framework/UI/ToggleButton/test.spec.lua b/BuiltInPlugins/AssetManager/Packages/Framework/UI/ToggleButton/test.spec.lua index 0d712bdf45..c4db80d801 100644 --- a/BuiltInPlugins/AssetManager/Packages/Framework/UI/ToggleButton/test.spec.lua +++ b/BuiltInPlugins/AssetManager/Packages/Framework/UI/ToggleButton/test.spec.lua @@ -27,7 +27,7 @@ return function() local mouse = Mouse.new({}) local theme if FlagsList:get("FFlagRefactorDevFrameworkTheme") then - theme = StudioTheme.new() + theme = StudioTheme.mock() else theme = Theme.new(function() return { diff --git a/BuiltInPlugins/AssetManager/Packages/Framework/UI/Tooltip/test.spec.lua b/BuiltInPlugins/AssetManager/Packages/Framework/UI/Tooltip/test.spec.lua index 984329d7eb..8f7a0a6fc9 100644 --- a/BuiltInPlugins/AssetManager/Packages/Framework/UI/Tooltip/test.spec.lua +++ b/BuiltInPlugins/AssetManager/Packages/Framework/UI/Tooltip/test.spec.lua @@ -25,7 +25,7 @@ return function() local focus = Focus.new(target) local theme if FlagsList:get("FFlagRefactorDevFrameworkTheme") then - theme = StudioTheme.new() + theme = StudioTheme.mock() else theme = Theme.new(function() return { diff --git a/BuiltInPlugins/AssetManager/Packages/Framework/UI/TreeView/test.spec.lua b/BuiltInPlugins/AssetManager/Packages/Framework/UI/TreeView/test.spec.lua index 953aef3483..8d06ee8a86 100644 --- a/BuiltInPlugins/AssetManager/Packages/Framework/UI/TreeView/test.spec.lua +++ b/BuiltInPlugins/AssetManager/Packages/Framework/UI/TreeView/test.spec.lua @@ -63,7 +63,7 @@ return function() local function createTreeView() local theme if FlagsList:get("FFlagRefactorDevFrameworkTheme") then - theme = StudioTheme.new() + theme = StudioTheme.mock() else theme = Theme.new(function() return { diff --git a/BuiltInPlugins/AssetManager/Packages/Framework/Util.lua b/BuiltInPlugins/AssetManager/Packages/Framework/Util.lua index 5454c404f1..6a3e12c089 100644 --- a/BuiltInPlugins/AssetManager/Packages/Framework/Util.lua +++ b/BuiltInPlugins/AssetManager/Packages/Framework/Util.lua @@ -19,6 +19,7 @@ return strict({ FitFrame = require(script.FitFrame), Flags = require(script.Flags), + getTestVariation = require(script.getTestVariation), Immutable = require(script.Immutable), LayoutOrderIterator = require(script.LayoutOrderIterator), Math = require(script.Math), diff --git a/BuiltInPlugins/AssetManager/Packages/Framework/Util/CrossPluginCommunication.lua b/BuiltInPlugins/AssetManager/Packages/Framework/Util/CrossPluginCommunication.lua index 37caa1bece..d3b82f5783 100644 --- a/BuiltInPlugins/AssetManager/Packages/Framework/Util/CrossPluginCommunication.lua +++ b/BuiltInPlugins/AssetManager/Packages/Framework/Util/CrossPluginCommunication.lua @@ -22,10 +22,12 @@ CrossPluginCommunication.__index = CrossPluginCommunication CrossPluginCommunication.BASE_FOLDER_NAME = "CrossPluginCommunication" -function CrossPluginCommunication.new(pluginNamespace) +function CrossPluginCommunication.new(pluginNamespace, hostService) assert(t.string(pluginNamespace), "pluginNamespace must be a string") + assert(t.optional(t.instance(pluginNamespace)), "hostService must be an instance if defined") local self = { + hostService = hostService or RobloxPluginGuiService, pluginNamespace = pluginNamespace } @@ -49,7 +51,7 @@ function CrossPluginCommunication:ensureFolderExists(parent, name) end function CrossPluginCommunication:getNamespaceFolder() - local base = self:ensureFolderExists(RobloxPluginGuiService, self.BASE_FOLDER_NAME) + local base = self:ensureFolderExists(self.hostService, self.BASE_FOLDER_NAME) return self:ensureFolderExists(base, self.pluginNamespace) end diff --git a/BuiltInPlugins/AssetManager/Packages/Framework/Util/CrossPluginCommunication.spec.lua b/BuiltInPlugins/AssetManager/Packages/Framework/Util/CrossPluginCommunication.spec.lua index 60338f2b0c..f3c1035f50 100644 --- a/BuiltInPlugins/AssetManager/Packages/Framework/Util/CrossPluginCommunication.spec.lua +++ b/BuiltInPlugins/AssetManager/Packages/Framework/Util/CrossPluginCommunication.spec.lua @@ -1,5 +1,3 @@ -local RobloxPluginGuiService = game:GetService("RobloxPluginGuiService") - local Framework = script.Parent.Parent local CrossPluginCommunication = require(Framework.Util.CrossPluginCommunication) @@ -8,8 +6,10 @@ return function() -- TODO DEVTOOLS-4397: Move cleanup to afterEach hook after the dependency structure is fixed + local hostService = workspace + local function createTestComms() - return CrossPluginCommunication.new(TEST_NAMESPACE) + return CrossPluginCommunication.new(TEST_NAMESPACE, hostService) end describe("Invoke", function() @@ -32,7 +32,7 @@ return function() comms:OnInvoke("blah", function() end) - local namespace = RobloxPluginGuiService:FindFirstChild(CrossPluginCommunication.BASE_FOLDER_NAME):FindFirstChild(TEST_NAMESPACE) + local namespace = hostService:FindFirstChild(CrossPluginCommunication.BASE_FOLDER_NAME):FindFirstChild(TEST_NAMESPACE) assert(namespace) diff --git a/BuiltInPlugins/AssetManager/Packages/Framework/Util/Palette.spec.lua b/BuiltInPlugins/AssetManager/Packages/Framework/Util/Palette.spec.lua index 8794c80717..02911b17c7 100644 --- a/BuiltInPlugins/AssetManager/Packages/Framework/Util/Palette.spec.lua +++ b/BuiltInPlugins/AssetManager/Packages/Framework/Util/Palette.spec.lua @@ -1,5 +1,16 @@ return function() - local Palette = require(script.Parent.Palette) + + local Util = script.Parent + local Flags = require(Util.Flags) + local FlagsList = Flags.new({ + FFlagRefactorDevFrameworkTheme = {"RefactorDevFrameworkTheme"}, + }) + + if FlagsList:get("FFlagRefactorDevFrameworkTheme") then + return + end + + local Palette = require(Util.Palette) it("should be a table of color StyleValues", function() expect(Palette).to.be.ok() diff --git a/BuiltInPlugins/AssetManager/Packages/Framework/Util/Typecheck/t.lua b/BuiltInPlugins/AssetManager/Packages/Framework/Util/Typecheck/t.lua index 44c8d50af8..045d7ac0dd 100644 --- a/BuiltInPlugins/AssetManager/Packages/Framework/Util/Typecheck/t.lua +++ b/BuiltInPlugins/AssetManager/Packages/Framework/Util/Typecheck/t.lua @@ -1,3 +1,5 @@ +--!nocheck + --[[ DeveloperFramework uses t from Osyris https://github.com/osyrisrblx/t diff --git a/BuiltInPlugins/AssetManager/Packages/Framework/Util/getTestVariation.lua b/BuiltInPlugins/AssetManager/Packages/Framework/Util/getTestVariation.lua new file mode 100644 index 0000000000..d09873db1d --- /dev/null +++ b/BuiltInPlugins/AssetManager/Packages/Framework/Util/getTestVariation.lua @@ -0,0 +1,30 @@ +-- This function hides ABTestService away and provides a quick interface for getting a test's variation. +-- Should the requested test not exist, it will return the Control group. + +local ABTestService = game:GetService("ABTestService") + +local AB_TEST_GROUP_CONTROL = "Control" + +-- abTestName : (string) the name of test. ex) "AllUsers.Studio.ExampleTestName" +-- abTestService : (table, optional) an optional override for ABTestService +-- RETURNS : (int) the variation that the user is in. 0 = Control +return function(abTestName, abTestService) + assert(type(abTestName) == "string", "Expected abTestName to be a string") + if abTestService then + assert(type(abTestService.GetVariant) == "function", "Expected the abTestService object to have a GetVariant member function") + else + abTestService = ABTestService + end + + local variation = abTestService:GetVariant(abTestName) + if variation == AB_TEST_GROUP_CONTROL then + return 0 + else + local _, _, variationNumber = string.find(variation, "Variation(%d+)") + if not variationNumber then + return 0 + end + + return tonumber(variationNumber) + end +end diff --git a/BuiltInPlugins/AssetManager/Packages/Framework/Util/getTestVariation.spec.lua b/BuiltInPlugins/AssetManager/Packages/Framework/Util/getTestVariation.spec.lua new file mode 100644 index 0000000000..5febceee91 --- /dev/null +++ b/BuiltInPlugins/AssetManager/Packages/Framework/Util/getTestVariation.spec.lua @@ -0,0 +1,41 @@ +return function() + local getTestVariation = require(script.Parent.getTestVariation) + local VARIATION_NAME_CONTROL = "Control" + + local function createTestService(result) + local DebugABService = {} + + function DebugABService:GetVariant(testName) + return result + end + + return DebugABService + end + + it("should expect some test name", function() + expect(function() + getTestVariation() + end).to.throw() + end) + + it("should return zero when in the control group", function() + local debugABTestService = createTestService(VARIATION_NAME_CONTROL) + local variation = getTestVariation("AllUsers.Studio.ShouldShowZero", debugABTestService) + expect(variation).to.equal(0) + end) + + it("should return the number of the variation when not in the control group", function() + local debugABTestService = createTestService("Variation1") + local variation = getTestVariation("AllUsers.Studio.ShouldShowNumber", debugABTestService) + expect(variation).to.equal(1) + + -- there aren't any variations past 8, but it should work with any number + debugABTestService = createTestService("Variation23") + variation = getTestVariation("AllUsers.Studio.ShouldShowNumber", debugABTestService) + expect(variation).to.equal(23) + + debugABTestService = createTestService("Variation456") + variation = getTestVariation("AllUsers.Studio.ShouldShowNumber", debugABTestService) + expect(variation).to.equal(456) + end) +end \ No newline at end of file diff --git a/BuiltInPlugins/AssetManager/Packages/UILibrary/_internal/Utils/Symbol.spec.lua b/BuiltInPlugins/AssetManager/Packages/UILibrary/_internal/Utils/Symbol.spec.lua index cde9be065b..f3312055c9 100644 --- a/BuiltInPlugins/AssetManager/Packages/UILibrary/_internal/Utils/Symbol.spec.lua +++ b/BuiltInPlugins/AssetManager/Packages/UILibrary/_internal/Utils/Symbol.spec.lua @@ -11,7 +11,7 @@ return function() it("should coerce to the given name", function() local symbol = Symbol.named("foo") - expect(tostring(symbol):find("foo")).to.be.ok() + expect(tostring(symbol):match("foo")).to.be.ok() end) it("should be unique when constructed", function() @@ -42,4 +42,4 @@ return function() expect(symbolA).never.to.equal(symbolB) end) end) -end \ No newline at end of file +end diff --git a/BuiltInPlugins/AssetManager/Src/Components/RecentlyImportedView.lua b/BuiltInPlugins/AssetManager/Src/Components/RecentlyImportedView.lua index 73098a22e8..a7b05a314b 100644 --- a/BuiltInPlugins/AssetManager/Src/Components/RecentlyImportedView.lua +++ b/BuiltInPlugins/AssetManager/Src/Components/RecentlyImportedView.lua @@ -78,6 +78,7 @@ function RecentlyImportedView:render() return Roact.createElement("Frame", { Size = size, LayoutOrder = layoutOrder, + ZIndex = 2, BackgroundTransparency = 1, }, { @@ -91,6 +92,7 @@ function RecentlyImportedView:render() RecentlyImportedViewBar = Roact.createElement("Frame", { Size = UDim2.new(1, 0, 0, recentViewTheme.Bar.Height), LayoutOrder = layoutIndex:getNextOrder(), + ZIndex = 100, BackgroundColor3 = recentViewTheme.Bar.BackgroundColor, BorderColor3 = theme.BorderColor, @@ -115,7 +117,7 @@ function RecentlyImportedView:render() Text = recentlyImportedViewText, TextColor3 = theme.TextColor, - TextSize = theme.FontSizeMedium, + TextSize = theme.FontSizeSmall, Font = theme.Font, TextXAlignment = Enum.TextXAlignment.Left, }), diff --git a/BuiltInPlugins/AssetManager/Src/Components/TopBar.lua b/BuiltInPlugins/AssetManager/Src/Components/TopBar.lua index 6f417b0763..757b89cbf5 100644 --- a/BuiltInPlugins/AssetManager/Src/Components/TopBar.lua +++ b/BuiltInPlugins/AssetManager/Src/Components/TopBar.lua @@ -28,6 +28,7 @@ local LayoutOrderIterator = UILibrary.Util.LayoutOrderIterator local StyledTooltip = UILibrary.Component.StyledTooltip local GetTextSize = UILibrary.Util.GetTextSize +local SetRecentViewToggled = require(Plugin.Src.Actions.SetRecentViewToggled) local SetSearchTerm = require(Plugin.Src.Actions.SetSearchTerm) local SetToPreviousScreen = require(Plugin.Src.Actions.SetToPreviousScreen) local SetToNextScreen = require(Plugin.Src.Actions.SetToNextScreen) @@ -39,6 +40,7 @@ local Screens = require(Plugin.Src.Util.Screens) local BulkImportService = game:GetService("BulkImportService") local FFlagAssetManagerAddAnalytics = game:GetFastFlag("AssetManagerAddAnalytics") +local FFlagStudioAssetManagerAddRecentlyImportedView = game:GetFastFlag("StudioAssetManagerAddRecentlyImportedView") local TopBar = Roact.PureComponent:extend("TopBar") @@ -69,6 +71,9 @@ function TopBar:render() local enabled = props.Enabled + local recentViewToggled = props.RecentViewToggled + local dispatchSetRecentViewToggled = props.dispatchSetRecentViewToggled + local currentScreen = props.CurrentScreen local previousScreens = props.PreviousScreens local nextScreens = props.NextScreens @@ -156,6 +161,9 @@ function TopBar:render() OnClick = function() if previousButtonEnabled and enabled then dispatchSetToPreviousScreen(previousButtonEnabled) + if FFlagStudioAssetManagerAddRecentlyImportedView and recentViewToggled then + dispatchSetRecentViewToggled(false) + end end end, }, { @@ -177,6 +185,9 @@ function TopBar:render() OnClick = function() if nextButtonEnabled and enabled then dispatchSetToNextScreen(nextButtonEnabled) + if FFlagStudioAssetManagerAddRecentlyImportedView and recentViewToggled then + dispatchSetRecentViewToggled(false) + end end end, }, { @@ -286,6 +297,7 @@ local function mapStateToProps(state, props) CurrentScreen = state.Screen.currentScreen, PreviousScreens = previousScreens, NextScreens = nextScreens, + RecentViewToggled = state.AssetManagerReducer.recentViewToggled, } end @@ -294,6 +306,9 @@ local function mapDispatchToProps(dispatch) dispatchLaunchBulkImporter = function(assetType) dispatch(LaunchBulkImport(assetType)) end, + dispatchSetRecentViewToggled = function(toggled) + dispatch(SetRecentViewToggled(toggled)) + end, dispatchSetSearchTerm = function(searchTerm) dispatch(SetSearchTerm(searchTerm)) end, diff --git a/BuiltInPlugins/AssetManager/Src/Resources/PluginTheme.lua b/BuiltInPlugins/AssetManager/Src/Resources/PluginTheme.lua index 8fd021fe9f..5a66ba99bd 100644 --- a/BuiltInPlugins/AssetManager/Src/Resources/PluginTheme.lua +++ b/BuiltInPlugins/AssetManager/Src/Resources/PluginTheme.lua @@ -436,7 +436,7 @@ local function createStyles(theme, getColor) ItemPadding = UDim.new(0, 6), Bar = { BackgroundColor = theme:GetColor(c.Titlebar), - Height = 38, + Height = 24, Padding = 10, Button = { diff --git a/BuiltInPlugins/ConvertToPackage/Packages/UILibrary.lua b/BuiltInPlugins/ConvertToPackage/Packages/UILibrary.lua index 7769f987a4..08a7c2267b 100644 --- a/BuiltInPlugins/ConvertToPackage/Packages/UILibrary.lua +++ b/BuiltInPlugins/ConvertToPackage/Packages/UILibrary.lua @@ -4,7 +4,7 @@ local FFlagEnableToolboxVideos = game:GetFastFlag("EnableToolboxVideos") -local Src = script +local Src = script._internal local Components = Src.Components local Utils = Src.Utils @@ -24,8 +24,6 @@ local Favorites = require(Components.Preview.Favorites) local ImagePreview = require(Components.Preview.ImagePreview) local AudioPreview = require(Components.Preview.AudioPreview) local AudioControl = FFlagEnableToolboxVideos and nil or require(Components.Preview.AudioControl) --- TODO FFlagRemoveUILibraryTimeline remove import -local Keyframe = require(Components.Timeline.Keyframe) local InfiniteScrollingFrame = require(Components.InfiniteScrollingFrame) local LoadingBar = require(Components.LoadingBar) local LoadingIndicator = require(Components.LoadingIndicator) @@ -35,8 +33,6 @@ local RadioButtons = require(Components.RadioButtons) local RoundFrame = require(Components.RoundFrame) local RoundTextBox = require(Components.RoundTextBox) local RoundTextButton = require(Components.RoundTextButton) --- TODO FFlagRemoveUILibraryTimeline remove import -local Scrubber = require(Components.Timeline.Scrubber) local SearchBar = require(Components.SearchBar) local Separator = require(Components.Separator) local StyledDialog = require(Components.StyledDialog) @@ -69,9 +65,6 @@ local Signal = require(Utils.Signal) local Dialog = require(Components.PluginWidget.Dialog) -game:DefineFastFlag("RemoveUILibraryTimeline", false) -local FFlagRemoveUILibraryTimeline = game:GetFastFlag("RemoveUILibraryTimeline") - local function createStrictTable(t) return setmetatable(t, { __index = function(_, index) @@ -100,7 +93,6 @@ local UILibrary = createStrictTable({ AudioPreview = AudioPreview, AudioControl = AudioControl, InfiniteScrollingFrame = InfiniteScrollingFrame, - Keyframe = (not FFlagRemoveUILibraryTimeline) and Keyframe or nil, LoadingBar = LoadingBar, LoadingIndicator = LoadingIndicator, ModelPreview = ModelPreview, @@ -109,7 +101,6 @@ local UILibrary = createStrictTable({ RoundFrame = RoundFrame, RoundTextBox = RoundTextBox, RoundTextButton = RoundTextButton, - Scrubber = (not FFlagRemoveUILibraryTimeline) and Scrubber or nil, SearchBar = SearchBar, Separator = Separator, StyledDialog = StyledDialog, @@ -164,14 +155,4 @@ local UILibrary = createStrictTable({ createTheme = require(Src.createTheme), }) -local virtualFolder = Instance.new("Folder") -virtualFolder.Name = "UILibraryInternals-Do-Not-Access-Directly" --- The number of parents to the plugin cannot change since UILibrary components reach out of UILibrary --- to get the plugin's copy of Roact -virtualFolder.Parent = script.Parent - -for _,v in pairs(script:GetChildren()) do - v.Parent = virtualFolder -end - return UILibrary \ No newline at end of file diff --git a/BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Camera.lua b/BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Camera.lua new file mode 100644 index 0000000000..2075f7aba5 --- /dev/null +++ b/BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Camera.lua @@ -0,0 +1,29 @@ +local Library = script.Parent + +local Roact = require(Library.Parent.Parent.Roact) +local Symbol = require(Library.Utils.Symbol) +local cameraKey = Symbol.named("MarkeplaceCamera") + +local CameraProvider = Roact.PureComponent:extend("CameraProvider") + +function CameraProvider:init(prop) + local camera = Instance.new("Camera") + camera.Name = "MarketplaceCamera" + + self._context[cameraKey] = camera +end + +function CameraProvider:render() + return Roact.oneChild(self.props[Roact.Children]) +end + +local function getCamera(component) + assert(component._context[cameraKey] ~= nil, "No CameraProvider Found") + local camera = component._context[cameraKey] + return camera +end + +return { + Provider = CameraProvider, + getCamera = getCamera, +} \ No newline at end of file diff --git a/BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Components/BaseDialog.lua b/BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Components/BaseDialog.lua new file mode 100644 index 0000000000..edb28acf30 --- /dev/null +++ b/BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Components/BaseDialog.lua @@ -0,0 +1,128 @@ +--[[ + A basic dialog with content and a set of buttons. + Other dialogs can use this component to provide more specific implementations. + While this component allows the creation of any arbitrary buttons, in most + cases a StyledDialog is preferred if the normal UILibrary buttons are desired. + + Required Props: + array Buttons = An array of items used to render + the buttons for this dialog. + function RenderButton(button, index, activated) = A function + used to render a button. This function is called for each + item in the Buttons array. It should return a Roact component + that connects a signal to the activated parameter. + + Props: + Vector2 Size = The starting size of the dialog. + Vector2 MinSize = The minimum size of the dialog, if it is resizable. + bool Resizable = Whether the dialog can be resized. + int BorderPadding = The padding to add around the edges of the dialog. + int ButtonPadding = The padding to add between buttons. + int ButtonHeight = The height of the buttons in the dialog, in pixels. + string Title = The title to display at the top of the window. + + function OnClose = A callback for when the user closed the dialog by + clicking the X in the corner of the window. + function OnButtonClicked(button) = A callback for when the user clicked + a button in the dialog. Returns the button that was clicked. +]] + +local Library = script.Parent.Parent +local Roact = require(Library.Parent.Parent.Roact) + +local Theming = require(Library.Theming) +local withTheme = Theming.withTheme + +local Dialog = require(Library.Components.PluginWidget.Dialog) + +local BaseDialog = Roact.PureComponent:extend("BaseDialog") + +function BaseDialog:init() + self.buttonClicked = function(button) + if self.props.OnButtonClicked then + self.props.OnButtonClicked(button) + end + end +end + +function BaseDialog:render() + return withTheme(function(theme) + local props = self.props + + local title = props.Title + local size = props.Size + local minSize = props.MinSize + local resizable = props.Resizable + local borderPadding = props.BorderPadding or 0 + + local buttons = props.Buttons + local buttonPadding = props.ButtonPadding or 0 + local buttonHeight = props.ButtonHeight or 0 + local renderButton = props.RenderButton + + assert(buttons ~= nil and type(buttons) == "table", + "BaseDialog requires a Buttons table.") + assert(renderButton ~= nil and type(renderButton) == "function", + "BaseDialog requires a RenderButton function.") + assert(buttonHeight ~= nil and type(buttonHeight) == "number", + "BaseDialog requires a ButtonHeight value.") + + local buttonComponents = { + Layout = Roact.createElement("UIListLayout", { + SortOrder = Enum.SortOrder.LayoutOrder, + FillDirection = Enum.FillDirection.Horizontal, + HorizontalAlignment = Enum.HorizontalAlignment.Center, + Padding = UDim.new(0, buttonPadding), + }), + } + + for index, button in ipairs(buttons) do + table.insert(buttonComponents, renderButton(button, index, function() + self.buttonClicked(button) + end)) + end + + return Roact.createElement(Dialog, { + Options = { + Size = size, + Resizable = resizable, + MinSize = minSize, + Modal = true, + InitialEnabled = true, + }, + Title = title, + OnClose = props.OnClose, + }, { + Background = Roact.createElement("Frame", { + Size = UDim2.new(1, 0, 1, 0), + BorderSizePixel = 0, + BackgroundColor3 = theme.dialog.background, + }, { + Padding = Roact.createElement("UIPadding", { + PaddingTop = UDim.new(0, borderPadding), + PaddingBottom = UDim.new(0, borderPadding), + PaddingLeft = UDim.new(0, borderPadding), + PaddingRight = UDim.new(0, borderPadding), + }), + + Content = Roact.createElement("Frame", { + LayoutOrder = 1, + Size = UDim2.new(1, 0, 1, -(buttonHeight + borderPadding)), + AnchorPoint = Vector2.new(0.5, 0), + Position = UDim2.new(0.5, 0, 0, 0), + BackgroundTransparency = 1, + }, self.props[Roact.Children]), + + Buttons = Roact.createElement("Frame", { + LayoutOrder = 2, + Size = UDim2.new(1, 0, 0, buttonHeight), + AnchorPoint = Vector2.new(0.5, 1), + Position = UDim2.new(0.5, 0, 1, 0), + BackgroundTransparency = 1, + }, buttonComponents), + }) + }) + end) +end + +return BaseDialog diff --git a/BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Components/BaseDialog.spec.lua b/BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Components/BaseDialog.spec.lua new file mode 100644 index 0000000000..ff5f454355 --- /dev/null +++ b/BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Components/BaseDialog.spec.lua @@ -0,0 +1,121 @@ +return function() + local Library = script.Parent.Parent + local Roact = require(Library.Parent.Parent.Roact) + + local MockWrapper = require(Library.MockWrapper) + + local BaseDialog = require(script.Parent.BaseDialog) + + local function createTestBaseDialog(props, children, container) + return Roact.createElement(MockWrapper, { + Container = container, + }, { + BaseDialog = Roact.createElement(BaseDialog, props, children) + }) + end + + it("should create and destroy without errors", function() + local element = createTestBaseDialog({ + Buttons = {}, + RenderButton = function(item) + end, + }) + local instance = Roact.mount(element) + Roact.unmount(instance) + end) + + it("should render correctly", function() + local container = Instance.new("Folder") + + local element = createTestBaseDialog({ + Buttons = {}, + RenderButton = function(item) + end, + }, {}, container) + + local instance = Roact.mount(element, container) + + local gui = container:FindFirstChildOfClass("BillboardGui") + expect(gui).to.be.ok() + expect(gui.FocusProvider).to.be.ok() + expect(gui.FocusProvider.Padding).to.be.ok() + expect(gui.FocusProvider.Content).to.be.ok() + expect(gui.FocusProvider.Buttons).to.be.ok() + + Roact.unmount(instance) + end) + + it("should require a Buttons table", function() + local element = createTestBaseDialog({ + RenderButton = function(item) + end, + }) + expect(function() + Roact.mount(element) + end).to.throw() + + element = createTestBaseDialog({ + Buttons = true, + RenderButton = function(item) + end, + }) + expect(function() + Roact.mount(element) + end).to.throw() + end) + + it("should require a RenderButtons function", function() + local element = createTestBaseDialog({ + Buttons = {}, + }) + expect(function() + Roact.mount(element) + end).to.throw() + + element = createTestBaseDialog({ + Buttons = {}, + RenderButton = true, + }) + expect(function() + Roact.mount(element) + end).to.throw() + end) + + it("should render its buttons", function() + local container = Instance.new("Folder") + + local element = createTestBaseDialog({ + Buttons = {"Frame"}, + RenderButton = function() + return Roact.createElement("Frame") + end, + }, {}, container) + + local instance = Roact.mount(element, container) + + local gui = container:FindFirstChildOfClass("BillboardGui") + local buttonContainer = gui.FocusProvider.Buttons + expect(buttonContainer["1"]).to.be.ok() + + Roact.unmount(instance) + end) + + it("should render its children", function() + local container = Instance.new("Folder") + + local element = createTestBaseDialog({ + Buttons = {}, + RenderButton = function() + end, + }, { + Frame = Roact.createElement("Frame"), + }, container) + + local instance = Roact.mount(element, container) + + local gui = container:FindFirstChildOfClass("BillboardGui") + expect(gui.FocusProvider.Content.Frame).to.be.ok() + + Roact.unmount(instance) + end) +end \ No newline at end of file diff --git a/BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Components/BulletPoint.lua b/BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Components/BulletPoint.lua new file mode 100644 index 0000000000..8721cc0e00 --- /dev/null +++ b/BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Components/BulletPoint.lua @@ -0,0 +1,93 @@ +--[[ + A line of text prefaced with a bullet point. Useful for lists of entries. + + Props: + string Text = The text to display after the bullet point + int LayoutOrder = Order in which the element is placed + int TextSize = The size of text + bool TextWrapped = Sets text wrapped + bool TextTruncate = Sets text truncate +]] + +local TextService = game:GetService("TextService") + +local Library = script.Parent.Parent +local Roact = require(Library.Parent.Parent.Roact) + +local Theming = require(Library.Theming) +local withTheme = Theming.withTheme + +local BulletPoint = Roact.PureComponent:extend("BulletPoint") + +local TEXT_SIZE = 20 + +function BulletPoint:init() + self.frameRef = Roact.createRef() + self.textConnection = nil + + self.updateFrameSize = function() + local frame = self.frameRef.current + local sizeX = frame.AbsoluteSize.x + local textSize = TextService:GetTextSize( + self.props.Text, + self.props.TextSize or TEXT_SIZE, + Enum.Font.SourceSans, + Vector2.new(sizeX, math.huge) + ) + frame.Size = UDim2.new(1, 0, 0, textSize.y) + end +end + +function BulletPoint:didMount() + local frame = self.frameRef.current + self.textConnection = frame:GetPropertyChangedSignal("AbsoluteSize"):connect(self.updateFrameSize) + self.updateFrameSize() +end + +function BulletPoint:willUnmount() + self.textConnection:Disconnect() + self.textConnection = nil +end + +function BulletPoint:render() + return withTheme(function(theme) + + local textSize = self.props.TextSize or TEXT_SIZE + local text = self.props.Text or "" + + return Roact.createElement("Frame", { + BackgroundTransparency = 1, + LayoutOrder = self.props.LayoutOrder or 1, + Size = UDim2.new(1, 0, 0, 0), + + [Roact.Ref] = self.frameRef, + }, { + Text = Roact.createElement("TextLabel", { + BackgroundTransparency = 1, + Position = UDim2.new(0, 16, 0, -1), + Size = UDim2.new(1, -16, 1, 0), + Text = text, + Font = theme.bulletPoint.font, + TextColor3 = theme.bulletPoint.text, + TextXAlignment = Enum.TextXAlignment.Left, + TextYAlignment = Enum.TextYAlignment.Top, + TextWrapped = self.props.TextWrapped or nil, + TextSize = textSize, + TextTruncate = self.props.TextTruncate or nil, + }), + + Dot = Roact.createElement("TextLabel", { + BackgroundTransparency = 1, + Size = UDim2.new(0, 4, 0, 4), + AnchorPoint = Vector2.new(0, 0.5), + TextColor3 = theme.bulletPoint.text, + Text = "•", + TextYAlignment = Enum.TextYAlignment.Top, + Font = theme.bulletPoint.font, + TextSize = textSize, + }), + }) + end) +end + +return BulletPoint diff --git a/BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Components/BulletPoint.spec.lua b/BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Components/BulletPoint.spec.lua new file mode 100644 index 0000000000..f2ad833346 --- /dev/null +++ b/BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Components/BulletPoint.spec.lua @@ -0,0 +1,36 @@ +return function() + local Library = script.Parent.Parent + local Roact = require(Library.Parent.Parent.Roact) + + local MockWrapper = require(Library.MockWrapper) + + local BulletPoint = require(script.Parent.BulletPoint) + + local function createTestBulletPoint(container) + return Roact.createElement(MockWrapper, { + Container = container, + }, { + BulletPoint = Roact.createElement(BulletPoint, { + Text = "test", + TextSize = 20, + }), + }) + end + + it("should create and destroy without errors", function() + local element = createTestBulletPoint() + local instance = Roact.mount(element) + Roact.unmount(instance) + end) + + it("should render correctly", function() + local container = Instance.new("Folder") + local instance = Roact.mount(createTestBulletPoint(container), container) + local bulletPoint = container:FindFirstChildOfClass("Frame") + + expect(bulletPoint.Text).to.be.ok() + expect(bulletPoint.Dot).to.be.ok() + + Roact.unmount(instance) + end) +end \ No newline at end of file diff --git a/BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Components/Button.lua b/BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Components/Button.lua new file mode 100644 index 0000000000..65825419c4 --- /dev/null +++ b/BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Components/Button.lua @@ -0,0 +1,142 @@ +--!nolint UnknownGlobal +--^ DEVTOOLS-4930 + +--[[ + A button with rounded corners. Colors are based on Theme. + + Required Props: + function RenderContents(theme, hovered) = A function that returns the + contents that will display in the button. The parameters passed + allow the function to style the contents based on the button's current + theme and/or produce different contents if the button is hovered. + + Props: + string Style = The theme to use for this button. Ex. "Default", "Primary". + Styles for buttons can be found in createTheme.lua. + string StyleState = Normally controlled by the button (e.g. hovered), but can + be overwritten with something like 'disabled' to pull from override themes + + UDim2 Size = The size of the button. + UDim2 Position = The position of the button. + Vector2 AnchorPoint = The center point of the button. + int LayoutOrder = The order in which this button appears in a UILayout. + int ZIndex = The display index of this button. + int BorderSizePixel = Border size of the button +]] + +local Library = script.Parent.Parent + +local Roact = require(Library.Parent.Parent.Roact) + +local Theming = require(Library.Theming) +local withTheme = Theming.withTheme +local join = require(Library.join) + +local RoundFrame = require(Library.Components.RoundFrame) + +local Button = Roact.PureComponent:extend("Button") + +function Button:init(initialProps) + self.state = { + hovered = false, + pressed = false + } + + self.onClick = function() + if self.props.OnClick then + self.props.OnClick() + end + end + + self.mouseEnter = function() + self:setState({ + hovered = true, + }) + end + + self.mouseLeave = function() + self:setState({ + hovered = false, + pressed = false + }) + end + + self.onMouseDown = function() + self:setState({ + hovered = true, + pressed = true, + }) + end + + self.onMouseUp = function() + self:setState({ + pressed = false, + }) + end +end + +function Button:render() + return withTheme(function(theme) + local props = self.props + local state = self.state + local hovered = state.hovered + local style = props.Style + local styleState = props.StyleState + local size = props.Size + local position = props.Position + local anchorPoint = props.AnchorPoint + local layoutOrder = props.LayoutOrder + local renderContents = props.RenderContents + local zIndex = props.ZIndex + local borderSize = props.BorderSizePixel + + assert(renderContents ~= nil and type(renderContents) == "function", + "Button requires a RenderContents function.") + + local buttonTheme = style and theme.button[style] or theme.button.Default + if styleState then + buttonTheme = join(buttonTheme, buttonTheme[styleState]) + elseif pressed then + buttonTheme = join(buttonTheme, buttonTheme.pressed) + elseif hovered then + buttonTheme = join(buttonTheme, buttonTheme.hovered) + end + + local isRound = buttonTheme.isRound + local content = renderContents(buttonTheme, hovered, pressed) + + local buttonProps = { + Size = size, + Position = position, + AnchorPoint = anchorPoint, + LayoutOrder = layoutOrder, + ZIndex = zIndex, + + BackgroundColor3 = buttonTheme.backgroundColor, + BorderColor3 = buttonTheme.borderColor, + BorderSizePixel = borderSize, + } + + if isRound then + return Roact.createElement(RoundFrame, join(buttonProps, { + OnActivated = self.onClick, + OnMouseEnter = self.mouseEnter, + OnMouseLeave = self.mouseLeave, + [Roact.Event.MouseButton1Down] = self.onMouseDown, + [Roact.Event.MouseButton1Up] = self.onMouseUp, + }), content) + else + return Roact.createElement("ImageButton", join(buttonProps, { + AutoButtonColor = false, + + [Roact.Event.MouseEnter] = self.mouseEnter, + [Roact.Event.MouseLeave] = self.mouseLeave, + [Roact.Event.Activated] = self.onClick, + [Roact.Event.MouseButton1Down] = self.onMouseDown, + [Roact.Event.MouseButton1Up] = self.onMouseUp, + }), content) + end + end) +end + +return Button diff --git a/BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Components/Button.spec.lua b/BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Components/Button.spec.lua new file mode 100644 index 0000000000..3a47c1896a --- /dev/null +++ b/BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Components/Button.spec.lua @@ -0,0 +1,79 @@ +return function() + local Library = script.Parent.Parent + local Roact = require(Library.Parent.Parent.Roact) + + local MockWrapper = require(Library.MockWrapper) + + local Button = require(script.Parent.Button) + + local function createTestButton(props, container) + return Roact.createElement(MockWrapper, { + Container = container, + }, { + Button = Roact.createElement(Button, props) + }) + end + + it("should create and destroy without errors", function() + local element = createTestButton({ + RenderContents = function() + end, + }) + local instance = Roact.mount(element) + Roact.unmount(instance) + end) + + it("should render correctly", function() + local container = Instance.new("Folder") + + local element = createTestButton({ + RenderContents = function() + end, + }, container) + + local instance = Roact.mount(element, container) + + local button = container:FindFirstChildOfClass("ImageButton") + expect(button).to.be.ok() + expect(button.Border).to.be.ok() + + Roact.unmount(instance) + end) + + it("should require a RenderContents function", function() + local element = createTestButton() + expect(function() + Roact.mount(element) + end).to.throw() + + element = createTestButton({ + RenderContents = true, + }) + expect(function() + Roact.mount(element) + end).to.throw() + end) + + it("should render the children in RenderContents", function() + local container = Instance.new("Folder") + + local element = createTestButton({ + RenderContents = function() + return { + SomeFrame = Roact.createElement("Frame"), + OtherFrame = Roact.createElement("Frame"), + } + end, + }, container) + + local instance = Roact.mount(element, container) + + local button = container:FindFirstChildOfClass("ImageButton") + expect(button).to.be.ok() + expect(button.Border).to.be.ok() + expect(button.Border.SomeFrame).to.be.ok() + expect(button.Border.OtherFrame).to.be.ok() + + Roact.unmount(instance) + end) +end \ No newline at end of file diff --git a/BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Components/CheckBox.lua b/BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Components/CheckBox.lua new file mode 100644 index 0000000000..8c8bf6f16e --- /dev/null +++ b/BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Components/CheckBox.lua @@ -0,0 +1,96 @@ +--[[ + Clickable checkbox, from a CheckBoxSet. + + Props: + string Id = Unique identifier of this CheckBox + string Title = Text to display on this CheckBox + bool Selected = Whether to display this CheckBox as selected + bool Enabled = Whether this CheckBox accepts input + int Height = How big the CheckBox should be + int TextSize = How big the CheckBox's text should be + func OnActivated = What happens when the CheckBox is clicked + int titlePadding = How many pixels to the right of the icon the title is put +]] + +local TextService = game:GetService("TextService") + +local Library = script.Parent.Parent +local Roact = require(Library.Parent.Parent.Roact) +local Theming = require(Library.Theming) +local withTheme = Theming.withTheme + +local CheckBox = Roact.PureComponent:extend("CheckBox") + +function CheckBox:init() + self.onActivated = function() + if self.props.Enabled then + self.props.OnActivated() + end + end +end + +function CheckBox:render() + return withTheme(function(theme) + local props = self.props + + local title = props.Title + local height = props.Height + local enabled = props.Enabled + local layoutOrder = props.LayoutOrder + local selected = props.Selected + local textSize = props.TextSize + local titlePadding = props.TitlePadding or 5 + + local titleSize = TextService:GetTextSize( + title, + textSize, + theme.checkBox.font, + Vector2.new() + ) + local titleWidth = titleSize.X + + return Roact.createElement("Frame", { + Size = UDim2.new(1, 0, 0, height), + BackgroundTransparency = 1, + BorderSizePixel = 0, + LayoutOrder = layoutOrder or 1, + }, { + Background = Roact.createElement("ImageButton", { + Size = UDim2.new(0, height, 0, height), + BackgroundTransparency = 1, + ImageTransparency = enabled and 0 or 0.4, + Image = theme.checkBox.backgroundImage, + ImageColor3 = theme.checkBox.backgroundColor, + + [Roact.Event.Activated] = self.onActivated, + }, { + Selection = Roact.createElement("ImageLabel", { + BackgroundTransparency = 1, + Size = UDim2.new(1, 0, 1, 0), + Visible = enabled and selected, + Image = theme.checkBox.selectedImage, + }), + + TitleLabel = Roact.createElement("TextButton", { + BackgroundTransparency = 1, + BorderSizePixel = 0, + Size = UDim2.new(0, titleWidth, 1, 0), + AnchorPoint = Vector2.new(0, 0.5), + Position = UDim2.new(1, titlePadding, 0.5, 0), + + TextColor3 = theme.checkBox.titleColor, + Font = theme.checkBox.font, + TextSize = textSize, + TextXAlignment = Enum.TextXAlignment.Left, + TextYAlignment = Enum.TextYAlignment.Center, + TextTransparency = enabled and 0 or 0.5, + Text = title, + + [Roact.Event.Activated] = self.onActivated, + }), + }), + }) + end) +end + +return CheckBox \ No newline at end of file diff --git a/BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Components/CheckBox.spec.lua b/BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Components/CheckBox.spec.lua new file mode 100644 index 0000000000..2a2d149eb1 --- /dev/null +++ b/BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Components/CheckBox.spec.lua @@ -0,0 +1,64 @@ +return function() + local Library = script.Parent.Parent + local Roact = require(Library.Parent.Parent.Roact) + + local MockWrapper = require(Library.MockWrapper) + + local CheckBox = require(script.Parent.CheckBox) + + local function createTestCheckBox(enabled, selected) + return Roact.createElement(MockWrapper, {}, { + checkBox = Roact.createElement(CheckBox, { + Title = "Title", + TextSize = 24, + Enabled = enabled, + Selected = selected, + OnClicked = function() + end, + }) + }) + end + + it("should create and destroy without errors", function() + local element = createTestCheckBox(true, false) + local instance = Roact.mount(element) + Roact.unmount(instance) + end) + + it("should render correctly", function () + local container = Instance.new("Folder") + local instance = Roact.mount(createTestCheckBox(true, false), container) + local frame = container:FindFirstChildOfClass("Frame") + + expect(frame.Background).to.be.ok() + expect(frame.Background.Selection).to.be.ok() + expect(frame.Background.TitleLabel).to.be.ok() + + Roact.unmount(instance) + end) + + it("should change color when highlighted", function () + local container = Instance.new("Folder") + + -- selected + local instance = Roact.mount(createTestCheckBox(true, true), container) + local frame = container:FindFirstChildOfClass("Frame") + expect(frame.Background.Selection.Visible).to.equal(true) + + -- unselected + instance = Roact.update(instance, createTestCheckBox(true, false)) + expect(frame.Background.Selection.Visible).to.equal(false) + Roact.unmount(instance) + end) + + it("should gray out when disabled", function () + local container = Instance.new("Folder") + local instance = Roact.mount(createTestCheckBox(false, true), container) + local frame = container:FindFirstChildOfClass("Frame") + + expect(frame.Background.Selection.Visible).to.equal(false) + expect(frame.Background.TitleLabel.TextTransparency).never.to.equal(0) + + Roact.unmount(instance) + end) +end \ No newline at end of file diff --git a/BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Components/DetailedDropdown.lua b/BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Components/DetailedDropdown.lua new file mode 100644 index 0000000000..9221875dd1 --- /dev/null +++ b/BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Components/DetailedDropdown.lua @@ -0,0 +1,353 @@ +--[[ + A dropdown menu styled to match the Roblox Studio start page. + Consists of a button used to open the dropdown as well as the menu itself. + Note that the logic for opening and closing the menu is contained within this component, + but the consumer is responsible for showing the current value in the button. + + Required Props: + UDim2 Size = The size of the button that opens the dropdown. + UDim2 Position = The position of the button that opens the dropdown. + int DisplayTextSize = The size of the text in the dropdown and button. + int DescriptionTextSize = The size of the subtext in the dropdown + int ItemHeight = The height of each entry in the dropdown, in pixels. + string ButtonText = Text to display currently selected option in menu + array Items = An ordered array of each item that should appear in the dropdown. + The array is formatted like this: + { + {Key = "Item1", Display = "SomeLocalizedTextForItem1", Description = "SomeLocalizedDescriptionForItem1"}, + {Key = "Item2", Display = "SomeLocalizedTextForItem2", Description = "SomeLocalizedDescriptionForItem2"}, + {Key = "Item3", Display = "SomeLocalizedTextForItem3", Description = "SomeLocalizedDescriptionForItem3"}, + } + Key is how the item will be referenced in code. Text is what will appear to the user. + function OnItemClicked(item) = A callback when the user selects an item in the dropdown. + Returns the item as it was defined in the Items array. + bool Enabled = Enables component if true and accepts input + + Optional Props: + int MaxItems = The maximum number of entries that can display at a time. + If this is less than the number of entries in the dropdown, a scrollbar will appear. + bool ShowRibbon = Whether to show a colored ribbon next to the currently + hovered dropdown entry. Usually should be enabled for Light theme only. + int TextPadding = The amount of padding, in pixels, around the text elements. + int IconSize = The size of the arrow icon in the button. + int IconPadding = The distance from the right side of the arrow icon to the button edge. + int ScrollBarPadding = The padding which appears on either side of the scrollbar. + int ScrollBarThickness = The horizontal width of the scrollbar. +]] +local FFlagStudioFixUILibDropdownStyle = game:GetFastFlag("StudioFixUILibDropdownStyle") +local FFlagStudioFixUILibDropdownText = game:GetFastFlag("StudioFixUILibDropdownText") + +-- Defaults +local TEXT_PADDING = 10 +local ICON_SIZE = 12 +local ICON_PADDING = 4 + +local RIBBON_WIDTH = 5 +local VERTICAL_OFFSET = 2 + +local MAX_WIDTH = 300 + +local TextService = game:GetService("TextService") + +local Library = script.Parent.Parent +local Roact = require(Library.Parent.Parent.Roact) + +local Theming = require(Library.Theming) +local withTheme = Theming.withTheme + +local DropdownMenu = require(Library.Components.DropdownMenu) +local RoundFrame = require(Library.Components.RoundFrame) +local createFitToContent = require(Library.Components.createFitToContent) + +local DetailedDropdown = Roact.PureComponent:extend("DetailedDropdown") + +local FitToContent = createFitToContent("Frame", "UIListLayout", { + SortOrder = Enum.SortOrder.LayoutOrder, + Padding = UDim.new(0, TEXT_PADDING), +}) + +function DetailedDropdown:init() + self.state = { + showDropdown = false, + isButtonHovered = false, + dropdownItem = nil, + } + self.buttonRef = Roact.createRef() + + self.onItemClicked = function(item) + if self.props.Enabled then + self.props.OnItemClicked(item.Key) + self.hideDropdown() + end + end + + self.showDropdown = function() + if self.props.Enabled then + self:setState({ + showDropdown = true, + }) + end + end + + self.hideDropdown = function() + if self.props.Enabled then + self:setState({ + showDropdown = false, + }) + end + end + + self.onKeyMouseEnter = function(item) + if self.props.Enabled then + self:setState({ + dropdownItem = item, + }) + end + end + + self.onKeyMouseLeave = function(item) + if self.props.Enabled then + if self.state.dropdownItem == item then + self:setState({ + dropdownItem = Roact.None, + }) + end + end + end + + self.onMouseEnter = function() + if self.props.Enabled then + self:setState({ + isButtonHovered = true, + }) + end + end + + self.onMouseLeave = function() + if self.props.Enabled then + self:setState({ + isButtonHovered = false, + }) + end + end +end + +function DetailedDropdown:createMainTextLabel(key, displayText, displayTextSize, displayTextColor, textPadding, font, height) + return Roact.createElement("TextLabel", { + Size = UDim2.new(1, 0, 0, height), + Font = font, + TextSize = displayTextSize, + Text = displayText, + TextXAlignment = Enum.TextXAlignment.Left, + TextColor3 = displayTextColor, + BackgroundTransparency = 1, + TextWrapped = true, + LayoutOrder = 0, + }, { + Padding = Roact.createElement("UIPadding", { + PaddingTop = UDim.new(0, textPadding), + PaddingLeft = UDim.new(0, textPadding), + }), + }) +end + +function DetailedDropdown:createDescriptionTextLabel(key, descriptionText, descriptionTextSize, descriptionTextColor, textPadding, font, height) + return Roact.createElement("TextLabel", { + Size = UDim2.new(1, 0, 0, height), + Font = font, + TextSize = descriptionTextSize, + Text = descriptionText, + TextXAlignment = Enum.TextXAlignment.Left, + TextColor3 = descriptionTextColor, + BackgroundTransparency = 1, + TextWrapped = true, + LayoutOrder = 1, + }, { + Padding = Roact.createElement("UIPadding", { + PaddingLeft = UDim.new(0, textPadding), + PaddingRight = UDim.new(0, textPadding), + }), + }) +end + +function DetailedDropdown:render() + return withTheme(function(theme) + local props = self.props + local state = self.state + local dropdownTheme = theme.detailedDropdown + + local showDropdown = state.showDropdown + local buttonRef = self.buttonRef and self.buttonRef.current + local buttonExtents + if buttonRef then + local buttonMin = buttonRef.AbsolutePosition + local buttonSize = buttonRef.AbsoluteSize + local buttonMax = buttonMin + buttonSize + buttonExtents = Rect.new(buttonMin.X, buttonMin.Y, buttonMax.X, buttonMax.Y) + end + + local items = props.Items or {} + local selectedItem = props.SelectedItem + local size = props.Size + local position = props.Position + local displayTextSize = props.DisplayTextSize + local descriptionTextSize = props.DescriptionTextSize + local itemHeight = props.ItemHeight + local maxItems = props.MaxItems + local showRibbon = props.ShowRibbon + local enabled = props.Enabled + + local textPadding = props.TextPadding or TEXT_PADDING + local iconSize = props.IconSize or ICON_SIZE + local iconPadding = props.IconPadding or ICON_PADDING + local scrollBarPadding = props.ScrollBarPadding + local scrollBarThickness = props.ScrollBarThickness + + local dropdownItem = state.dropdownItem + local isButtonHovered = state.isButtonHovered + local buttonText = props.ButtonText + + local maxItemWidth = 0 + local maxWidth = props.MaxWidth or MAX_WIDTH + local maxHeight = maxItems and (maxItems * itemHeight) or nil + + for _, data in ipairs(items) do + local displayTextBound = TextService:GetTextSize(data.Display, + displayTextSize, dropdownTheme.font, Vector2.new(math.huge, math.huge)) + + local displayItemWidth = displayTextBound.X + textPadding * 2 + + local descriptionTextBound = TextService:GetTextSize(data.Description, + descriptionTextSize, dropdownTheme.font, Vector2.new(math.huge, math.huge)) + + local descriptionItemWidth = descriptionTextBound.X + textPadding * 2 + + maxItemWidth = math.max(maxItemWidth, displayItemWidth, descriptionItemWidth) + end + + maxWidth = math.min(maxItemWidth, maxWidth) + + local hoverTheme = dropdownTheme.selected + if FFlagStudioFixUILibDropdownStyle then + hoverTheme = dropdownTheme.hovered + end + + local buttonTheme = (showDropdown or isButtonHovered) and hoverTheme + or dropdownTheme + + return Roact.createElement("ImageButton", { + LayoutOrder = props.LayoutOrder or 0, + AnchorPoint = props.AnchorPoint or Vector2.new(0,0), + Size = size, + Position = position, + BackgroundTransparency = 1, + Image = "", + + [Roact.Ref] = self.buttonRef, + + [Roact.Event.Activated] = self.showDropdown, + [Roact.Event.MouseEnter] = self.onMouseEnter, + [Roact.Event.MouseLeave] = self.onMouseLeave, + }, { + RoundFrame = Roact.createElement(RoundFrame, { + Size = UDim2.new(1, 0, 1, 0), + BackgroundColor3 = enabled and buttonTheme.backgroundColor or buttonTheme.disabled, + BorderColor3 = buttonTheme.borderColor, + }), + + ArrowIcon = Roact.createElement("ImageLabel", { + Size = UDim2.new(0, iconSize, 0, iconSize), + Position = UDim2.new(1, -iconPadding, 0.5, 0), + AnchorPoint = Vector2.new(1, 0.5), + BackgroundTransparency = 1, + ImageColor3 = enabled and buttonTheme.displayText or buttonTheme.disabledText, + Image = dropdownTheme.arrowImage, + }), + + TextLabel = Roact.createElement("TextLabel", { + Size = UDim2.new(1, FFlagStudioFixUILibDropdownText and -iconSize or 0, 1, 0), + BackgroundTransparency = 1, + Font = dropdownTheme.font, + TextColor3 = enabled and buttonTheme.displayText or buttonTheme.disabledText, + TextSize = displayTextSize, + Text = buttonText, + TextXAlignment = Enum.TextXAlignment.Left, + TextTruncate = FFlagStudioFixUILibDropdownText and Enum.TextTruncate.AtEnd or nil, + }, { + Padding = Roact.createElement("UIPadding", { + PaddingLeft = UDim.new(0, textPadding), + }), + }), + + Dropdown = showDropdown and buttonRef and Roact.createElement(DropdownMenu, { + OnItemClicked = self.onItemClicked, + OnFocusLost = self.hideDropdown, + SourceExtents = buttonExtents, + Offset = Vector2.new(0, VERTICAL_OFFSET), + MaxHeight = maxHeight, + ShowBorder = false, + ScrollBarPadding = scrollBarPadding, + ScrollBarThickness = scrollBarThickness, + + Items = items, + RenderItem = function(item, index, activated) + local key = item.Key + local selected = key == selectedItem + local displayText = item.Display + local descriptionText = item.Description + local isHovered = dropdownItem == key + local displayTextColor = isHovered and dropdownTheme.hovered.displayText + or dropdownTheme.displayText + local descriptionTextColor = dropdownTheme.descriptionText + + local displayTextBound = TextService:GetTextSize(displayText, + displayTextSize, dropdownTheme.font, Vector2.new(maxWidth, math.huge)) + + local descriptionTextBound = TextService:GetTextSize(descriptionText, + descriptionTextSize, dropdownTheme.font, Vector2.new(maxWidth, math.huge)) + + local itemColor = dropdownTheme.backgroundColor + if FFlagStudioFixUILibDropdownStyle and selected then + itemColor = dropdownTheme.selected.backgroundColor + elseif isHovered then + itemColor = dropdownTheme.hovered.backgroundColor + end + + return Roact.createElement("ImageButton", { + Size = UDim2.new(0, maxWidth, 0, displayTextBound.Y + descriptionTextBound.Y + textPadding * 2), + BackgroundColor3 = itemColor, + BorderSizePixel = 0, + LayoutOrder = index, + AutoButtonColor = false, + [Roact.Event.Activated] = activated, + [Roact.Event.MouseEnter] = function() + self.onKeyMouseEnter(key) + end, + [Roact.Event.MouseLeave] = function() + self.onKeyMouseLeave(key) + end, + }, { + Roact.createElement(FitToContent, { + LayoutOrder = index, + BackgroundTransparency = 1, + } , { + Ribbon = isHovered and showRibbon and Roact.createElement("Frame", { + Size = UDim2.new(0, RIBBON_WIDTH, 1, 0), + BackgroundColor3 = dropdownTheme.selected.backgroundColor, + BorderSizePixel = 0, + }), + + MainTextLabel = self:createMainTextLabel(key, displayText, displayTextSize, displayTextColor, + textPadding, dropdownTheme.font, displayTextBound.Y), + + DescriptionTextLabel = self:createDescriptionTextLabel(key, descriptionText, descriptionTextSize, descriptionTextColor, + textPadding, dropdownTheme.font, descriptionTextBound.Y), + }) + }) + end, + }) + }) + end) +end + +return DetailedDropdown diff --git a/BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Components/DetailedDropdown.spec.lua b/BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Components/DetailedDropdown.spec.lua new file mode 100644 index 0000000000..80388c2ca8 --- /dev/null +++ b/BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Components/DetailedDropdown.spec.lua @@ -0,0 +1,34 @@ +return function() + local Library = script.Parent.Parent + local Roact = require(Library.Parent.Parent.Roact) + + local MockWrapper = require(Library.MockWrapper) + + local DetailedDropdown = require(script.Parent.DetailedDropdown) + + local function createTestDetailedDropdown(props, children) + return Roact.createElement(MockWrapper, {}, { + DetailedDropdown = Roact.createElement(DetailedDropdown, props, children) + }) + end + + it("should create and destroy without errors", function() + local element = createTestDetailedDropdown() + local instance = Roact.mount(element) + Roact.unmount(instance) + end) + + it("should render correctly", function() + local container = Instance.new("Folder") + local instance = Roact.mount(createTestDetailedDropdown(), container) + local button = container:FindFirstChildOfClass("ImageButton") + + expect(button).to.be.ok() + expect(button.RoundFrame).to.be.ok() + expect(button.ArrowIcon).to.be.ok() + expect(button.TextLabel).to.be.ok() + expect(button.TextLabel.Padding).to.be.ok() + + Roact.unmount(instance) + end) +end \ No newline at end of file diff --git a/BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Components/DragTarget.lua b/BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Components/DragTarget.lua new file mode 100644 index 0000000000..a4666cb555 --- /dev/null +++ b/BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Components/DragTarget.lua @@ -0,0 +1,58 @@ +--[[ + A component that can listen to change in mouse position while active, + and then has a callback for removal once the user is done dragging. + + Props: + function OnDragMoved(input) = A callback for when the user drags + the mouse. The input param is the InputObject from the InputChanged event. + + function OnDragEnded() = A callback for when the user has stopped dragging. + + Usage: + From a stateful component, hold on to a dragging state. When the user + presses the mouse on a draggable element, set the dragging state to + true. When dragging is true, render this element. Hook up this element's + OnDragEnded function to setting the dragging state to false. +]] + +local Library = script.Parent.Parent +local Roact = require(Library.Parent.Parent.Roact) + +local Focus = require(Library.Focus) +local CaptureFocus = Focus.CaptureFocus + +local DragTarget = Roact.PureComponent:extend("DragTarget") + +function DragTarget:init() + self.inputChanged = function(rbx, input) + if input.UserInputType == Enum.UserInputType.MouseMovement then + if self.props.OnDragMoved then + self.props.OnDragMoved(input) + end + end + end + + self.inputEnded = function() + if self.props.OnDragEnded then + self.props.OnDragEnded() + end + end +end + +function DragTarget:render() + return Roact.createElement(CaptureFocus, { + OnFocusLost = self.inputEnded, + }, { + DragListener = Roact.createElement("ImageButton", { + BackgroundTransparency = 1, + Size = UDim2.new(1, 0, 1, 0), + + [Roact.Event.InputChanged] = self.inputChanged, + [Roact.Event.InputEnded] = self.inputEnded, + [Roact.Event.MouseButton1Up] = self.inputEnded, + [Roact.Event.MouseButton2Up] = self.inputEnded, + }, self.props[Roact.Children]) + }) +end + +return DragTarget diff --git a/BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Components/DragTarget.spec.lua b/BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Components/DragTarget.spec.lua new file mode 100644 index 0000000000..e94a8dee44 --- /dev/null +++ b/BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Components/DragTarget.spec.lua @@ -0,0 +1,38 @@ +return function() + local Library = script.Parent.Parent + local Roact = require(Library.Parent.Parent.Roact) + + local MockWrapper = require(Library.MockWrapper) + + local DragTarget = require(script.Parent.DragTarget) + + local function createTestDragTarget(container) + return Roact.createElement(MockWrapper, { + Container = container, + }, { + DragTarget = Roact.createElement(DragTarget) + }) + end + + it("should create and destroy without errors", function() + local element = createTestDragTarget() + local instance = Roact.mount(element) + Roact.unmount(instance) + end) + + it("should render correctly", function() + local container = Instance.new("Folder") + + local element = createTestDragTarget(container) + + local instance = Roact.mount(element, container) + + local gui = container:FindFirstChild("MockGui") + expect(gui).to.be.ok() + expect(gui.TopLevelDetector).to.be.ok() + expect(gui.TopLevelDetector.ScrollBlocker).to.be.ok() + expect(gui.TopLevelDetector.ScrollBlocker.DragListener).to.be.ok() + + Roact.unmount(instance) + end) +end \ No newline at end of file diff --git a/BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Components/DropShadow.lua b/BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Components/DropShadow.lua new file mode 100644 index 0000000000..ace6eaede4 --- /dev/null +++ b/BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Components/DropShadow.lua @@ -0,0 +1,58 @@ +--[[ + A rectangular drop shadow that appears behind an element. + + Props: + Vector2 Offset = The offset of this drop shadow from the element it appears beneath. + float Transparency = The transparency of the drop shadow, from 0 to 1. + Color3 Color = The color of the drop shadow. + SizePixel = The size of the drop shadow, in pixels. + ZIndex = The render order of the drop shadow. Make sure it is behind your element. +]] + +local Library = script.Parent.Parent + +local Roact = require(Library.Parent.Parent.Roact) + +local Theming = require(Library.Theming) +local withTheme = Theming.withTheme + +local SLICE_SIZE = 8 +local DROP_SHADOW_SLICE = Rect.new(SLICE_SIZE, SLICE_SIZE, SLICE_SIZE, SLICE_SIZE) + +local DropShadow = Roact.PureComponent:extend("DropShadow") + +function DropShadow:render() + return withTheme(function(theme) + local props = self.props + local shadowTheme = theme.dropShadow + + local shadowColor = props.Color + local shadowTransparency = props.Transparency + local offset = props.Offset or Vector2.new() + local shadowSize = props.SizePixel or SLICE_SIZE + local zindex = props.ZIndex or 0 + + -- SliceScale is multiplicative, so we need to normalize to the slice size + local sliceScale = shadowSize / SLICE_SIZE + + return Roact.createElement("ImageLabel", { + Size = UDim2.new(1, shadowSize, 1, shadowSize), + AnchorPoint = Vector2.new(0.5, 0.5), + Position = UDim2.new(0.5, offset.X, 0.5, offset.Y), + ZIndex = zindex, + + BackgroundTransparency = 1, + BorderSizePixel = 0, + + Image = shadowTheme.image, + ImageColor3 = shadowColor, + ImageTransparency = shadowTransparency, + + ScaleType = Enum.ScaleType.Slice, + SliceCenter = DROP_SHADOW_SLICE, + SliceScale = sliceScale, + }) + end) +end + +return DropShadow diff --git a/BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Components/DropShadow.spec.lua b/BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Components/DropShadow.spec.lua new file mode 100644 index 0000000000..e3c71b1050 --- /dev/null +++ b/BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Components/DropShadow.spec.lua @@ -0,0 +1,30 @@ +return function() + local Library = script.Parent.Parent + local Roact = require(Library.Parent.Parent.Roact) + + local MockWrapper = require(Library.MockWrapper) + + local DropShadow = require(script.Parent.DropShadow) + + local function createTestDropShadow(props) + return Roact.createElement(MockWrapper, {}, { + DropShadow = Roact.createElement(DropShadow, props) + }) + end + + it("should create and destroy without errors", function() + local element = createTestDropShadow() + local instance = Roact.mount(element) + Roact.unmount(instance) + end) + + it("should render correctly", function () + local container = Instance.new("Folder") + local instance = Roact.mount(createTestDropShadow(), container) + local shadow = container:FindFirstChildOfClass("ImageLabel") + + expect(shadow).to.be.ok() + + Roact.unmount(instance) + end) +end \ No newline at end of file diff --git a/BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Components/DropdownMenu.lua b/BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Components/DropdownMenu.lua new file mode 100644 index 0000000000..506de613f3 --- /dev/null +++ b/BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Components/DropdownMenu.lua @@ -0,0 +1,241 @@ +--[[ + A generic dropdown menu interface which can accept any kind of components. + The consuming component is in charge of implementing the logic that dictates + when this dropdown menu should show and hide. + + This dropdown detects if it is too close to the corners of the gui and realigns if needed. + For example, if it is too close to the bottom of the gui to render all elements, it + renders its elements above the hosting button instead of below. + + For an example of how this component can be used, see StyledDropdown. + + Required Props: + Rect SourceExtents = A Rect representing the absolute position and size of + the button which is hosting this dropdown. + + table Items = An ordered array of each item that should appear in the dropdown. + Each item in the array can be of any format, and will be passed to the RenderItem function. + function RenderItem(item, index, activated) = A function used to render a dropdown item. + Item is an entry from the Items array that was passed into this component's props. + Index is the index of the current item in the Items array. + Activated is a callback that the item should connect if it is clickable. + + function OnItemClicked(item) = A callback for when the user selects a dropdown entry. + Returns the item as it was defined in the Items array. + function OnFocusLost = A callback for when the user clicks away from the dropdown + without selecting an item. + + Optional Props: + int MaxHeight = An optional maximum height for this dropdown. If the items surpass + the max height, a scrollbar will be added to the dropdown so all items are visible. + int ScrollBarPadding = The padding which appears on either side of the scrollbar. + int ScrollBarThickness = The horizontal width of the scrollbar. + bool ShowBorder = Whether to show a border around the elements in the dropdown. + Vector2 Offset = An offset from the button which is hosting this dropdown. + Note that the dropdown already takes into account the size of the hosting button, + and will already automatically place itself below the button. This offset is optional + and can be used to add some extra padding. + Enum.VerticalAlignment StartDirection=Bottom The direction the DropdownMenu will appear + from SourceExtents by default. This can only be Top/Bottom. This will not lock the + direction of the DropdownMenu. If there is not enough room in the default direction, + it will flip to the other direction +]] + +local ROUNDED_FRAME_SLICE = Rect.new(3, 3, 13, 13) +local SCROLLBAR_THICKNESS = 8 +local SCROLLBAR_PADDING = 2 + +local RunService = game:GetService("RunService") + +local Library = script.Parent.Parent +local Roact = require(Library.Parent.Parent.Roact) + +local Theming = require(Library.Theming) +local withTheme = Theming.withTheme + +local Focus = require(Library.Focus) +local CaptureFocus = Focus.CaptureFocus +local withFocus = Focus.withFocus + +local StyledScrollingFrame = require(Library.Components.StyledScrollingFrame) + +local DropdownMenu = Roact.PureComponent:extend("DropdownMenu") + +function DropdownMenu:init(props) + assert( props.StartDirection == Enum.VerticalAlignment.Top or + props.StartDirection == Enum.VerticalAlignment.Bottom or + props.StartDirection == nil, + + "StartDirection must be Enum.VerticalAlignment.Bottom, Enum.VerticalAlignment.Top, or nil. " + .."Got '"..tostring(props.StartDirection).."'" + ) + + self.direction = props.StartDirection or Enum.VerticalAlignment.Bottom + self.layout = 0 + + self.recalculateSize = function(rbx) + -- We have to wait one step to change the state here, or + -- we will change the state while the component is rendering + -- and the component won't move to the right location. + local nextStep + nextStep = RunService.Heartbeat:Connect(function() + nextStep:Disconnect() + + -- The component may have since been unmounted, in which case + -- we shouldn't update state or it will fail with an error + if not self.mounted then return end + + self:setState({ + menuSize = rbx.AbsoluteContentSize + }) + end) + end + + self.resetLayout = function() + self.layout = 0 + end + + self.nextLayout = function() + self.layout = self.layout + 1 + return self.layout + end +end + +function DropdownMenu:didMount() + self.mounted = true +end + +function DropdownMenu:willUnmount() + self.mounted = false +end + +function DropdownMenu:render() + return withTheme(function(theme) + return withFocus(function(pluginGui) + local props = self.props + local state = self.state + + local items = props.Items + local renderItem = self.props.RenderItem + local sourceExtents = props.SourceExtents + + assert(items ~= nil and type(items) == "table", + "DropdownMenu requires an Items table.") + assert(renderItem ~= nil and type(renderItem) == "function", + "DropdownMenu requires a RenderItem function.") + assert(sourceExtents ~= nil, + "DropdownMenu requires a SourceExtents prop.") + + local components = {} + + local dropdownTheme = theme.dropdownMenu + + local canRender = state.menuSize ~= nil + local menuSize = state.menuSize or Vector2.new() + local width = props.ListWidth or menuSize.X + local height = menuSize.Y + + local offset = props.Offset or Vector2.new() + local showBorder = props.ShowBorder + local scrollBarThickness = props.ScrollBarThickness or SCROLLBAR_THICKNESS + local scrollBarPadding = props.ScrollBarPadding or SCROLLBAR_PADDING + + local maxHeight = props.MaxHeight + if maxHeight == nil or maxHeight > height then + maxHeight = height + elseif maxHeight < height then + -- Add scrollbar gutter + width = width + scrollBarThickness + (scrollBarPadding * 2) + end + + local sourcePosition = sourceExtents.Min + local sourceSize = Vector2.new(sourceExtents.Width, sourceExtents.Height) + local guiSize = pluginGui.AbsoluteSize + + local xPos, yPos + if sourcePosition.X + offset.X + width <= guiSize.X then + xPos = sourcePosition.X + offset.X + else + xPos = sourcePosition.X + sourceSize.X + offset.X - width + end + + local enoughRoomOnBottom = sourcePosition.Y + sourceSize.Y + offset.Y + maxHeight < guiSize.Y + local enoughRoomOnTop = sourcePosition.Y - offset.Y - maxHeight > 0 + + -- Don't flip if there is not enough room on either side. This will just cause a spasm of + -- flip-flopping every render + if enoughRoomOnBottom or enoughRoomOnTop then + if self.direction == Enum.VerticalAlignment.Bottom and not enoughRoomOnBottom then + self.direction = Enum.VerticalAlignment.Top + elseif self.direction == Enum.VerticalAlignment.Top and not enoughRoomOnTop then + self.direction = Enum.VerticalAlignment.Bottom + end + end + + local verticalAlignment + if self.direction == Enum.VerticalAlignment.Bottom then + yPos = sourcePosition.Y + sourceSize.Y + offset.Y + verticalAlignment = Enum.VerticalAlignment.Top + else + yPos = sourcePosition.Y - offset.Y - maxHeight + verticalAlignment = Enum.VerticalAlignment.Bottom + end + + local position = UDim2.new(0, xPos, 0, yPos) + + components.Layout = Roact.createElement("UIListLayout", { + SortOrder = Enum.SortOrder.LayoutOrder, + FillDirection = Enum.FillDirection.Vertical, + VerticalAlignment = verticalAlignment, + [Roact.Change.AbsoluteContentSize] = self.recalculateSize, + }) + + for index, item in ipairs(items) do + table.insert(components, renderItem(item, index, function() + self.props.OnItemClicked(item) + end)) + end + + local contents = { + Border = showBorder and Roact.createElement("ImageLabel", { + Visible = canRender, + Position = position, + Size = UDim2.new(0, width, 0, maxHeight), + ZIndex = 3, + + BackgroundTransparency = 1, + ImageColor3 = dropdownTheme.borderColor, + + Image = dropdownTheme.borderImage, + ScaleType = Enum.ScaleType.Slice, + SliceCenter = ROUNDED_FRAME_SLICE, + }), + } + + if maxHeight and maxHeight < height then + contents.ScrollingContainer = Roact.createElement(StyledScrollingFrame, { + Visible = canRender, + Position = position, + Size = UDim2.new(0, width, 0, maxHeight), + BackgroundTransparency = 1, + CanvasSize = UDim2.new(0, 0, 0, height), + ScrollBarPadding = scrollBarPadding, + ScrollBarThickness = scrollBarThickness, + }, components) + else + contents.Container = Roact.createElement("Frame", { + Visible = canRender, + Position = position, + Size = UDim2.new(0, width, 0, height), + BackgroundTransparency = 1, + }, components) + end + + return Roact.createElement(CaptureFocus, { + OnFocusLost = props.OnFocusLost, + }, contents) + end) + end) +end + +return DropdownMenu diff --git a/BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Components/DropdownMenu.spec.lua b/BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Components/DropdownMenu.spec.lua new file mode 100644 index 0000000000..36fa138ed8 --- /dev/null +++ b/BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Components/DropdownMenu.spec.lua @@ -0,0 +1,228 @@ +return function() + local Library = script.Parent.Parent + local Roact = require(Library.Parent.Parent.Roact) + + local MockWrapper = require(Library.MockWrapper) + + local DropdownMenu = require(script.Parent.DropdownMenu) + + local sourceExtents = Rect.new(0, 0, 150, 150) + + local function createTestDropdownMenu(props, children, container) + return Roact.createElement(MockWrapper, { + Container = container, + }, { + DropdownMenu = Roact.createElement(DropdownMenu, props, children) + }) + end + + it("should create and destroy without errors", function() + local element = createTestDropdownMenu({ + SourceExtents = sourceExtents, + Items = {}, + RenderItem = function(item) + end, + }) + local instance = Roact.mount(element) + Roact.unmount(instance) + end) + + it("should render correctly", function() + local container = Instance.new("Folder") + + local element = createTestDropdownMenu({ + SourceExtents = sourceExtents, + Items = {}, + RenderItem = function(item) + end, + }, {}, container) + + local instance = Roact.mount(element, container) + + local gui = container:FindFirstChild("MockGui") + expect(gui).to.be.ok() + expect(gui.TopLevelDetector).to.be.ok() + expect(gui.TopLevelDetector.ScrollBlocker).to.be.ok() + expect(gui.TopLevelDetector.ScrollBlocker.Container).to.be.ok() + + local dropdownContainer = gui.TopLevelDetector.ScrollBlocker.Container + expect(dropdownContainer.Layout).to.be.ok() + + Roact.unmount(instance) + end) + + it("should require an Items table", function() + local element = createTestDropdownMenu({ + RenderItem = function() + end, + SourceExtents = sourceExtents, + }) + expect(function() + Roact.mount(element) + end).to.throw() + + element = createTestDropdownMenu({ + Items = true, + RenderItem = function() + end, + SourceExtents = sourceExtents, + }) + expect(function() + Roact.mount(element) + end).to.throw() + end) + + it("should require a RenderItem function", function() + local element = createTestDropdownMenu({ + Items = {}, + SourceExtents = sourceExtents, + }) + expect(function() + Roact.mount(element) + end).to.throw() + + element = createTestDropdownMenu({ + Items = {}, + RenderItem = true, + SourceExtents = sourceExtents, + }) + expect(function() + Roact.mount(element) + end).to.throw() + end) + + it("should require a SourceExtents prop", function() + local element = createTestDropdownMenu({ + Items = {}, + RenderItem = function() + end, + }) + expect(function() + Roact.mount(element) + end).to.throw() + end) + + it("should error if given invalid StartDirection", function() + local element = createTestDropdownMenu({ + Items = {}, + SourceExtents = sourceExtents, + RenderItem = function() + end, + StartDirection = 0, + }) + expect(function() + Roact.mount(element) + end).to.throw() + end) + + it("should render items", function() + local container = Instance.new("Folder") + + local element = createTestDropdownMenu({ + SourceExtents = sourceExtents, + Items = {"Frame"}, + RenderItem = function() + return Roact.createElement("Frame") + end, + }, {}, container) + + local instance = Roact.mount(element, container) + + local gui = container:FindFirstChild("MockGui") + local dropdownContainer = gui.TopLevelDetector.ScrollBlocker.Container + expect(dropdownContainer["1"]).to.be.ok() + + Roact.unmount(instance) + end) + + it("should respect the order of items", function() + local container = Instance.new("Folder") + + local element = createTestDropdownMenu({ + SourceExtents = sourceExtents, + Items = {"FirstFrame", "SecondFrame", "ThirdFrame"}, + RenderItem = function(item, index) + return Roact.createElement("TextLabel", { + LayoutOrder = index, + Text = item, + }) + end, + }, {}, container) + + local instance = Roact.mount(element, container) + + local gui = container:FindFirstChild("MockGui") + local dropdownContainer = gui.TopLevelDetector.ScrollBlocker.Container + expect(dropdownContainer["1"].LayoutOrder).to.equal(1) + expect(dropdownContainer["2"].LayoutOrder).to.equal(2) + expect(dropdownContainer["3"].LayoutOrder).to.equal(3) + expect(dropdownContainer["1"].Text).to.equal("FirstFrame") + expect(dropdownContainer["2"].Text).to.equal("SecondFrame") + expect(dropdownContainer["3"].Text).to.equal("ThirdFrame") + + Roact.unmount(instance) + end) + + it("should preserve menu direction when there is enough room", function() + local function getMenuDirection(listLayout) + return listLayout.VerticalAlignment == Enum.VerticalAlignment.Top and -1 or 1 + end + + local container = Instance.new("Folder") + + local elementAtTop = createTestDropdownMenu({ + SourceExtents = Rect.new(0, 0, 150, 150), + Items = {"FirstFrame"}, + RenderItem = function(item, index) + return Roact.createElement("TextLabel", { + LayoutOrder = index, + Text = item, + Size = UDim2.new(1, 0, 0, 20), + }) + end, + }, {}, container) + + local instance = Roact.mount(elementAtTop, container) + + local gui = container:FindFirstChild("MockGui") + local listLayout = gui.TopLevelDetector.ScrollBlocker.Container.Layout + + -- No way to get MockGui canvas size to dock SourceExtents at the bottom/middle unless + -- we mount it and then check the instance's size + local elementAtBottom = createTestDropdownMenu({ + SourceExtents = Rect.new(0, gui.AbsoluteSize.Y + 150, 150, 150), + Items = {"FirstFrame"}, + RenderItem = function(item, index) + return Roact.createElement("TextLabel", { + LayoutOrder = index, + Text = item, + Size = UDim2.new(1, 0, 0, 20), + }) + end, + }, {}, container) + local elementInMiddle = createTestDropdownMenu({ + SourceExtents = Rect.new(0, math.floor(gui.AbsoluteSize.Y/2), 150, 150), + Items = {"FirstFrame"}, + RenderItem = function(item, index) + return Roact.createElement("TextLabel", { + LayoutOrder = index, + Text = item, + Size = UDim2.new(1, 0, 0, 20), + }) + end, + }, {}, container) + + -- The default direction is down, so we should be displaying beneath SourceExtents + expect(getMenuDirection(listLayout)).to.equal(-1) + + -- There is not enough room below, so we flip to top + Roact.update(instance, elementAtBottom) + expect(getMenuDirection(listLayout)).to.equal(1) + + -- There is now enough room below, but we preserve direction, so we are still above SourceExtents + Roact.update(instance, elementInMiddle) + expect(getMenuDirection(listLayout)).to.equal(1) + + Roact.unmount(instance) + end) +end \ No newline at end of file diff --git a/BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Components/ExpandableList.lua b/BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Components/ExpandableList.lua new file mode 100644 index 0000000000..a29b4fdb27 --- /dev/null +++ b/BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Components/ExpandableList.lua @@ -0,0 +1,108 @@ +--[[ + A generic expandable list interface which can accept any kind of components. Intended to be a lightweight control + where it will toggle visibility of a frame which contains content that user will pass in + + Required Props: + table TopLevelItem = Property which takes in table of Roact elements to display top level button. + Will always be displayed and entire element(s) will be clickable to toggle dropdown visibility + table Content = Property which takes in table of Roact elements to display in dropdown area. + + LayoutOrder = props.LayoutOrder (Required, passed through) + Position = props.Position (Required, passed through) + AnchorPoint = props.AnchorPoint (Required, passed through) + + function OnExpandedStateChanged() - Invoked whenever the ExpandableList is opened/closed + bool IsExpanded - Whether the ExpandableList is expanded or not +]] + +local Library = script.Parent.Parent +local Roact = require(Library.Parent.Parent.Roact) + +local Theming = require(Library.Theming) +local withTheme = Theming.withTheme + +local join = require(Library.join) + +local createFitToContent = require(Library.Components.createFitToContent) + +local ExpandableList = Roact.PureComponent:extend("ExpandableList") + +local ContentFit = createFitToContent("Frame", "UIListLayout", { + SortOrder = Enum.SortOrder.LayoutOrder, + Padding = UDim.new(0, 0), +}) + +local TopLevelContentFit = createFitToContent("ImageButton", "UIListLayout", { + SortOrder = Enum.SortOrder.LayoutOrder, + Padding = UDim.new(0, 0), + FillDirection = Enum.FillDirection.Horizontal, +}) + +local TopLevelItem + +function ExpandableList:init() + self.state = { + isButtonHovered = false, + } + self.buttonRef = Roact.createRef() + + self.toggleList = function() + self.props.OnExpandedStateChanged() + end + + self.onMouseEnter = function() + self:setState({ + isButtonHovered = true, + }) + end + + self.onMouseLeave = function() + self:setState({ + isButtonHovered = false, + }) + end +end + + +function ExpandableList:render() + return withTheme(function(theme) + local props = self.props + local state = self.state + + local topLevelItem = props.TopLevelItem + local content = props.Content + + assert(topLevelItem ~= nil and type(topLevelItem) == "table", + "ExpandableList requires a TopLevelItem table.") + assert(content ~= nil and type(content) == "table", + "ExpandableList requires Content table.") + + return Roact.createElement(ContentFit, { + BackgroundTransparency = 1, + LayoutOrder = props.LayoutOrder or 0, + AnchorPoint = props.AnchorPoint or Vector2.new(0,0), + BorderSizePixel = 0, + Position = props.Position, + }, { + TopLevelItem = Roact.createElement(TopLevelContentFit, { + LayoutOrder = 0, + BorderSizePixel = 0, + BackgroundTransparency = 1, + Image = "", + + [Roact.Ref] = self.buttonRef, + [Roact.Event.Activated] = self.toggleList, + [Roact.Event.MouseEnter] = self.onMouseEnter, + [Roact.Event.MouseLeave] = self.onMouseLeave, + }, topLevelItem), + + ExpandableFrame = Roact.createElement(ContentFit, { + LayoutOrder = 1, + BackgroundTransparency = 1, + Visible = props.IsExpanded, + }, content), + }) + end) +end + +return ExpandableList diff --git a/BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Components/ExpandableList.spec.lua b/BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Components/ExpandableList.spec.lua new file mode 100644 index 0000000000..31f1645e77 --- /dev/null +++ b/BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Components/ExpandableList.spec.lua @@ -0,0 +1,143 @@ +return function() + local Library = script.Parent.Parent + local Roact = require(Library.Parent.Parent.Roact) + + local MockWrapper = require(Library.MockWrapper) + + local ExpandableList = require(script.Parent.ExpandableList) + + local function createTestExpandableList(props, children, container) + return Roact.createElement(MockWrapper, { + Container = container, + }, { + ExpandableList = Roact.createElement(ExpandableList, props, children) + }) + end + + it("should create and destroy without errors", function() + local element = createTestExpandableList({ + TopLevelItem = {}, + Content = {}, + }) + local instance = Roact.mount(element) + Roact.unmount(instance) + end) + + it("should require a top level item table", function() + local element = createTestExpandableList({ + Content = {}, + }) + expect(function() + Roact.mount(element) + end).to.throw() + local element = createTestExpandableList({ + TopLevelItem = true, + Content = {} + }) + expect(function() + Roact.mount(element) + end).to.throw() + end) + + it("should require a content table", function() + local element = createTestExpandableList({ + TopLevelItem = {}, + }) + expect(function() + Roact.mount(element) + end).to.throw() + local element = createTestExpandableList({ + TopLevelItem = {}, + Content = true + }) + expect(function() + Roact.mount(element) + end).to.throw() + end) + + it("should render correctly", function () + local container = Instance.new("Folder") + local instance = Roact.mount(createTestExpandableList({ + TopLevelItem = {}, + Content = {}, + }, {}, container), container) + local frame = container:FindFirstChildOfClass("Frame") + + expect(frame.TopLevelItem).to.be.ok() + expect(frame.ExpandableFrame).to.be.ok() + + Roact.unmount(instance) + end) + + it("items should be sized to contents", function () + local container = Instance.new("Folder") + local instance = Roact.mount(createTestExpandableList({ + TopLevelItem = { + ArrowIcon = Roact.createElement("TextLabel", { + Size = UDim2.new(0, 100, 0, 100), + }), + }, + Content = { + Label = Roact.createElement("TextLabel", { + Size = UDim2.new(0, 100, 0, 100), + }), + }, + }, {}, container), container) + local frame = container:FindFirstChildOfClass("Frame") + + expect(frame.TopLevelItem.Size.X.Scale).to.equal(1) + expect(frame.TopLevelItem.Size.Y.Offset).to.equal(100) + + expect(frame.ExpandableFrame.Size.X.Scale).to.equal(1) + expect(frame.ExpandableFrame.Size.Y.Offset).to.equal(100) + + + Roact.unmount(instance) + end) + + it("list should only show top item initially", function () + local container = Instance.new("Folder") + local instance = Roact.mount(createTestExpandableList({ + TopLevelItem = { + ArrowIcon = Roact.createElement("TextLabel", { + Size = UDim2.new(0, 100, 0, 100), + }), + }, + Content = { + Label = Roact.createElement("TextLabel", { + Size = UDim2.new(0, 100, 0, 100), + }), + }, + IsExpanded = false, + }, {}, container), container) + local frame = container:FindFirstChildOfClass("Frame") + + expect(frame.Size.X.Scale).to.equal(1) + expect(frame.Size.Y.Offset).to.equal(100) + + Roact.unmount(instance) + end) + + it("list should only show both items when expanded is true", function () + local container = Instance.new("Folder") + local instance = Roact.mount(createTestExpandableList({ + TopLevelItem = { + ArrowIcon = Roact.createElement("TextLabel", { + Size = UDim2.new(0, 100, 0, 100), + }), + }, + Content = { + Label = Roact.createElement("TextLabel", { + Size = UDim2.new(0, 100, 0, 100), + }), + }, + IsExpanded = true, + }, {}, container), container) + local frame = container:FindFirstChildOfClass("Frame") + + expect(frame.Size.X.Scale).to.equal(1) + expect(frame.Size.Y.Offset).to.equal(200) + + Roact.unmount(instance) + end) +end \ No newline at end of file diff --git a/BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Components/InfiniteScrollingFrame.lua b/BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Components/InfiniteScrollingFrame.lua new file mode 100644 index 0000000000..aff10bcdb8 --- /dev/null +++ b/BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Components/InfiniteScrollingFrame.lua @@ -0,0 +1,136 @@ +--[[ + A scroll frame that will check the left space between currently rendered asset + When scrolling down, it will try to re-render. If we found less than defined space or empty space + between the render asset and the canvas, then we will try to call request more function defined in the property + to fetch more assets. The function is responsible for the paging method to fetch more assets. + After the asset is returned, we will re-calculate canvase size. + This component will send out request to try to load more pages on didMount and after didUpdate. + + Required Properties: + function NextPageFunc - called during re-render when there is more empty spaces. This function should includes all the + parameters needed for the request except for the currentPage. Target page will be determined by the infiScroller. + + Optional Properties: + UDim2 Position - The position of the scrolling frame. + UDim2 Size - The size of the scrolling frame. + int LayoutOrder - sets order of element in layout + int NextPageRequestDistance - space left in layout before making request to fetch more elements + int CanvasHeight - used to specify height of canvas. + Roact ref LayoutRef - used to calculate the height of the canvas. +]] + +local Library = script.Parent.Parent +local Roact = require(Library.Parent.Parent.Roact) + +local StyledScrollingFrame = require(Library.Components.StyledScrollingFrame) + +local InfiniteScrollingFrame = Roact.PureComponent:extend("InfiniteScrollingFrame") + +local DEFAULT_CANVAS_HEIGHT = 900 + +local DEFAULT_REQUEST_DISTANCE = 0 + +local FFlagUILibraryInfiniteScrollingFrameRender = game:DefineFastFlag("UILibraryInfiniteScrollingFrameRender", false) + +function InfiniteScrollingFrame:init(props) + self.state = { + isRequestingNextPage = false, + } + + self.isRequestingNextPage = false + + self.scrollingFrameRef = Roact.createRef() + + self.checkCanvasAndRequest = function() + local scrollingFrame = self.scrollingFrameRef.current + if not scrollingFrame then return end + + local canvasY = scrollingFrame.CanvasPosition.Y + local windowHeight = scrollingFrame.AbsoluteWindowSize.Y + local canvasHeight = scrollingFrame.CanvasSize.Y.Offset + + local requestDistance = self.props.NextPageRequestDistance or DEFAULT_REQUEST_DISTANCE + + -- Where the bottom of the scrolling frame is relative to canvas size + local bottom = canvasY + windowHeight + local dist = canvasHeight - bottom + + if dist <= requestDistance and not self.state.isRequestingNextPage then + if FFlagUILibraryInfiniteScrollingFrameRender then + self.isRequestingNextPage = true + else + self:setState({ + isRequestingNextPage = true, + }) + end + self.requestNextPage() + end + end + + self.onScroll = function() + self.checkCanvasAndRequest(self) + end + + self.requestNextPage = function() + self.props.NextPageFunc() + end +end + +function InfiniteScrollingFrame:didMount() + self.checkCanvasAndRequest(self) +end + +function InfiniteScrollingFrame:didUpdate(previousProps, previousState) + -- check if request has fetched more children + if previousState.isRequestingNextPage then + for k,v in pairs(self.props[Roact.Children]) do + if v ~= previousProps[Roact.Children][k] then + if FFlagUILibraryInfiniteScrollingFrameRender then + self.isRequestingNextPage = false + else + self:setState({ + isRequestingNextPage = false, + }) + end + self.checkCanvasAndRequest(self) + end + end + end +end + +function InfiniteScrollingFrame:render() + local props = self.props + + local nextPageFunc = self.props.NextPageFunc + + assert(nextPageFunc ~= nil and type(nextPageFunc) == "function", + "InfiniteScrollingFrame requires a NextPageFunc function.") + + local position = props.Position + local size = props.Size + local layoutOrder = props.LayoutOrder + + local layout= props.LayoutRef and props.LayoutRef.current + local canvasHeight = DEFAULT_CANVAS_HEIGHT + if layout then + canvasHeight = layout.AbsoluteContentSize.Y + elseif props.CanvasHeight then + canvasHeight = props.CanvasHeight + end + + return Roact.createElement(StyledScrollingFrame, { + Position = position, + Size = size, + LayoutOrder = layoutOrder, + CanvasSize = UDim2.new(1, 0, 0, canvasHeight), + ZIndex = 1, + + ScrollingEnabled = true, + + OnScroll = self.onScroll, + + [Roact.Ref] = self.scrollingFrameRef, + }, props[Roact.Children]) +end + +return InfiniteScrollingFrame diff --git a/BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Components/InfiniteScrollingFrame.spec.lua b/BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Components/InfiniteScrollingFrame.spec.lua new file mode 100644 index 0000000000..a48c231372 --- /dev/null +++ b/BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Components/InfiniteScrollingFrame.spec.lua @@ -0,0 +1,69 @@ +return function() + local Library = script.Parent.Parent + local Roact = require(Library.Parent.Parent.Roact) + + local join = require(Library.join) + local MockWrapper = require(Library.MockWrapper) + + local InfiniteScrollingFrame = require(script.Parent.InfiniteScrollingFrame) + + local function createTestScrollingFrame(props, children) + props = join(props or {}, { + NextPageFunc = function() + return "foo" + end + }) + + return Roact.createElement(MockWrapper, {}, { + ScrollingFrame = Roact.createElement(InfiniteScrollingFrame, props, children) + }) + end + + it("should create and destroy without errors", function() + local element = createTestScrollingFrame() + local instance = Roact.mount(element) + Roact.unmount(instance) + end) + + it("should render correctly", function() + local container = Instance.new("Folder") + local instance = Roact.mount(createTestScrollingFrame(), container) + local frame = container:FindFirstChildOfClass("Frame") + + expect(frame).to.be.ok() + expect(frame.ScrollBarBackground).to.be.ok() + expect(frame.ScrollingFrame).to.be.ok() + + Roact.unmount(instance) + end) + + it("should render its children in the ScrollingFrame", function() + local container = Instance.new("Folder") + local instance = Roact.mount(createTestScrollingFrame({}, { + ChildFrame = Roact.createElement("Frame"), + }), container) + local frame = container:FindFirstChildOfClass("Frame") + + expect(frame).to.be.ok() + expect(frame.ScrollBarBackground).to.be.ok() + expect(frame.ScrollingFrame).to.be.ok() + expect(frame.ScrollingFrame.ChildFrame).to.be.ok() + + Roact.unmount(instance) + end) + + it("should add padding to both sides of the ScrollBar", function() + local container = Instance.new("Folder") + local instance = Roact.mount(createTestScrollingFrame({ + ScrollBarPadding = 2, + ScrollBarThickness = 8, + }), container) + local frame = container:FindFirstChildOfClass("Frame") + + expect(frame).to.be.ok() + expect(frame.ScrollBarBackground).to.be.ok() + expect(frame.ScrollBarBackground.Size.X.Offset).to.equal(12) + + Roact.unmount(instance) + end) +end \ No newline at end of file diff --git a/BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Components/LoadingBar.lua b/BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Components/LoadingBar.lua new file mode 100644 index 0000000000..08a000aba3 --- /dev/null +++ b/BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Components/LoadingBar.lua @@ -0,0 +1,115 @@ +--[[ + LoadingBar - creates a loading bar used for validation/publishing + + 1. loads to holdPercent + 2. waits for onFinish to be non-nil + 3. loads to 100% + 4. loads to 150% (so the user can see the finished loading bar for a short delay) + + Necessary Props: + string LoadingText - the loading bar text + number HoldPercent [0, 1] - percentage to wait at + number LoadingTime - total time it takes to load without waiting for onFinish + bool InstallationFinished - indicates whether or not the installation has fininshed. +]] + +local Library = script.Parent.Parent +local Roact = require(Library.Parent.Parent.Roact) + +local RunService = game:GetService("RunService") + +local RoundFrame = require(Library.Components.RoundFrame) + +local LOADING_TITLE_HEIGHT = 20 +local LOADING_TITLE_PADDING = 10 + +local Theming = require(Library.Theming) +local withTheme = Theming.withTheme + +local LoadingBar = Roact.Component:extend("LoadingBar") + +function LoadingBar:init(props) + self:setState({ + progress = 0, + time = 0, + }) +end + +function LoadingBar:loadUntil(percent) + while self.state.progress < percent do + local dt = RunService.RenderStepped:Wait() + if not self.isMounted then + break + end + local newTime = self.state.time + dt + self:setState({ + time = newTime, + progress = newTime/self.props.LoadingTime + }) + end +end + +function LoadingBar:didMount() + self.isMounted = true + spawn(function() + -- go to 92% + self:loadUntil(self.props.HoldPercent) + + -- wait until props.onFinish + while self.isMounted and not self.props.InstallationFinished do + RunService.RenderStepped:Wait() + end + + -- go to 100% + self:loadUntil(1) + + -- wait for a moment to show "full loading screen" + self:loadUntil(1.5) + end) +end + +function LoadingBar:willUnmount() + self.isMounted = false +end + +function LoadingBar:render() + return withTheme(function(theme) + local props = self.props + local state = self.state + + local progress = math.min(math.max(state.progress, 0), 1) + local loadingText = props.LoadingText .. " ( " .. math.floor((progress * 100) + 0.5) .. "% )" + + return Roact.createElement("Frame", { + BackgroundTransparency = 1, + Size = props.Size, + Position = props.Position, + }, { + LoadingTitle = Roact.createElement("TextLabel", { + BackgroundTransparency = 1, + Font = theme.loadingBar.font, + Position = UDim2.new(0, 0, 0, -(LOADING_TITLE_HEIGHT + LOADING_TITLE_PADDING)), + Size = UDim2.new(1, 0, 0, LOADING_TITLE_HEIGHT), + Text = loadingText, + TextColor3 = theme.loadingBar.text, + TextSize = theme.loadingBar.fontSize, + TextXAlignment = Enum.TextXAlignment.Center, + TextYAlignment = Enum.TextYAlignment.Center, + }), + + LoadingBackgroundBar = Roact.createElement(RoundFrame, { + BorderSizePixel = 0, + BackgroundColor3 = theme.loadingBar.bar.backgroundColor, + Size = UDim2.new(1, 0, 1, 0), + }, { + LoadingBar = Roact.createElement(RoundFrame, { + BorderSizePixel = 0, + BackgroundColor3 = theme.loadingBar.bar.foregroundColor, + Size = UDim2.new(progress, 0, 1, 0), + }), + }), + }) + end) +end + +return LoadingBar \ No newline at end of file diff --git a/BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Components/LoadingIndicator.lua b/BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Components/LoadingIndicator.lua new file mode 100644 index 0000000000..e75ebd6901 --- /dev/null +++ b/BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Components/LoadingIndicator.lua @@ -0,0 +1,139 @@ +--[[ + Loading indicator + + Props: + Vector2 AnchorPoint = Vector2.new(0, 0) + UDim2 Position = UDim2.new(0, 0, 0, 0) + UDim2 Size = UDim2.new(0, 92, 0, 24) + number ZIndex = 0 + boolean Visible = true + number Count = 3 : number of blocks in loading animation + number GapRatio = 1.5 : sets gap between blocks + number EndRatio = 0.25 : used for calculating block width +]] + +local Library = script.Parent.Parent +local Roact = require(Library.Parent.Parent.Roact) + +local Theming = require(Library.Theming) +local withTheme = Theming.withTheme + +local RunService = game:GetService("RunService") + +local LoadingIndicator = Roact.PureComponent:extend("LoadingIndicator") + +local ANIMATION_SPEED = 5 +local DEFAULT_BLOCK_COUNT = 3 + +function LoadingIndicator:init() + self.state = { + animationTime = math.pi / 2, + sinTime = 1, + direction = 1, + index = 1, + } + + self.animationConnection = RunService.RenderStepped:connect(function(deltaTime) + self:updateAnimation(deltaTime) + end) +end + +function LoadingIndicator:willUnmount() + if self.animationConnection then + self.animationConnection:Disconnect() + end +end + +function LoadingIndicator:updateAnimation(deltaTime) + self:setState(function(prevState, props) + local newAnimationTime = prevState.animationTime + deltaTime + local newSinTime = math.sin(newAnimationTime * ANIMATION_SPEED) + + local direction = prevState.direction + local newDirection = direction + local newIndex = prevState.index + + -- If sin has changed sign, move to the next block + if (direction > 0 and newSinTime < 0) or (direction < 0 and newSinTime > 0) then + newDirection = -direction + newIndex = newIndex + 1 + + if newIndex > (self.props.count or DEFAULT_BLOCK_COUNT) then + newIndex = 1 + end + end + + return { + animationTime = newAnimationTime, + sinTime = newSinTime, + direction = newDirection, + index = newIndex, + } + end) +end + +function LoadingIndicator:render() + return withTheme(function(theme) + local props = self.props + local state = self.state + + local loadingIndicatorTheme = theme.loadingIndicator + + local baseColor = loadingIndicatorTheme.baseColor + local endColor = loadingIndicatorTheme.endColor + + local anchorPoint = props.AnchorPoint or Vector2.new(0, 0) + local position = props.Position or UDim2.new(0, 0, 0, 0) + local size = props.Size or UDim2.new(0, 92, 0, 24) + local zindex = props.ZIndex or 0 + local visible = (props.Visible ~= nil and props.Visible) or (props.Visible == nil) + + local blockCount = props.Count or DEFAULT_BLOCK_COUNT + + local gapBetweenBlockRatio = props.GapRatio or 1.5 + local endRatio = props.EndRatio or 0.25 + + local blockWidth = 1 / (blockCount + (blockCount * gapBetweenBlockRatio) - gapBetweenBlockRatio + (2 * endRatio)) + local gapWidth = blockWidth * gapBetweenBlockRatio + + local smallHeight = 0.6 + + local children = { + UIListLayout = Roact.createElement("UIListLayout", { + SortOrder = Enum.SortOrder.LayoutOrder, + FillDirection = Enum.FillDirection.Horizontal, + Padding = UDim.new(gapWidth, 0), + HorizontalAlignment = Enum.HorizontalAlignment.Center, + VerticalAlignment = Enum.VerticalAlignment.Center, + }), + } + + local sinTime = math.abs(state.sinTime) + local index = state.index + + for i = 1, blockCount, 1 do + local height = i == index and smallHeight + ((1 - smallHeight) * sinTime) or smallHeight + + local color = i == index and baseColor:lerp(endColor, sinTime) or baseColor + + children["Frame" .. i] = Roact.createElement("Frame", { + Size = UDim2.new(blockWidth, 0, height, 0), + LayoutOrder = i, + BorderSizePixel = 0, + BackgroundColor3 = color, + }) + end + + return Roact.createElement("Frame", { + AnchorPoint = anchorPoint, + Position = position, + Size = size, + ZIndex = zindex, + BorderSizePixel = 0, + Visible = visible, + BackgroundTransparency = 1, + }, children) + end) +end + +return LoadingIndicator diff --git a/BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Components/LoadingIndicator.spec.lua b/BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Components/LoadingIndicator.spec.lua new file mode 100644 index 0000000000..3a99ec2859 --- /dev/null +++ b/BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Components/LoadingIndicator.spec.lua @@ -0,0 +1,16 @@ +return function() + local Library = script.Parent.Parent + local Roact = require(Library.Parent.Parent.Roact) + + local MockWrapper = require(Library.MockWrapper) + + local LoadingIndicator = require(script.Parent.LoadingIndicator) + + it("should create and destroy without errors", function() + local element = Roact.createElement(MockWrapper, {}, { + LoadingIndicator = Roact.createElement(LoadingIndicator), + }) + local instance = Roact.mount(element) + Roact.unmount(instance) + end) +end diff --git a/BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Components/MultilineTextEntry.lua b/BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Components/MultilineTextEntry.lua new file mode 100644 index 0000000000..aa221dcf69 --- /dev/null +++ b/BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Components/MultilineTextEntry.lua @@ -0,0 +1,143 @@ +--[[ + A multiline text entry with a dynmically appearing scrollbar. + Used in a RoundTextBox when Multiline is true. + + Props: + string Text = The text to display + bool Visible = Whether to display this component + int TextSize = The size of text + function SetText(text) = Callback to tell parent that text has changed + function FocusChanged(focus) = Callback to tell parent that this component has focus + function HoverChanged(hovered) = Callback when the mouse enters or leaves this component. +]] + +local TextService = game:GetService("TextService") + +local Library = script.Parent.Parent +local Roact = require(Library.Parent.Parent.Roact) + +local StyledScrollingFrame = require(Library.Components.StyledScrollingFrame) + +local SCROLL_BAR_OUTSET = 9 + +local MultilineTextEntry = Roact.PureComponent:extend("MultilineTextEntry") + +function MultilineTextEntry:init() + self.frameRef = Roact.createRef() + self.textBoxRef = Roact.createRef() + self.textConnections = nil + + -- TODO: Get rid of function and replace with API call CLIPLAYEREX-2806 when it ships + self.getPositionAtIndex = function(index) + local frame = self.frameRef.current + local sizeX = frame.AbsoluteSize.x - SCROLL_BAR_OUTSET + local textSize = TextService:GetTextSize( + string.sub(self.props.Text, 0, index), + self.props.TextSize, + Enum.Font.SourceSans, + Vector2.new(sizeX, math.huge) + ) + return textSize + end + + self.updateCanvas = function() + local frame = self.frameRef.current + local sizeX = frame.AbsoluteSize.x - SCROLL_BAR_OUTSET + local textBox = self.textBoxRef.current + local textSize = TextService:GetTextSize( + self.props.Text, + self.props.TextSize, + Enum.Font.SourceSans, + Vector2.new(sizeX, math.huge) + ) + frame.CanvasSize = UDim2.new(0, 0, 0, textSize.y) + frame.CanvasPosition = Vector2.new(0, self.getPositionAtIndex(textBox.CursorPosition).y - 2 * self.props.TextSize) + end + + self.textChanged = function(rbx) + if rbx.Text ~= self.props.Text then + self.props.SetText(rbx.Text) + end + end + + self.mouseEnter = function() + self.props.HoverChanged(true) + end + self.mouseLeave = function() + self.props.HoverChanged(false) + end +end + +function MultilineTextEntry:didMount() + local textBox = self.textBoxRef.current + local frame = self.frameRef.current + self.textConnections = { + textBox:GetPropertyChangedSignal("Text"):connect(self.updateCanvas), + frame:GetPropertyChangedSignal("AbsoluteSize"):connect(self.updateCanvas), + } + self.updateCanvas() +end + +function MultilineTextEntry:willUnmount() + for _, connection in ipairs(self.textConnections) do + connection:Disconnect() + end + self.textConnections = nil +end + +function MultilineTextEntry:render() + local visible = self.props.Visible + local text = self.props.Text + local textColor = self.props.TextColor3 + local textSize = self.props.TextSize + local font = self.props.Font + + + return Roact.createElement(StyledScrollingFrame, { + Size = UDim2.new(1, SCROLL_BAR_OUTSET, 1, 0), + BackgroundTransparency = 1, + ClipsDescendants = true, + ShowBackground = false, + + [Roact.Ref] = self.frameRef, + }, { + Padding = Roact.createElement("UIPadding", { + PaddingRight = UDim.new(0, SCROLL_BAR_OUTSET), + }), + + Text = Roact.createElement("TextBox", { + Visible = visible, + MultiLine = true, + TextWrapped = true, + + Size = UDim2.new(1, 0, 1, 0), + BackgroundTransparency = 1, + BorderSizePixel = 0, + + ClearTextOnFocus = false, + Font = font, + TextSize = textSize, + TextXAlignment = Enum.TextXAlignment.Left, + TextYAlignment = Enum.TextYAlignment.Top, + TextColor3 = textColor, + Text = text, + + [Roact.Event.Focused] = function() + self.props.FocusChanged(true) + end, + + [Roact.Event.FocusLost] = function() + self.props.FocusChanged(false) + end, + + [Roact.Event.MouseEnter] = self.mouseEnter, + [Roact.Event.MouseLeave] = self.mouseLeave, + + [Roact.Change.Text] = self.textChanged, + + [Roact.Ref] = self.textBoxRef, + }), + }) +end + +return MultilineTextEntry diff --git a/BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Components/MultilineTextEntry.spec.lua b/BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Components/MultilineTextEntry.spec.lua new file mode 100644 index 0000000000..30bf91066b --- /dev/null +++ b/BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Components/MultilineTextEntry.spec.lua @@ -0,0 +1,47 @@ +return function() + local Library = script.Parent.Parent + local Roact = require(Library.Parent.Parent.Roact) + + local MockWrapper = require(Library.MockWrapper) + + local MultilineTextEntry = require(script.Parent.MultilineTextEntry) + + local function createTestMultilineTextEntry(visible) + return Roact.createElement(MockWrapper, {}, { + MultilineTextEntry = Roact.createElement(MultilineTextEntry, { + Text = "Text", + Visible = visible, + TextSize = 22, + }), + }) + end + + it("should create and destroy without errors", function() + local element = createTestMultilineTextEntry(true) + local instance = Roact.mount(element) + Roact.unmount(instance) + end) + + it("should render correctly", function () + local container = Instance.new("Folder") + local instance = Roact.mount(createTestMultilineTextEntry(true), container) + local frame = container:FindFirstChildOfClass("Frame") + + expect(frame.ScrollingFrame).to.be.ok() + expect(frame.ScrollingFrame).to.be.ok() + expect(frame.ScrollingFrame.Padding).to.be.ok() + expect(frame.ScrollingFrame.Text).to.be.ok() + + Roact.unmount(instance) + end) + + it("should hide its text when not visible", function () + local container = Instance.new("Folder") + local instance = Roact.mount(createTestMultilineTextEntry(false), container) + local frame = container:FindFirstChildOfClass("Frame") + + expect(frame.ScrollingFrame.Text.Visible).to.equal(false) + + Roact.unmount(instance) + end) +end \ No newline at end of file diff --git a/BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Components/PluginWidget/Dialog.lua b/BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Components/PluginWidget/Dialog.lua new file mode 100644 index 0000000000..9e264c33c7 --- /dev/null +++ b/BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Components/PluginWidget/Dialog.lua @@ -0,0 +1,93 @@ +--[[ + Creates a QWidgetPluginGui dialog. + + Props: + table Options = An options table to pass to the Create function. + + bool Enabled = Whether the dialog is currently enabled. + string Title = The title to display at the top of the dialog. + string Name = The name of the dialog. + ZIndexBehavior ZIndexBehavior = The ordering behavior of elements + in the dialog based on ZIndex. + + function OnClose() = A callback for when the dialog closes. +]] + +local Library = script.Parent.Parent.Parent + +local HttpService = game:GetService("HttpService") + +local Plugin = require(Library.Plugin) +local getPlugin = Plugin.getPlugin +local Roact = require(Library.Parent.Parent.Roact) + +local Focus = require(Library.Focus) +local FocusProvider = Focus.Provider + +local Dialog = Roact.PureComponent:extend("Dialog") + +function Dialog:init(props) + local options = props.Options + local title = props.Title or "" + local name = props.Name or title + local id = title .. HttpService:GenerateGUID() + local zIndexBehavior = props.ZIndexBehavior or Enum.ZIndexBehavior.Sibling + + local plugin = getPlugin(self) + local widget = plugin:CreateQWidgetPluginGui(id, options) + widget.Name = name + widget.ZIndexBehavior = zIndexBehavior + self.widget = widget + + if props.OnClose and widget:IsA("PluginGui") then + widget:BindToClose(function() + props.OnClose() + end) + end +end + +function Dialog:updateWidget() + local props = self.props + local enabled = props.Enabled + local title = props.Title + + local widget = self.widget + if widget then + if enabled ~= nil then + widget.Enabled = enabled + end + + if title ~= nil and widget:IsA("PluginGui") then + widget.Title = title + end + end +end + +function Dialog:didMount() + self:updateWidget() +end + +function Dialog:didUpdate() + self:updateWidget() +end + +function Dialog:render() + return self.widget.Enabled and Roact.createElement(Roact.Portal, { + target = self.widget, + }, { + FocusProvider = Roact.createElement(FocusProvider, { + pluginGui = self.widget, + }, self.props[Roact.Children]), + }) +end + +function Dialog:willUnmount() + if self.changedConnection then + self.changedConnection:Disconnect() + end + if self.widget then + self.widget:Destroy() + end +end + +return Dialog \ No newline at end of file diff --git a/BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Components/Preview/ActionBar.lua b/BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Components/Preview/ActionBar.lua new file mode 100644 index 0000000000..63f1b0995a --- /dev/null +++ b/BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Components/Preview/ActionBar.lua @@ -0,0 +1,192 @@ +--[[ + This component is responsible for managing action bar, which provides two components. + Insert button and open more button. + + Necessary properties: + Position = UDim2 + Size = UDim2 + TryInsert = call back + Text = button text + Color = button color + + Optionlal properties: + LayoutOrder = num + AssetId = id, for analytics + InstallDisabled = true if we're a plugin and we are loading, disables install attempts while loading + DisplayResultOfInsertAttempt = if true, overwrites button color/text once you click it based on result of insert + ShowRobuxIcon = Whether to show a robux icon next to the text. +]] + +local Library = script.Parent.Parent.Parent + +local Roact = require(Library.Parent.Parent.Roact) + +local GetTextSize = require(Library.Utils.GetTextSize) + +local Theming = require(Library.Theming) +local withTheme = Theming.withTheme + +local RoundFrame = require(Library.Components.RoundFrame) + +local ActionBar = Roact.PureComponent:extend("ActionBar") + +local BUTTON_STATUS = { + default = 0, + hovered = 1, +} + + +function ActionBar:init(props) + self.state = { + insertButtonStatus = BUTTON_STATUS.default + } + + self.onInsertButtonEnter = function() + self:setState({ + insertButtonStatus = BUTTON_STATUS.hovered + }) + end + + self.onInsertButtonLeave = function() + self:setState({ + insertButtonStatus = BUTTON_STATUS.default + }) + end + + self.onShowMoreActiveted = function() + self.props.TryCreateContextMenu() + end + + self.onInsertActivated = function() + -- If we're working with a plugin, it might still be loading/already clicked and completed + -- In these cases, we do not want to allow an insert attempt + if self.props.InstallDisabled then + return + end + + self.props.TryInsert() + end +end + +function ActionBar:render() + return withTheme(function(theme) + local props = self.props + local size = props.Size + local position = props.Position + local anchorPoint = props.AnchorPoint + local showRobuxIcon = props.ShowRobuxIcon + local isDisabled = props.InstallDisabled + local layoutOrder = props.LayoutOrder + + local text = props.Text + + local actionBarTheme = theme.assetPreview.actionBar + + local color = actionBarTheme.button.backgroundColor + if isDisabled then + color = actionBarTheme.button.backgroundDisabledColor + elseif self.state.insertButtonStatus == BUTTON_STATUS.hovered then + color = actionBarTheme.button.backgroundHoveredColor + end + + local textColor = isDisabled and actionBarTheme.text.colorDisabled or actionBarTheme.text.color + local textWidth = GetTextSize(text, theme.assetPreview.textSizeLarge, theme.assetPreview.fontBold).X + + local padding = -(actionBarTheme.padding * 2 + actionBarTheme.centerPadding) + + return Roact.createElement("Frame", { + Size = size, + Position = position, + AnchorPoint = anchorPoint, + + BackgroundTransparency = 0, + BackgroundColor3 = actionBarTheme.background, + BorderSizePixel = 0, + + LayoutOrder = layoutOrder, + },{ + UIPadding = Roact.createElement("UIPadding", { + PaddingBottom = UDim.new(0, 12), + PaddingLeft = UDim.new(0, 12), + PaddingRight = UDim.new(0, 12), + PaddingTop = UDim.new(0, 12), + }), + + UIListLayout = Roact.createElement("UIListLayout", { + FillDirection = Enum.FillDirection.Horizontal, + HorizontalAlignment = Enum.HorizontalAlignment.Center, + VerticalAlignment = Enum.VerticalAlignment.Center, + SortOrder = Enum.SortOrder.LayoutOrder, + Padding = UDim.new(0, 10), + }), + + ShowMoreButton = Roact.createElement(RoundFrame, { + Size = UDim2.new(0, 28, 0, 28), + + BackgroundColor3 = actionBarTheme.showMore.backgroundColor, + BackgroundTransparency = 0, + BorderSizePixel = 1, + BorderColor3 = actionBarTheme.showMore.borderColor, + + OnActivated = self.onShowMoreActiveted, + + LayoutOrder = 1, + }, { + ShowMoreImageLabel = Roact.createElement("ImageLabel", { + AnchorPoint = Vector2.new(0.5, 0.5), + Position = UDim2.new(0.5, 0, 0.5, 0), + Size = UDim2.new(0, 16, 0, 16), + + Image = actionBarTheme.images.showMore, + BackgroundTransparency = 1, + }) + }), + + InsertButton = Roact.createElement(RoundFrame, { + Size = UDim2.new(1, padding, 1, 0), + BackgroundColor3 = color, + BackgroundTransparency = 0, + BorderSizePixel = 0, + + OnActivated = self.onInsertActivated, + OnMouseEnter = self.onInsertButtonEnter, + OnMouseLeave = self.onInsertButtonLeave, + + LayoutOrder = 2, + }, { + Layout = Roact.createElement("UIListLayout", { + SortOrder = Enum.SortOrder.LayoutOrder, + FillDirection = Enum.FillDirection.Horizontal, + VerticalAlignment = Enum.VerticalAlignment.Center, + HorizontalAlignment = Enum.HorizontalAlignment.Center, + Padding = UDim.new(0, 2) + }), + + Icon = showRobuxIcon and Roact.createElement("ImageLabel", { + LayoutOrder = 1, + Size = actionBarTheme.robuxSize, + BackgroundTransparency = 1, + Image = actionBarTheme.images.robuxSmall, + ImageColor3 = actionBarTheme.images.colorWhite, + }), + + InsertTextLabel = Roact.createElement("TextLabel", { + LayoutOrder = 2, + Size = UDim2.new(0, textWidth, 1, 0), + + Text = text, + Font = theme.assetPreview.fontBold, + TextSize = theme.assetPreview.textSizeMedium, + TextColor3 = textColor, + TextXAlignment = Enum.TextXAlignment.Center, + TextYAlignment = Enum.TextYAlignment.Center, + + BackgroundTransparency = 1, + BorderSizePixel = 0, + }), + }), + }) + end) +end + +return ActionBar \ No newline at end of file diff --git a/BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Components/Preview/ActionBar.spec.lua b/BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Components/Preview/ActionBar.spec.lua new file mode 100644 index 0000000000..5b94387e48 --- /dev/null +++ b/BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Components/Preview/ActionBar.spec.lua @@ -0,0 +1,26 @@ +return function() + local Library = script.Parent.Parent.Parent + local Roact = require(Library.Parent.Parent.Roact) + + local MockWrapper = require(Library.MockWrapper) + + local ActionBar = require(Library.Components.Preview.ActionBar) + + local function createTestAsset(container, name) + local element = Roact.createElement(MockWrapper, {}, { + Roact.createElement(ActionBar, { + Size = UDim2.new(1, 0, 1, 0), + Position = UDim2.new(0, 0, 0, 0), + AnchorPoint = Vector2.new(0, 1), + Text = "foo", + }) + }) + + return Roact.mount(element, container or nil, name or "") + end + + it("should create and destroy without errors", function() + local instance = createTestAsset() + Roact.unmount(instance) + end) +end \ No newline at end of file diff --git a/BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Components/Preview/AssetDescription.lua b/BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Components/Preview/AssetDescription.lua new file mode 100644 index 0000000000..c5344e5c6e --- /dev/null +++ b/BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Components/Preview/AssetDescription.lua @@ -0,0 +1,98 @@ +--[[ + This component handles the description rows of the assetPreview component. + It's in charge of showing category name in the left and content in the right. + + Required Properties: + Position = UDim2 + LeftContent = string, the name for the category. + RightContent = string, the name of the category. + + Optional Properties: + UseBoldLine = bool, decide if we bold the underlying line or not. + HideSeparator = bool, whether or not to hide the separator after the component + LayoutOrder = num +]] + +local Library = script.Parent.Parent.Parent + +local Roact = require(Library.Parent.Parent.Roact) +local join = require(Library.join) + +local Theming = require(Library.Theming) +local withTheme = Theming.withTheme + +local AssetDescription = Roact.PureComponent:extend("AssetDescription") + +function AssetDescription:render() + return withTheme(function(theme) + local props = self.props + local position = props.Position or UDim2.new(1, 0, 1, 0) + local leftContent = props.LeftContent or "" + local rightContent = props.RightContent or "" + + local useBoldLine = props.UseBoldLine or false + local hideSeparator = props.HideSeparator or false + + local descriptionTheme = theme.assetPreview.description + + local layoutOrder = props.LayoutOrder + + local children = join({ + -- Make sure left side and right side won't be cut off. + UIPadding = Roact.createElement("UIPadding", { + PaddingBottom = UDim.new(0, 0), + PaddingLeft = UDim.new(0, 1), + PaddingRight = UDim.new(0, 1), + PaddingTop = UDim.new(0, 0), + }), + + LeftContent = Roact.createElement("TextLabel", { + Size = UDim2.new(1, 0, 1, 0), + + Text = leftContent, + Font = theme.assetPreview.font, + TextColor3 = descriptionTheme.leftTextColor, + TextSize = theme.assetPreview.textSizeLarge, + TextXAlignment = Enum.TextXAlignment.Left, + + BackgroundTransparency = 1, + + AutoLocalize = false, + }), + + RightContent = Roact.createElement("TextLabel", { + Size = UDim2.new(1, 0, 1, 0), + + Text = rightContent, + Font = theme.assetPreview.font, + TextColor3 = descriptionTheme.rightTextColor, + TextSize = theme.assetPreview.textSizeLarge, + TextXAlignment = Enum.TextXAlignment.Right, + + BackgroundTransparency = 1, + + AutoLocalize = false, + }), + + ButtonLine = not hideSeparator and Roact.createElement("Frame", { + Position = UDim2.new(0, 0, 1, 3), + Size = UDim2.new(1, 0, 0, 1), + + BorderSizePixel = useBoldLine and 1 or 0, + BackgroundColor3 = descriptionTheme.lineColor, + BorderColor3 = descriptionTheme.lineColor, + }) + }, props[Roact.Children] or {}) + + return Roact.createElement("Frame", { + Position = position, + Size = UDim2.new(1, 0, 0, theme.assetPreview.description.height), + + BackgroundTransparency = 1, + BackgroundColor3 = descriptionTheme.background, + LayoutOrder = layoutOrder, + }, children) + end) +end + +return AssetDescription diff --git a/BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Components/Preview/AssetDescription.spec.lua b/BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Components/Preview/AssetDescription.spec.lua new file mode 100644 index 0000000000..2a4153e437 --- /dev/null +++ b/BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Components/Preview/AssetDescription.spec.lua @@ -0,0 +1,28 @@ +return function() + local Library = script.Parent.Parent.Parent + local Roact = require(Library.Parent.Parent.Roact) + + local MockWrapper = require(Library.MockWrapper) + + local AssetDescription = require(Library.Components.Preview.AssetDescription) + + local function createTestAsset(container, name) + local element = Roact.createElement(MockWrapper,{},{ + AssetDescription = Roact.createElement(AssetDescription, { + Position = UDim2.new(1, 0, 1, 0), + Size = UDim2.new(1, 0, 1, 0), + LeftContent = "", + RightContent = "", + + UseBoldLine = false, + }) + }) + + return Roact.mount(element, container or nil, name or "") + end + + it("should create and destroy without errors", function() + local instance = createTestAsset() + Roact.unmount(instance) + end) +end diff --git a/BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Components/Preview/AssetPreview.lua b/BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Components/Preview/AssetPreview.lua new file mode 100644 index 0000000000..69c9fcdbc6 --- /dev/null +++ b/BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Components/Preview/AssetPreview.lua @@ -0,0 +1,512 @@ +--[[ + AssetPreview component is responsible for manageing the models will be displaying on the + ViewPortFrame. + Models, detail information regarding the asset should be coming from the parent. Asnyc reqest for getting + the asset should also be done in the parent. + + Necessary property: + Position = UDim2, the position of Asset Preview with respect to it's parent. + AnchorPoint = Vector2, used to center Asset Preview with respect to it's parent. + MaxPreviewWidth = number, the maximum width allowed for this component. + MaxPreviewHeight = number, the maximum height allowed for this component. + + AssetData = table, a table contains asset data. + CurrentPreview = asset , the current asset displayed in AssetPreview both for 3D view and Tree View. + + ActionBarText = string, the text shown in the large button of the ActionBar. + TryInsert = callback, this determines the behavior of the large button in the ActionBar. + + OnFavoritedActivated = callback, this callback is invoked when the favorites button is clicked for the asset. + FavoriteCounts = number, the number of favorites that this asset has. + Favorited = boolean, whether or not the current user has this asset favorited. + + TryCreateContextMenu = callback, that creates a context menu in the triple dot (...) of the ActionBar. + OnTreeItemClicked = callback, that determines the behavior of when an item is clicked in the TreeView + The TreeView is a part of the PreviewController. + + Optional property: + InstallDisabled = boolean, used in PluginPurchaseFlow to disable the ActionBar install button + when the plugin is already installed. + PurchaseFlow = component, component which is the start of the PluginPurhaseFlow + SuccessDialog = component, success dialog shown at the end of the PluginPurchaseFlow + ShowRobuxIcon = boolean, to determine whether or not the Robux Icon should be shown in the ActionBar. + ShowInstallationBar = boolean, determines if the installation bar should be shown, this is used in PluginPurchaseFlow + LoadingBarText = string, the text that should be displayed with the loading/installation bar. + + HasRating = boolean, determines whether or not Voting and Favorites should be displayed. + Voting = table, table of voting information structed as: + { + UpVotes = number, + DownVotes = number, + } + OnVoteUp = callback, to be invoked when the vote up button is clicked in the Vote component. + OnVoteDown = callback, to be invoked when the vote down button is clicked in the Vote component. + + SearchByCreator = callback, to search for asset in the current Marketplace category + that are created by the same creator as current asset. + + ZIndex = num, used to override the zIndex depth of the base button. +]] + +local RunService = game:GetService("RunService") +local StudioService = game:GetService("StudioService") + +local Library = script.Parent.Parent.Parent + +local Roact = require(Library.Parent.Parent.Roact) + +local Favorites = require(Library.Components.Preview.Favorites) +local PreviewController = require(Library.Components.Preview.PreviewController) +local Vote = require(Library.Components.Preview.Vote) +local ActionBar = require(Library.Components.Preview.ActionBar) +local AssetDescription = require(Library.Components.Preview.AssetDescription) +local LoadingBar = require(Library.Components.LoadingBar) +local SearchLinkText = require(Library.Components.Preview.SearchLinkText) + +local LayoutOrderIterator = require(Library.Utils.LayoutOrderIterator) +local GetTextSize = require(Library.Utils.GetTextSize) + +local Theming = require(Library.Theming) +local withTheme = Theming.withTheme + +local AssetType = require(Library.Utils.AssetType) + +local AssetPreview = Roact.PureComponent:extend("AssetPreview") + +-- TODO: Later, I will need to move all the unchanged numbers +-- into the constants. +local TITLE_HEIGHT = 18 + +local VERTICAL_PADDING = 10 +local TOP_PADDING = 12 +local BOTTOM_PADDING = 20 + +local VOTE_HEIGHT = 36 + +local ACTION_BAR_HEIGHT = 52 +local INSTALLATION_BAR_SECTION_HEIGHT = 80 +local INSTALLATION_BAR_SECTION_PADDING = 16 +local INSTALLATION_BAR_HEIGHT = 6 +local INSTALLATION_ANIMATION_TIME = 1.0 --seconds + + +-- Multiply minimum treeview width by 2 to get minimum threshold +-- When the asset preview is twice the minimum width, then we +-- can split the view in half to show the treeview on the right. +local TREEVIEW_ON_BOTTOM_WIDTH_THRESHOLD = 242 * 2 + +local function getGenreString(genreArray) + local arraySize = #genreArray + if arraySize == 0 then + return "All" + else + return tostring(genreArray[1]) + end +end + +function AssetPreview:init(props) + self.state = { + enableScroller = true, + overrideEnableVoting = false, + } + + self.assetSizeInited = false + + self.baseScrollRef = Roact.createRef() + self.baseLayouterRef = Roact.createRef() + + self.assetBaseButtonRef = Roact.createRef() + + self.onModelPreviewFrameEntered = function() + self:setState({ + enableScroller = false + }) + end + + self.onModelPreviewFrameLeft = function() + self:setState({ + enableScroller = true + }) + end + + -- For first time setting the canvas size. + self.onScrollContentSizeChange = function(rbx) + local baseScroller = self.baseScrollRef.current + local listLayouter = self.baseLayouterRef.current + local absSize = listLayouter and listLayouter.AbsoluteContentSize or Vector2.new() + if baseScroller and listLayouter then + baseScroller.CanvasSize = UDim2.new(1, 0, 0, absSize.Y + BOTTOM_PADDING) + end + + self:adjustAssetHeight() + end + + self.adjustAssetHeight = function() + -- Init the total height of asset preview component + local listLayouter = self.baseLayouterRef.current + local assetBaseButton = self.assetBaseButtonRef.current + if assetBaseButton then + local absSize = listLayouter and listLayouter.AbsoluteContentSize or Vector2.new() + local assetHeight = math.min(absSize.Y + ACTION_BAR_HEIGHT + BOTTOM_PADDING, self.props.MaxPreviewHeight) + assetBaseButton.Size = UDim2.new(0, self.props.MaxPreviewWidth, 0, assetHeight) + end + end + + self.searchByCreator = function() + local assetData = props.AssetData + local creator = assetData.Creator + local creatorName = creator.Name + if self.props.SearchByCreator then + self.props.SearchByCreator(creatorName) + end + end + if self.props.ClearPurchaseFlow then + self.props.ClearPurchaseFlow(props.AssetData.Asset.Id) + end +end + +function AssetPreview:didMount() + --[[ + FIXME (psewell) + THIS IS A HACK! ScrollingFrames can sometimes render the scroll bar in the + wrong place. Because of this, we have to hide the ScrollingFrame for a step + so that the scroll bar appears in the right place when we make it visible. + + This is a temporary fix recommended by PlayerEx. + There is a permanent fix on the way for this bug in C++. + See https://jira.rbx.com/browse/CLIPLAYEREX-2494 + We will enable the flag FFlagStudioRemoveToolboxScrollingFrameHack when the fix is done. + ]] + + local scrollingFrame = self.baseScrollRef.current + local baseButton = self.assetBaseButtonRef.current + if scrollingFrame and baseButton then + local stepConnection + stepConnection = RunService.Heartbeat:Connect(function() + scrollingFrame.Visible = true + stepConnection:Disconnect() + end) + end +end + +function AssetPreview:didUpdate() + self:adjustAssetHeight() +end + +function AssetPreview:render() + return withTheme(function(theme) + -- TODO: Time to tide up the properties passed from the asset. + local props = self.props + + local assetPreviewTheme = theme.assetPreview + + local maxPreviewWidth = props.MaxPreviewWidth + local maxPreviewHeight = props.MaxPreviewHeight + + local position = props.Position + local anchorPoint = props.AnchorPoint + + local assetData = props.AssetData + + -- Data structure from the server + local Asset = assetData.Asset + local assetId = Asset.Id + local assetName = Asset.Name or "Test Name" + local detailDescription = Asset.Description + local created = Asset.Created + local updated = Asset.Updated + local assetGenres = Asset.AssetGenres + + local creator = assetData.Creator + local creatorName = creator.Name + + local typeId = assetData.Asset.TypeId or Enum.AssetType.Model.Value + + local currentPreview = props.CurrentPreview + local previewModel = props.PreviewModel + + local assetPreviewType + if (typeId == Enum.AssetType.Plugin.Value) then + assetPreviewType = AssetType:markAsPlugin() + else + assetPreviewType = AssetType:getAssetType(currentPreview) + end + + local isPluginAsset, isPluginInstalled + isPluginAsset = AssetType:isPlugin(assetPreviewType) + isPluginInstalled = isPluginAsset and StudioService:IsPluginInstalled(assetId) + + local installDisabled = props.InstallDisabled + local showRobuxIcon = props.ShowRobuxIcon or false + local purchaseFlow = props.PurchaseFlow or nil + local successDialog = props.SuccessDialog or nil + local showInstallationBar = props.ShowInstallationBar or false + + local hasRating = props.HasRating or false + + local voting = props.Voting or {} + local upVoteRate = 0 + if voting.UpVotes and voting.DownVotes then + local totalVotes = voting.UpVotes + voting.DownVotes + if totalVotes > 0 then + upVoteRate = voting.UpVotes / totalVotes + end + end + local rating = upVoteRate * 100 + + local putTreeviewOnBottom = maxPreviewWidth <= TREEVIEW_ON_BOTTOM_WIDTH_THRESHOLD + + local assetSize = UDim2.new(0, maxPreviewWidth, 0, maxPreviewHeight) + + local zIndex = props.ZIndex or 0 + + local onTreeItemClicked = props.OnTreeItemClicked + + local tryCreateContextMenu = props.TryCreateContextMenu + + local enableScroller = self.state.enableScroller + + local detailDescriptionWidth = props.MaxPreviewWidth - 4 * assetPreviewTheme.padding - 2 + local textSize = GetTextSize(detailDescription, + assetPreviewTheme.textSizeLarge, + assetPreviewTheme.font, + Vector2.new(detailDescriptionWidth, 9000)) + local detailDescriptionHeight = textSize.y + VERTICAL_PADDING + + local layoutIndex = LayoutOrderIterator.new() + + local closeImageSize = UDim2.new(0, 28, 0, 28) + + return Roact.createElement("ImageButton", { + Position = position, + Size = assetSize, + AnchorPoint = anchorPoint, + + ZIndex = zIndex, + + BackgroundTransparency = 0, + BackgroundColor3 = assetPreviewTheme.background, + AutoButtonColor = false, + BorderSizePixel = 0, + + [Roact.Ref] = self.assetBaseButtonRef, + },{ + CloseImage = Roact.createElement("ImageLabel", { + Position = UDim2.new(1, 0, 0, 0), + Size = closeImageSize, + AnchorPoint = Vector2.new(0, 1), + + Image = assetPreviewTheme.images.deleteButton, + BackgroundTransparency = 1, + }), + + BaseScrollFrame = Roact.createElement("ScrollingFrame", { + Size = UDim2.new(1, assetPreviewTheme.padding, 1, -ACTION_BAR_HEIGHT), + Visible = true, --See comment in didMount + + ScrollBarThickness = 8, + ScrollBarImageColor3 = theme.scrollingFrame.scrollbarImageColor, + BorderSizePixel = 0, + BackgroundTransparency = 1, + TopImage = assetPreviewTheme.images.scrollbarTopImage, + MidImage = assetPreviewTheme.images.scrollbarMiddleImage, + BottomImage = assetPreviewTheme.images.scrollbarBottomImage, + ScrollingEnabled = enableScroller, + + [Roact.Ref] = self.baseScrollRef, + },{ + UIPadding = Roact.createElement("UIPadding", { + PaddingBottom = UDim.new(0, BOTTOM_PADDING), + PaddingLeft = UDim.new(0, assetPreviewTheme.padding), + PaddingRight = UDim.new(0, assetPreviewTheme.padding * 2), + PaddingTop = UDim.new(0, TOP_PADDING), + }), + + UIListLayout = Roact.createElement("UIListLayout", { + FillDirection = Enum.FillDirection.Vertical, + HorizontalAlignment = Enum.HorizontalAlignment.Center, + VerticalAlignment = Enum.VerticalAlignment.Top, + SortOrder = Enum.SortOrder.LayoutOrder, + Padding = UDim.new(0, VERTICAL_PADDING), + + [Roact.Change.AbsoluteContentSize] = self.onScrollContentSizeChange, + [Roact.Ref] = self.baseLayouterRef, + }), + + AssetName = Roact.createElement("TextLabel", { + Size = UDim2.new(1, 0, 0, TITLE_HEIGHT), + + Text = assetName, + Font = assetPreviewTheme.fontBold, + TextSize = assetPreviewTheme.textSizeTitle, + TextXAlignment = Enum.TextXAlignment.Left, + TextColor3 = assetPreviewTheme.assetNameColor, + BackgroundTransparency = 1, + TextTruncate = Enum.TextTruncate.AtEnd, + + AutoLocalize = false, + + LayoutOrder = layoutIndex:getNextOrder(), + }), + + Rating = hasRating and Roact.createElement("Frame", { + BackgroundTransparency = 1, + Size = UDim2.new(1, 0, 0, 12), + + LayoutOrder = layoutIndex:getNextOrder(), + }, { + VoteIcon = Roact.createElement("ImageLabel", { + Size = UDim2.new(0, 16, 0, 16), + BackgroundTransparency = 1, + Image = assetPreviewTheme.images.thumbUpSmall, + }), + + VoteText = Roact.createElement("TextLabel", { + Size = UDim2.new(1, 0, 1, 0), + Position = UDim2.new(0, 22, 0, 3), + BackgroundTransparency = 1, + + Text = ("%d%%"):format(rating), + TextXAlignment = Enum.TextXAlignment.Left, + TextSize = assetPreviewTheme.textSizeMedium, + Font = assetPreviewTheme.Font, + TextColor3 = theme.assetPreview.vote.textColor, + + LayoutOrder = 1, + }), + }), + + PreviewController = Roact.createElement(PreviewController, { + Width = assetPreviewTheme.padding * 2, + + CurrentPreview = currentPreview, + PreviewModel = previewModel, + AssetPreviewType = assetPreviewType, + AssetId = assetId, + PutTreeviewOnBottom = putTreeviewOnBottom, + + OnTreeItemClicked = onTreeItemClicked, + OnModelPreviewFrameEntered = self.onModelPreviewFrameEntered, + OnModelPreviewFrameLeft = self.onModelPreviewFrameLeft, + + LayoutOrder = layoutIndex:getNextOrder(), + }), + + LoadingIndicator = showInstallationBar and Roact.createElement("Frame", { + Size = UDim2.new(1, 0, 0, INSTALLATION_BAR_SECTION_HEIGHT), + BackgroundTransparency = 1, + LayoutOrder = layoutIndex:getNextOrder(), + }, { + UIPadding = Roact.createElement("UIPadding", { + PaddingBottom = UDim.new(0, 0), + PaddingLeft = UDim.new(0, INSTALLATION_BAR_SECTION_PADDING), + PaddingRight = UDim.new(0, INSTALLATION_BAR_SECTION_PADDING), + PaddingTop = UDim.new(0, (INSTALLATION_BAR_SECTION_HEIGHT * 0.5) + 10), + }), + + LoadingBar = Roact.createElement(LoadingBar, { + LoadingText = self.props.LoadingBarText, + Size = UDim2.new(1, 0, 0, INSTALLATION_BAR_HEIGHT), + HoldPercent = 0.92, + LoadingTime = INSTALLATION_ANIMATION_TIME, + InstallationFinished = isPluginInstalled, + }), + }), + + Favorites = Roact.createElement(Favorites, { + Size = UDim2.new(1, 0, 0, 20), + + FavoriteCounts = self.props.FavoriteCounts, + Favorited = self.props.Favorited, + + OnActivated = self.props.OnFavoritedActivated, + + LayoutOrder = layoutIndex:getNextOrder(), + }), + + DetailDescription = Roact.createElement("TextLabel", { + Size = UDim2.new(1, 0, 0, detailDescriptionHeight), + + BackgroundTransparency = 1, + TextWrapped = true, + + Text = detailDescription, + TextSize = assetPreviewTheme.textSizeLarge, + Font = assetPreviewTheme.font, + TextColor3 = assetPreviewTheme.descriptionTextColor, + TextXAlignment = Enum.TextXAlignment.Left, + + LayoutOrder = layoutIndex:getNextOrder(), + }), + + Vote = hasRating and Roact.createElement(Vote, { + Size = UDim2.new(1, 0, 0, VOTE_HEIGHT), + + Voting = voting, + AssetId = assetId, + + OnVoteUpButtonActivated = props.OnVoteUp, + OnVoteDownButtonActivated = props.OnVoteDown, + + LayoutOrder = layoutIndex:getNextOrder(), + }), + + Developer = Roact.createElement(AssetDescription, { + LeftContent = "Creator", + RightContent = "", + + LayoutOrder = layoutIndex:getNextOrder(), + }, { + LinkText = Roact.createElement(SearchLinkText, { + Text = creatorName, + Position = UDim2.fromScale(1, 0.5), + AnchorPoint = Vector2.new(1, 0.5), + OnClick = self.searchByCreator, + }) + }), + + Category = Roact.createElement(AssetDescription, { + LeftContent = "Genre", + RightContent = getGenreString(assetGenres), + + LayoutOrder = layoutIndex:getNextOrder(), + }), + + Created = Roact.createElement(AssetDescription, { + LeftContent = "Created", + RightContent = created, + + LayoutOrder = layoutIndex:getNextOrder(), + }), + + Updated = Roact.createElement(AssetDescription, { + LeftContent = "Last Updated", + RightContent = updated, + HideSeparator = true, + + LayoutOrder = layoutIndex:getNextOrder(), + }) + }), + + ActionBar = Roact.createElement(ActionBar, { + Text = self.props.ActionBarText, + Size = UDim2.new(1, 0, 0, ACTION_BAR_HEIGHT), + Position = UDim2.new(0, 0, 1, 0), + AnchorPoint = Vector2.new(0, 1), + AssetId = assetId, + + Asset = Asset, + TryInsert = self.props.TryInsert, + TryCreateContextMenu = tryCreateContextMenu, + InstallDisabled = installDisabled, + ShowRobuxIcon = showRobuxIcon, + }), + + PurchaseFlow = purchaseFlow, + + SuccessDialog = successDialog, + }) + end) +end + +return AssetPreview \ No newline at end of file diff --git a/BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Components/Preview/AssetPreview.spec.lua b/BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Components/Preview/AssetPreview.spec.lua new file mode 100644 index 0000000000..c684365bf8 --- /dev/null +++ b/BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Components/Preview/AssetPreview.spec.lua @@ -0,0 +1,71 @@ +return function() + local Library = script.Parent.Parent.Parent + local Roact = require(Library.Parent.Parent.Roact) + + local MockWrapper = require(Library.MockWrapper) + + local AssetPreview = require(Library.Components.Preview.AssetPreview) + + local function createTestAsset(container, name) + local testModel = Instance.new("Model") + + local assetData = { + Asset = { + Id = 123456, + Description = "This is a test asset", + Created = "", + Updated = "", + AssetGenres = {}, + }, + + Creator = { + Name = "Roblox Studio", + }, + } + + local element = Roact.createElement(MockWrapper, {}, { + AssetPreview = Roact.createElement(AssetPreview, { + Position = UDim2.new(0.5, 0, 0.5, 0), + AnchorPoint = Vector2.new(0.5, 0.5), + AssetData = assetData, + CurrentPreview = testModel, + + ShowInstallationBar = true, + InstallDisabled = false, + ShowRobuxIcon = true, + LoadingBarText = "Installing", + + FavoriteCounts = 1000, + Favorited = true, + OnFavoritedActivated = function() end, + + OnTreeItemClicked = function() end, + TryCreateContextMenu = function() end, + SearchByCreator = function() end, + + Voting = {}, + OnVoteUp = function() end, + OnVoteDown = function() end, + + ActionBarText = "Insert", + CanInsertAsset = true, + TryInsert = function() end, + + MaxPreviewWidth = 250, + MaxPreviewHeight = 400, + + ZIndex = 0, + + PurchaseFlow = Roact.createElement("Frame"), + SuccessDialog = Roact.createElement("Frame"), + }) + }) + + return Roact.mount(element, container or nil, name or "") + end + + it("should create and destroy without errors", function() + local instance = createTestAsset() + Roact.unmount(instance) + end) +end \ No newline at end of file diff --git a/BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Components/Preview/AudioControl.lua b/BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Components/Preview/AudioControl.lua new file mode 100644 index 0000000000..42617ae0a1 --- /dev/null +++ b/BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Components/Preview/AudioControl.lua @@ -0,0 +1,137 @@ +--[[ + Audio control is a piece of control panel we used in asset preview to provide play, + pause function for giving assetId. And a time label to show time left. + Start time counter when it's playing. + + Necessary properties: + UDim2 position + UDim2 size + number audioControlOffset, used to control the position of the audio control depending if we show tree view. + number timeLength, length got from the sound instance. + bool isPlaying, come from audio preview, used to change the button control. + number timePassed, audio preview know's the time length, is suited to calculate this. + + function onResume, accept an assetId. + function onPause, pause + function onPlay, This one will reset time length and time remaining. + + the sound object inside the Toolbox plugin to play. We don't want to too many sound source. +]] +local FFlagEnableToolboxVideos = game:GetFastFlag("EnableToolboxVideos") + +local Library = script.Parent.Parent.Parent +local Roact = require(Library.Parent.Parent.Roact) + +local Theming = require(Library.Theming) +local withTheme = Theming.withTheme + +local getTimeString = require(Library.Utils.getTimeString) +local RoundButton = require(Library.Components.RoundFrame) + +local LoadingIndicator = require(Library.Components.LoadingIndicator) +local TIME_LABEL_HEIGHT = 15 +local BUTTON_SIZE = 28 + +local AudioControl = Roact.PureComponent:extend("AudioControl") +if FFlagEnableToolboxVideos then + return AudioControl +end +function AudioControl:init(props) + self.state = { + init = false; + } + + self.onActivated = function() + if not self.props.isLoaded then + return + end + if self.props.isPlaying then + self.pauseASound() + else + self.startPlaying() + end + end + + self.startPlaying = function() + if self.state.init then + props.onResume() + else + -- Will update the time length and time remaining. + props.onPlay() + self:setState({ + init = true + }) + end + end + + self.pauseASound = function() + props.onPause() + end +end + +function AudioControl:render() + return withTheme(function(theme) + local props = self.props + local size = props.size + local anchorPoint = props.anchorPoint + local position = props.position + local timeLength = props.timeLength + local audioPreviewTheme = theme.assetPreview.audioPreview + local audioControlOffset = props.audioControlOffset + local isPlaying = props.isPlaying + local isLoaded = props.isLoaded + + local timePassed = props.timePassed + local timeString = getTimeString(timePassed) .. '/' .. getTimeString(timeLength) + + return Roact.createElement("Frame", { + AnchorPoint = anchorPoint, + BackgroundTransparency = 1, + Position = position, + Size = size, + }, { + Button = Roact.createElement(RoundButton, { + AnchorPoint = Vector2.new(0.5, 0), + AutoButtonColor = false, + BackgroundColor3 = isLoaded and audioPreviewTheme.buttonBackgroundColor or audioPreviewTheme.buttonDisabledBackgroundColor, + BackgroundTransparency = isLoaded and 0 or audioPreviewTheme.buttonDisabledBackgroundTransparency, + BorderSizePixel = 0, + Position = UDim2.new(0, 24, 0, 0), + Size = UDim2.new(0, BUTTON_SIZE, 0, BUTTON_SIZE), + + OnActivated = self.onActivated, + }, { + PlayOrPauseIcon = Roact.createElement("ImageLabel", { + AnchorPoint = Vector2.new(0.5, 0.5), + BackgroundTransparency = 1, + Image = isPlaying and audioPreviewTheme.pauseButton or audioPreviewTheme.playButton, + ImageColor3 = audioPreviewTheme.buttonColor, + ImageTransparency = isLoaded and 0 or audioPreviewTheme.buttonDisabledBackgroundTransparency, + Position = UDim2.new(0.5, 0, 0.5, 0), + Size = UDim2.new(1, 0, 1, 0), + }), + }), + + TimeComponent = isLoaded and Roact.createElement("TextLabel", { + AnchorPoint = Vector2.new(1, 0.5), + Position = UDim2.new(1, -audioControlOffset, 0.5, 0), + Size = UDim2.new(0, 204, 0, TIME_LABEL_HEIGHT), + BorderSizePixel = 0, + BackgroundTransparency = 1, + Text = timeString, + Font = audioPreviewTheme.font, + TextSize = audioPreviewTheme.fontSize, + TextXAlignment = Enum.TextXAlignment.Right, + TextColor3 = audioPreviewTheme.textColor, + }), + + LoadingIndicator = (not isLoaded) and Roact.createElement(LoadingIndicator, { + AnchorPoint = Vector2.new(1, 0.5), + Position = UDim2.new(1, -audioControlOffset, 0.5, 0), + Size = UDim2.new(0, 50, 0, TIME_LABEL_HEIGHT), + }), + }) + end) +end + +return AudioControl \ No newline at end of file diff --git a/BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Components/Preview/AudioControl.spec.lua b/BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Components/Preview/AudioControl.spec.lua new file mode 100644 index 0000000000..4d3945b0f9 --- /dev/null +++ b/BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Components/Preview/AudioControl.spec.lua @@ -0,0 +1,41 @@ +return function() + local FFlagEnableToolboxVideos = game:GetFastFlag("EnableToolboxVideos") + if FFlagEnableToolboxVideos then + return + end + + local Library = script.Parent.Parent.Parent + local Roact = require(Library.Parent.Parent.Roact) + + local MockWrapper = require(Library.MockWrapper) + + local AudioControl = require(Library.Components.Preview.AudioControl) + + local function createTestAsset(container, name) + local emptyFunc = function() + end + + local element = Roact.createElement(MockWrapper, {}, { + Roact.createElement(AudioControl, { + position = UDim2.new(0, 0, 0, 0), + size = UDim2.new(1, 0, 1, 0), + audioControlOffset = 30, + assetId = 0, + timeLength = 1, + isPlaying = false, + isLoaded = true, + timePassed = 0, + onResume = emptyFunc, + onPause = emptyFunc, + onPlay = emptyFunc, + }) + }) + + return Roact.mount(element, container or nil, name or "") + end + + it("should create and destroy without errors", function() + local instance = createTestAsset() + Roact.unmount(instance) + end) +end \ No newline at end of file diff --git a/BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Components/Preview/AudioPreview.lua b/BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Components/Preview/AudioPreview.lua new file mode 100644 index 0000000000..8eb1c5239b --- /dev/null +++ b/BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Components/Preview/AudioPreview.lua @@ -0,0 +1,356 @@ +--[[ + This component will be used in the asset preview for audio asset. + It will provide a still image for sound asset and a progress bar. + The progress bar will keep moving if the sound is playing. + + Necessary properties: + number SoundId, used for play and pause the sound + bool ShowTreeView, used to adjust time label component for audio control based on if we are + showing tree view button or not. + + Optional properties: + UDim2 position, default to UDim2(0, 0, 0, 0) + UDim2 size, default to UDim2(1, 0, 1, 0) + number layoutOrder, used by the layouter to change the position of the component + callBack ReportPlay, analytics events. + callback ReportPause, + + Props automatically received from wrapMedia(): + number _CurrentTime: The time in seconds that the media's TimePosition should currently be. + boolean _IsPlaying: Whether or not the Sound or VideoFrame should be currently playing. + callback _MediaPlayingUpdateSignal: Called when the media's Changed event is fired. Sets the isPlaying state. + callback _OnMediaEnded: Called when the media's Ended event is fired. Resets the currentTime & stops playing. + callback _Pause: Called when clicking the pause button. + callback _Play: Called when clicking the play button. + callBack _SetCurrentTime: Called if the currentTime has been changed, such as when moving a progressbar slider. +]] +local FFlagHideOneChildTreeviewButton = game:GetFastFlag("HideOneChildTreeviewButton") +local FFlagEnableToolboxVideos = game:GetFastFlag("EnableToolboxVideos") + +local RunService = game:GetService("RunService") +local wrapMedia = require(script.Parent.wrapMedia) + +local Library = script.Parent.Parent.Parent +local Roact = require(Library.Parent.Parent.Roact) + +local Theming = require(Library.Theming) +local withTheme = Theming.withTheme + +local PluginContext = require(Library.Plugin) +local getPlugin = PluginContext.getPlugin + +local PROGRESS_BAR_HEIGHT = 6 +local AUDIO_CONTROL_HEIGHT = 35 +local AUDIO_CONTROL_WIDTH_OFFSET_WITH_TREE = 50 +local AUDIO_CONTROL_WIDTH_OFFSET_NO_TREE = 70 + +if FFlagHideOneChildTreeviewButton then + AUDIO_CONTROL_WIDTH_OFFSET_NO_TREE = 10 +end + +local AudioControl = FFlagEnableToolboxVideos and nil or require(Library.Components.Preview.AudioControl) +local MediaControl = require(Library.Components.Preview.MediaControl) + +local AudioPreview = Roact.PureComponent:extend("AudioPreview") + +AudioPreview.defaultProps = { + size = UDim2.new(1, 0, 1, 0), +} + +function AudioPreview:init(props) + local plugin = getPlugin(self) + self.soundRef = Roact.createRef() + + self.state = { + timeLength = 0, + isPlaying = FFlagEnableToolboxVideos and nil or false, + isLoaded = false, + currentTime = FFlagEnableToolboxVideos and nil or 0, + } + + self.playSound = function(assetId) + if FFlagEnableToolboxVideos then + return + end + local soundObj = self.soundRef.current + if soundObj then + soundObj.SoundId = self.props.SoundId + plugin:PlaySound(soundObj) + end + self:setState({ + isPlaying = true, + currentTime = 0, + }) + + if self.props.reportPlay then + self.props.ReportPlay() + end + end + + self.resumeSound = function(assetId) + if FFlagEnableToolboxVideos then + return + end + local soundObj = self.soundRef.current + if soundObj then + soundObj.SoundId = self.props.SoundId + plugin:ResumeSound(soundObj) + end + + self:setState({ + isPlaying = true, + timeLength = soundObj.TimeLength, + }) + + if self.props.reportPlay then + self.props.ReportPlay() + end + end + + self.pauseSound = function(assetId) + if FFlagEnableToolboxVideos then + return + end + local soundObj = self.soundRef.current + if soundObj then + plugin:PauseSound(soundObj) + end + + self:setState({ + isPlaying = false, + }) + + if self.props.ReportPause then + self.props.ReportPause() + end + end + + self.onSoundEnded = function(soundId) + if FFlagEnableToolboxVideos then + return + end + self:setState({ + isPlaying = false, + timeLength = 0, + currentTime = 0, + }) + end + + self.dispatchMediaPlayingUpdate = function(updateType) + local soundObj = self.soundRef.current + if not soundObj or not self.isMounted then + return + end + if updateType == "PLAY" then + soundObj.SoundId = self.props.SoundId + plugin:ResumeSound(soundObj) + if self.props.reportPlay then + self.props.ReportPlay() + end + elseif updateType == "PAUSE" then + plugin:PauseSound(soundObj) + if self.props.ReportPause then + self.props.ReportPause() + end + end + end + + self.onSoundChange = function(rbx, property) + local soundObj = self.soundRef.current + if not self.isMounted then + return + end + local isLoaded = soundObj and soundObj.IsLoaded + if property == "TimeLength" then + self:setState({ + isLoaded = isLoaded, + timeLength = soundObj.TimeLength, + }) + if FFlagEnableToolboxVideos then + self.props._SetTimeLength(soundObj.TimeLength) + end + elseif isLoaded ~= self.state.isLoaded then + self:setState({ + isLoaded = isLoaded, + }) + end + end + + self.getAudioLength = function() + local soundObj = self.soundRef.current + if soundObj then + return math.max(soundObj.TimeLength, 1) + end + end +end + +function AudioPreview:didMount() + self.isMounted = true + if FFlagEnableToolboxVideos then + self.mediaPlayingUpdateConnection = self.props._MediaPlayingUpdateSignal:connect(self.dispatchMediaPlayingUpdate) + else + local soundObj = self.soundRef.current + if soundObj then + soundObj.SoundId = self.props.SoundId + end + + self.runServiceConnection = RunService.RenderStepped:Connect(function(step) + if (not self.state.isPlaying) then + return + end + local state = self.state + local newTime = self.state.currentTime + step + + if newTime >= state.timeLength then + newTime = state.timeLength + end + + if self.isMounted then + self:setState({ + currentTime = newTime + }) + end + end) + end +end + +function AudioPreview:willUnmount() + self.isMounted = false + if FFlagEnableToolboxVideos then + if self.mediaPlayingUpdateConnection then + self.mediaPlayingUpdateConnection:disconnect() + self.mediaPlayingUpdateConnection = nil + end + else + if self.runServiceConnection then + self.runServiceConnection:Disconnect() + end + end +end + +function AudioPreview:render() + return withTheme(function(theme) + local props = self.props + local state = self.state + local position = props.position + local size = props.size + local audioPreviewTheme = theme.assetPreview.audioPreview + + local layoutOrder = props.layoutOrder + local soundId = props.SoundId + + local currentTime = FFlagEnableToolboxVideos and props._CurrentTime or state.currentTime + local pause = props._Pause + local play = props._Play + local onMediaEnded = props._OnMediaEnded + + local progress + if state.timeLength ~= nil and state.timeLength ~= 0 then + progress = currentTime / state.timeLength + else + progress = 0 + end + + local showTreeView = props.ShowTreeView + local audioControlOffset = showTreeView and AUDIO_CONTROL_WIDTH_OFFSET_WITH_TREE or AUDIO_CONTROL_WIDTH_OFFSET_NO_TREE + local timeLength = self.getAudioLength() or 0 + local isLoaded = state.isLoaded + local isPlaying = FFlagEnableToolboxVideos and props._IsPlaying or state.isPlaying + + return Roact.createElement("Frame", { + BackgroundTransparency = 1, + BackgroundColor3 = audioPreviewTheme.backgroundColor, + LayoutOrder = layoutOrder, + Position = position, + Size = size, + },{ + UIListLayout = Roact.createElement("UIListLayout", { + FillDirection = Enum.FillDirection.Vertical, + HorizontalAlignment = Enum.HorizontalAlignment.Left, + VerticalAlignment = Enum.VerticalAlignment.Center, + SortOrder = Enum.SortOrder.LayoutOrder, + Padding = UDim.new(0, 10), + }), + + AudioPlayerFrame = Roact.createElement("Frame", { + Size = UDim2.new(1, 0, 1, -PROGRESS_BAR_HEIGHT- AUDIO_CONTROL_HEIGHT), + BackgroundTransparency = 1, + LayoutOrder = 1, + }, { + AudioPlayerImage = Roact.createElement("ImageLabel", { + Size = UDim2.new(1, 0, 1, 0), + Image = audioPreviewTheme.audioPlay_BG, + ScaleType = Enum.ScaleType.Fit, + BackgroundTransparency = 1, + BorderSizePixel = 0, + ImageColor3 = audioPreviewTheme.audioPlay_BG_Color, + }) + }), + + ProgressBarFrame = Roact.createElement("Frame", { + AnchorPoint = Vector2.new(0, 0.5), + Position = UDim2.new(0, 0, 0.5, 0), + Size = UDim2.new(1, 0, 0, PROGRESS_BAR_HEIGHT), + + BackgroundColor3 = audioPreviewTheme.progressBar_BG_Color, + BorderSizePixel = 0, + BackgroundTransparency = 0, + + LayoutOrder = 2, + }, { + ProgressBar = Roact.createElement("Frame", { + AnchorPoint = Vector2.new(0, 0.5), + BackgroundColor3 = audioPreviewTheme.progressBar, + BackgroundTransparency = 0, + BorderSizePixel = 0, + Position = UDim2.new(0, 0, 0.5, 0), + Size = UDim2.new(progress, 0, 0, PROGRESS_BAR_HEIGHT), + }) + }), + + MediaControl = FFlagEnableToolboxVideos and Roact.createElement(MediaControl, { + LayoutOrder = 3, + IsPlaying = isPlaying, + IsLoaded = isLoaded, + OnPause = pause, + OnPlay = play, + ShowTreeView = showTreeView, + TimeLength = timeLength, + TimePassed = currentTime, + }), + + AudioControlBase = (not FFlagEnableToolboxVideos) and Roact.createElement("Frame", { + Size = UDim2.new(1, 0, 0, AUDIO_CONTROL_HEIGHT), + BackgroundTransparency = 1, + LayoutOrder = 3, + }, { + AudioControl = Roact.createElement(AudioControl, { + position = UDim2.new(0, 0, 0, 0), + size = UDim2.new(1, 0, 0, AUDIO_CONTROL_HEIGHT), + audioControlOffset = audioControlOffset, + timeLength = timeLength, + isPlaying = isPlaying, + isLoaded = isLoaded, + timePassed = state.currentTime, + onResume = self.resumeSound, + onPause = self.pauseSound, + onPlay = self.playSound, + }), + }), + + SoundObj = Roact.createElement("Sound", { + Looped = false, + SoundId = FFlagEnableToolboxVideos and soundId or nil, + [Roact.Ref] = self.soundRef, + [Roact.Event.Changed] = self.onSoundChange, + [Roact.Event.Ended] = FFlagEnableToolboxVideos and onMediaEnded or self.onSoundEnded, + }) + }) + end) +end + +if FFlagEnableToolboxVideos then + return wrapMedia(AudioPreview) +else + return AudioPreview +end \ No newline at end of file diff --git a/BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Components/Preview/AudioPreview.spec.lua b/BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Components/Preview/AudioPreview.spec.lua new file mode 100644 index 0000000000..fc0653e7ec --- /dev/null +++ b/BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Components/Preview/AudioPreview.spec.lua @@ -0,0 +1,25 @@ +return function() + local Library = script.Parent.Parent.Parent + local Roact = require(Library.Parent.Parent.Roact) + + local MockWrapper = require(Library.MockWrapper) + + local AudioPreview = require(Library.Components.Preview.AudioPreview) + + local function createTestAsset(container, name) + local element = Roact.createElement(MockWrapper, {}, { + Roact.createElement(AudioPreview, { + SoundId = 123, + ReportPlay = function() end, + ReportPause = function() end, + }) + }) + + return Roact.mount(element, container or nil, name or "") + end + + it("should create and destroy without errors", function() + local instance = createTestAsset() + Roact.unmount(instance) + end) +end \ No newline at end of file diff --git a/BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Components/Preview/Favorites.lua b/BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Components/Preview/Favorites.lua new file mode 100644 index 0000000000..357dab03cb --- /dev/null +++ b/BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Components/Preview/Favorites.lua @@ -0,0 +1,122 @@ +--[[ + This component is designed to show the favorites counts for the assetPreview. + It will send request to fetch the data when loaded. And update accordingly. + + Necessary Properties: + Size = UDim2, + + FavoriteCounts = number, the number of favorites this asset has. + Favorited = bool, does the current user have this asset favorited. + OnActivated = callback, function to invoke when the favorited button is clicked. + + LayoutOrder = number, +]] + +local Library = script.Parent.Parent.Parent + +local Roact = require(Library.Parent.Parent.Roact) + +local Theming = require(Library.Theming) +local withTheme = Theming.withTheme + +local Favorites = Roact.PureComponent:extend("Favorites") + +-- For less then 10k, we use , to seperate the number. +-- For larger than 10k, we use xxk+ +local function getFavoritesCountString(counts) + local countsString + if counts > 10000 then + countsString = ("%dk+"):format(math.floor(counts / 1000)) + else + if counts > 1000 then + countsString = ("%d,%d"):format(counts / 1000, counts % 1000) + else + countsString = tostring(counts) + end + end + + return countsString +end + +function Favorites:init(props) + self.state = { + hovered = false + } + + self.onMouseEnter = function(rbx, x, y) + self:setState({ + hovered = true + }) + end + + self.onMouseLeave = function(rbx, x, y) + self:setState({ + hovered = false + }) + end +end + +function Favorites:render() + return withTheme(function(theme) + local props = self.props + local state = self.state + + local favoriteCounts = props.FavoriteCounts or 0 + + local favoritesTheme = theme.assetPreview.favorites + local favoritesImage = (state.hovered or props.Favorited) and favoritesTheme.favorited or favoritesTheme.unfavorited + local textContent = getFavoritesCountString(tonumber(favoriteCounts)) + local contentColor = favoritesTheme.contentColor + local size = props.Size + + local layoutOrder = props.LayoutOrder + local verticalAlignment = props.VerticalAlignment or Enum.VerticalAlignment.Center + + return Roact.createElement("Frame", { + Size = size, + + BackgroundTransparency = 1, + LayoutOrder = layoutOrder, + }, { + UIListLayout = Roact.createElement("UIListLayout", { + FillDirection = Enum.FillDirection.Horizontal, + SortOrder = Enum.SortOrder.LayoutOrder, + VerticalAlignment = verticalAlignment, + Padding = UDim.new(0, 4), + }), + + ImageContent = Roact.createElement("ImageButton", { + Size = UDim2.new(0, 20, 0, 20), + + BackgroundTransparency = 1, + + Image = favoritesImage, + + [Roact.Event.MouseEnter] = self.onMouseEnter, + [Roact.Event.MouseLeave] = self.onMouseLeave, + [Roact.Event.Activated] = self.props.OnActivated, + + LayoutOrder = 1, + }), + + TextContent = Roact.createElement("TextLabel", { + Size = UDim2.new(1, -20, 1, 0), + + Text = tostring(textContent), + TextColor3 = contentColor, + Font = theme.assetPreview.font, + TextSize = theme.assetPreview.textSizeMedium, + TextXAlignment = Enum.TextXAlignment.Left, + TextYAlignment = Enum.TextYAlignment.Center, + + BackgroundTransparency = 1, + + LayoutOrder = 2, + }) + }) + end) +end + +return Favorites + + diff --git a/BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Components/Preview/Favorites.spec.lua b/BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Components/Preview/Favorites.spec.lua new file mode 100644 index 0000000000..d0b32e3a35 --- /dev/null +++ b/BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Components/Preview/Favorites.spec.lua @@ -0,0 +1,78 @@ +return function() + local Library = script.Parent.Parent.Parent + local Roact = require(Library.Parent.Parent.Roact) + + local MockWrapper = require(Library.MockWrapper) + + local Favorites = require(Library.Components.Preview.Favorites) + + local IMAGE_FAVORITED = "rbxasset://textures/StudioToolbox/AssetPreview/star_filled.png" + local IMAGE_UNFAVORITED = "rbxasset://textures/StudioToolbox/AssetPreview/star_stroke.png" + + local function createTestFavorites(container, name, props) + local testFavoriteActivationValue = false + local favorited = true + if props then + favorited = props.Favorited + end + + local element = Roact.createElement(MockWrapper, {}, { + Favorites = Roact.createElement(Favorites, { + Size = UDim2.new(1,0,1,0), + + FavoriteCounts = 1000, + Favorited = favorited, + OnActivated = function() + testFavoriteActivationValue = not testFavoriteActivationValue + end, + + LayoutOrder = 1, + }) + }) + + return Roact.mount(element, container or nil, name or "") + end + + it("should create and destroy without errors", function() + local instance = createTestFavorites() + Roact.unmount(instance) + end) + + it("should render correctly", function() + local container = Instance.new("Folder") + local instance = createTestFavorites(container) + + local element = container:FindFirstChildOfClass("Frame") + expect(element.TextContent).to.be.ok() + expect(element.ImageContent).to.be.ok() + end) + + it("should properly set the initial favorite counts", function() + local container = Instance.new("Folder") + local instance = createTestFavorites(container) + + local element = container:FindFirstChildOfClass("Frame") + + expect(element.TextContent.Text).to.be.equal("1000") + end) + + it("should display the correct icon for a favorited asset", function() + local container = Instance.new("Folder") + local instance = createTestFavorites(container, nil, { + Favorited = true + }) + + local element = container:FindFirstChildOfClass("Frame") + expect(element.ImageContent.Image).to.be.equal(IMAGE_FAVORITED) + end) + + it("should display the correct icon for an unfavorited asset", function() + local container = Instance.new("Folder") + local instance = createTestFavorites(container, nil, { + Favorited = false + }) + + local element = container:FindFirstChildOfClass("Frame") + expect(element.ImageContent.Image).to.be.equal(IMAGE_UNFAVORITED) + end) +end \ No newline at end of file diff --git a/BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Components/Preview/ImagePreview.lua b/BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Components/Preview/ImagePreview.lua new file mode 100644 index 0000000000..ad75bcd475 --- /dev/null +++ b/BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Components/Preview/ImagePreview.lua @@ -0,0 +1,57 @@ +--[[ + This component is used to display a single image in an AssetPreview. + + Necessary properties: + Position = UDim2 + Size = UDim2 + ImageContent = String, url/rbxassetid of the image object to shown, + e.g. http://www.roblox.com/asset/?id= + rbxassetid:// + ScaleType = Enum.ScaleType.*, scaling type to use +]] + +local Library = script.Parent.Parent.Parent + +local Roact = require(Library.Parent.Parent.Roact) + +local Theming = require(Library.Theming) +local withTheme = Theming.withTheme + +local ImagePreview = Roact.PureComponent:extend("ImagePreview") + +function ImagePreview:render() + return withTheme(function(theme) + local props = self.props + local imageContent = props.ImageContent + + local position = props.Position + local size = props.Size or UDim2.new(1, 0, 1, 0) + + local imagePreviewTheme = theme.assetPreview.imagePreview + local scaleType = props.ScaleType or Enum.ScaleType.Fit + + local layoutOrder = props.LayoutOrder + + return Roact.createElement("Frame", { + Position = position, + Size = size, + + BackgroundTransparency = 0, + BackgroundColor3 = imagePreviewTheme.background, + BorderSizePixel = 0, + + LayoutOrder = layoutOrder, + },{ + ImageContent = Roact.createElement("ImageLabel", { + Size = UDim2.new(1, 0, 1, 0), + ScaleType = scaleType, + + BackgroundTransparency = 1, + + Image = imageContent, + }), + }) + end) +end + +return ImagePreview \ No newline at end of file diff --git a/BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Components/Preview/ImagePreview.spec.lua b/BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Components/Preview/ImagePreview.spec.lua new file mode 100644 index 0000000000..e3555e89b5 --- /dev/null +++ b/BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Components/Preview/ImagePreview.spec.lua @@ -0,0 +1,27 @@ +return function() + local Library = script.Parent.Parent.Parent + local Roact = require(Library.Parent.Parent.Roact) + + local MockWrapper = require(Library.MockWrapper) + + local ImagePreview = require(Library.Components.Preview.ImagePreview) + + local function createTestAsset(container, name) + local image = "rbxasset://textures/AnimationEditor/animation_editor_blue.png" + local element = Roact.createElement(MockWrapper, {}, { + Roact.createElement(ImagePreview, { + ImageContent = image, + TextContent = "ImagePreviewTest", + Position = UDim2.new(1, 0, 1, 0), + Size = UDim2.new(0, 100, 0, 20) + }) + }) + + return Roact.mount(element, container or nil, name or "") + end + + it("should create and destroy without errors", function() + local instance = createTestAsset() + Roact.unmount(instance) + end) +end \ No newline at end of file diff --git a/BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Components/Preview/InstanceTreeViewItem.lua b/BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Components/Preview/InstanceTreeViewItem.lua new file mode 100644 index 0000000000..d12b79dc1f --- /dev/null +++ b/BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Components/Preview/InstanceTreeViewItem.lua @@ -0,0 +1,161 @@ +--[[ + A single item displayed in a TreeView component. + + Required Props: + element = instance, The instance to display. + indent = number, The level of indentation this item appears at. + canExpand = boolean, Whether this item has children and can be expanded. + isExpanded = boolean, Whether this item is showing its children. + isSelected = boolean, Whether this item is the selected item. + rowIndex = number, The order in which this item appears in the list. + toggleSelected = callback, A callback when this item is clicked. +]] +local FFlagAssetManagerLuaCleanup1 = settings():GetFFlag("AssetManagerLuaCleanup1") + +local Library = script.Parent.Parent.Parent + +local Roact = require(Library.Parent.Parent.Roact) + +local GetClassIcon = require(Library.Utils.GetClassIcon) +local TooltipWrapper = require(Library.Components.Tooltip) + +local Theming = require(Library.Theming) +local withTheme = Theming.withTheme + +local ELEMENT_PADDING = 5 + +local TreeViewItem = Roact.PureComponent:extend("TreeViewItem") + +function TreeViewItem:init() + self.state = { + Hovering = false, + } + + self.mouseEnter = function() + self:setState({ + Hovering = true, + }) + end + + self.mouseLeave = function() + self:setState({ + Hovering = false, + }) + end + + self.onClick = function() + self.props.toggleSelected() + end +end + +function TreeViewItem:render(props) + return withTheme(function(theme) + local treeViewTheme = theme.instanceTreeView + local instance = self.props.element + local name = instance.Name + local iconInfo = GetClassIcon(instance) + if FFlagAssetManagerLuaCleanup1 then + if typeof(instance) == "table" and instance.Icon then + iconInfo = instance.Icon + end + end + + local indent = self.props.indent + local expandable = self.props.canExpand + local expanded = self.props.isExpanded + local selected = self.props.isSelected + local layoutOrder = self.props.rowIndex or 1 + local height = treeViewTheme.treeItemHeight + local hover = self.state.Hovering + + local selectionOffset = height + + local labelOffset = selectionOffset + ELEMENT_PADDING + + (iconInfo and (height + treeViewTheme.treeViewIndent) or 0) + + return Roact.createElement("Frame", { + Size = UDim2.new(1, 0, 0, height), + BackgroundTransparency = 1, + LayoutOrder = layoutOrder, + }, { + Padding = Roact.createElement("UIPadding", { + PaddingLeft = UDim.new(0, indent * treeViewTheme.treeViewIndent), + }), + + Layout = Roact.createElement("UIListLayout", { + SortOrder = Enum.SortOrder.LayoutOrder, + Padding = UDim.new(0, ELEMENT_PADDING), + FillDirection = Enum.FillDirection.Horizontal, + }), + + Expand = Roact.createElement("ImageButton", { + LayoutOrder = 0, + Size = UDim2.new(0, height, 0, height), + BackgroundTransparency = 1, + ImageTransparency = 1, + + [Roact.Event.Activated] = self.props.toggleExpanded, + }, { + ExpandIcon = expandable and Roact.createElement("ImageLabel", { + BackgroundTransparency = 1, + ScaleType = Enum.ScaleType.Fit, + Size = UDim2.new(0, 9, 0, 9), + AnchorPoint = Vector2.new(0.5, 0.5), + Position = UDim2.new(0.5, 0, 0.5, 1), + ImageTransparency = expandable and 0 or 1, + Image = expanded and treeViewTheme.arrowExpanded or treeViewTheme.arrowCollapsed, + ImageColor3 = treeViewTheme.arrowColor, + }), + }), + + Icon = iconInfo and Roact.createElement("ImageLabel", { + ZIndex = 2, + LayoutOrder = 1, + Size = UDim2.new(0, height, 0, height), + BackgroundTransparency = 1, + Image = iconInfo.Image, + ImageRectSize = iconInfo.ImageRectSize, + ImageRectOffset = iconInfo.ImageRectOffset, + }), + + Name = Roact.createElement("TextLabel", { + ZIndex = 2, + LayoutOrder = 2, + BackgroundTransparency = 1, + Size = UDim2.new(1, -labelOffset, 0, height), + Font = treeViewTheme.font, + TextXAlignment = Enum.TextXAlignment.Left, + TextSize = treeViewTheme.textSize, + TextTruncate = Enum.TextTruncate.AtEnd, + Text = name, + TextColor3 = selected and treeViewTheme.selectedText or treeViewTheme.textColor, + BorderSizePixel = 0, + }, { + Tooltip = Roact.createElement(TooltipWrapper, { + Text = name, + Enabled = hover, + ShowDelay = treeViewTheme.tooltipShowDelay, + }), + }), + + -- We have to create a Folder so that the hover is not affected by the UIListLayout. + HoverFolder = Roact.createElement("Folder", {}, { + Hover = Roact.createElement("ImageButton", { + Size = UDim2.new(1, -selectionOffset, 1, 4), + Position = UDim2.new(0, selectionOffset, 0.5, 0), + AnchorPoint = Vector2.new(0, 0.5), + BackgroundTransparency = (hover or selected) and 0 or 1, + BackgroundColor3 = selected and treeViewTheme.selected or treeViewTheme.hover, + BorderSizePixel = 0, + AutoButtonColor = false, + + [Roact.Event.MouseEnter] = self.mouseEnter, + [Roact.Event.MouseLeave] = self.mouseLeave, + [Roact.Event.Activated] = self.onClick, + }), + }), + }) + end) +end + +return TreeViewItem \ No newline at end of file diff --git a/BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Components/Preview/InstanceTreeViewItem.spec.lua b/BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Components/Preview/InstanceTreeViewItem.spec.lua new file mode 100644 index 0000000000..e835de7900 --- /dev/null +++ b/BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Components/Preview/InstanceTreeViewItem.spec.lua @@ -0,0 +1,44 @@ +return function() + local Library = script.Parent.Parent.Parent + local Roact = require(Library.Parent.Parent.Roact) + + local MockWrapper = require(Library.MockWrapper) + + local TreeViewItem = require(Library.Components.Preview.InstanceTreeViewItem) + + it("should create and destroy without errors", function() + local element = Roact.createElement(MockWrapper, {}, { + TreeViewItem = Roact.createElement(TreeViewItem, { + element = Instance.new("Part"), + indent = 0, + }), + }) + local instance = Roact.mount(element) + Roact.unmount(instance) + end) + + it("should render correctly", function() + local element = Roact.createElement(MockWrapper, {}, { + TreeViewItem = Roact.createElement(TreeViewItem, { + element = Instance.new("Part"), + indent = 0, + canExpand = true, + }), + }) + local container = Instance.new("Folder") + local instance = Roact.mount(element, container, "TreeViewItem") + + local treeViewItem = container.TreeViewItem + expect(treeViewItem).to.be.ok() + expect(treeViewItem.Padding).to.be.ok() + expect(treeViewItem.Layout).to.be.ok() + expect(treeViewItem.Expand).to.be.ok() + expect(treeViewItem.Expand.ExpandIcon).to.be.ok() + expect(treeViewItem.Icon).to.be.ok() + expect(treeViewItem.Name).to.be.ok() + expect(treeViewItem.HoverFolder).to.be.ok() + expect(treeViewItem.HoverFolder.Hover).to.be.ok() + + Roact.unmount(instance) + end) +end diff --git a/BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Components/Preview/MediaControl.lua b/BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Components/Preview/MediaControl.lua new file mode 100644 index 0000000000..4482705399 --- /dev/null +++ b/BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Components/Preview/MediaControl.lua @@ -0,0 +1,128 @@ +--[[ + The control panel for Sounds and VideoFrames that provides a + play/pause button, progress bar, and a time label to show time left. + + Required Props: + boolean IsPlaying: Whether or not the Sound or VideoFrame is currently playing. + boolean IsLoaded: Whether or not the Sound or VideoFrame is loaded. + callback OnPause: Called when clicking the pause button. + callback OnPlay: Called when first clicking the play button. + boolean ShowTreeView: used to control the position of the play/pause button. + number TimeLength: The total Sound/VideoFrame length. + number TimePassed: How much time has passed since playing the Sound/VideoFrame. + + Optional Props: + Vector2 AnchorPoint: The AnchorPoint of the component + UDim2 LayoutOrder: The LayoutOrder of the component + UDim2 Position: The Position of the component +]] +local FFlagHideOneChildTreeviewButton = game:GetFastFlag("HideOneChildTreeviewButton") + +local Library = script.Parent.Parent.Parent +local Roact = require(Library.Parent.Parent.Roact) + +local Theming = require(Library.Theming) +local withTheme = Theming.withTheme + +local getTimeString = require(Library.Utils.getTimeString) +local RoundButton = require(Library.Components.RoundFrame) + +local LoadingIndicator = require(Library.Components.LoadingIndicator) + +local TIME_LABEL_HEIGHT = 15 +local BUTTON_SIZE = 28 +local AUDIO_CONTROL_HEIGHT = 35 +local AUDIO_CONTROL_WIDTH_OFFSET_WITH_TREE = 50 +local AUDIO_CONTROL_WIDTH_OFFSET_NO_TREE = 70 + +if FFlagHideOneChildTreeviewButton then + AUDIO_CONTROL_WIDTH_OFFSET_NO_TREE = 10 +end + +local MediaControl = Roact.PureComponent:extend("MediaControl") + +MediaControl.defaultProps = { + TimePassed = 0, +} + +function MediaControl:init() + self.onActivated = function() + if not self.props.IsLoaded then + return + end + + if self.props.IsPlaying then + self.props.OnPause() + else + self.props.OnPlay() + end + end +end + +function MediaControl:render() + return withTheme(function(theme) + local audioPreviewTheme = theme.assetPreview.audioPreview + local props = self.props + + local anchorPoint = props.AnchorPoint + local layoutOrder = props.LayoutOrder + local isLoaded = props.IsLoaded + local isPlaying = props.IsPlaying + local position = props.Position + local showTreeView = props.ShowTreeView + local timeLength = props.TimeLength + local timePassed = props.TimePassed + + local controlOffset = showTreeView and AUDIO_CONTROL_WIDTH_OFFSET_WITH_TREE or AUDIO_CONTROL_WIDTH_OFFSET_NO_TREE + local timeString = getTimeString(timePassed) .. '/' .. getTimeString(timeLength) + + return Roact.createElement("Frame", { + AnchorPoint = anchorPoint, + BackgroundTransparency = 1, + LayoutOrder = layoutOrder, + Position = position, + Size = UDim2.new(1, 0, 0, AUDIO_CONTROL_HEIGHT), + }, { + Button = Roact.createElement(RoundButton, { + AnchorPoint = Vector2.new(0.5, 0), + AutoButtonColor = false, + BackgroundColor3 = isLoaded and audioPreviewTheme.buttonBackgroundColor or audioPreviewTheme.buttonDisabledBackgroundColor, + BackgroundTransparency = isLoaded and 0 or audioPreviewTheme.buttonDisabledBackgroundTransparency, + BorderSizePixel = 0, + OnActivated = self.onActivated, + Position = UDim2.new(0, 24, 0, 0), + Size = UDim2.new(0, BUTTON_SIZE, 0, BUTTON_SIZE), + }, { + PlayOrPauseIcon = Roact.createElement("ImageLabel", { + AnchorPoint = Vector2.new(0.5, 0.5), + BackgroundTransparency = 1, + Image = isPlaying and audioPreviewTheme.pauseButton or audioPreviewTheme.playButton, + ImageColor3 = audioPreviewTheme.buttonColor, + ImageTransparency = isLoaded and 0 or audioPreviewTheme.buttonDisabledBackgroundTransparency, + Position = UDim2.new(0.5, 0, 0.5, 0), + Size = UDim2.new(1, 0, 1, 0), + }), + }), + + TimeComponent = isLoaded and Roact.createElement("TextLabel", { + AnchorPoint = Vector2.new(1, 0.5), + BackgroundTransparency = 1, + Font = audioPreviewTheme.font, + Position = UDim2.new(1, -controlOffset, 0.5, 0), + Size = UDim2.new(0, 204, 0, TIME_LABEL_HEIGHT), + Text = timeString, + TextColor3 = audioPreviewTheme.textColor, + TextSize = audioPreviewTheme.fontSize, + TextXAlignment = Enum.TextXAlignment.Right, + }), + + LoadingIndicator = (not isLoaded) and Roact.createElement(LoadingIndicator, { + AnchorPoint = Vector2.new(1, 0.5), + Position = UDim2.new(1, -controlOffset, 0.5, 0), + Size = UDim2.new(0, 50, 0, TIME_LABEL_HEIGHT), + }), + }) + end) +end + +return MediaControl \ No newline at end of file diff --git a/BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Components/Preview/MediaControl.spec.lua b/BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Components/Preview/MediaControl.spec.lua new file mode 100644 index 0000000000..3ff5a182f1 --- /dev/null +++ b/BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Components/Preview/MediaControl.spec.lua @@ -0,0 +1,34 @@ +return function() + local Library = script.Parent.Parent.Parent + local Roact = require(Library.Parent.Parent.Roact) + + local MockWrapper = require(Library.MockWrapper) + + local MediaControl = require(Library.Components.Preview.MediaControl) + + local function createTestAsset(container, name) + local emptyFunc = function() end + + local element = Roact.createElement(MockWrapper, {}, { + Roact.createElement(MediaControl, { + AnchorPoint = Vector2.new(0, 0), + IsPlaying = false, + IsLoaded = true, + LayoutOrder = 1, + OnPause = emptyFunc, + OnPlay = emptyFunc, + Position = UDim2.new(0, 0, 0, 0), + ShowTreeView = false, + TimeLength = 1, + TimePassed = 0, + }) + }) + + return Roact.mount(element, container or nil, name or "") + end + + it("should create and destroy without errors", function() + local instance = createTestAsset() + Roact.unmount(instance) + end) +end \ No newline at end of file diff --git a/BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Components/Preview/MediaProgressBar.lua b/BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Components/Preview/MediaProgressBar.lua new file mode 100644 index 0000000000..e7abdeafa7 --- /dev/null +++ b/BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Components/Preview/MediaProgressBar.lua @@ -0,0 +1,179 @@ +--[[ + A one-knob slider + TODO: DEVTOOLS-4330 - Replace this entire component with DevFramework's one-knob slider once assetPreview is out of UILibrary + + Required Props: + number Min: Min value of the slider + number Max: Max value of the slider + number CurrentValue: Current value for the lower range handle + callback OnValuesChanged: A callback that takes in params: minValue, maxValue. The callback is called whenever the min or max value changes. + + Optional Props: + Vector2 AnchorPoint: The anchorPoint of the component + boolean Disabled: Whether to render in the enabled/disabled state + number LayoutOrder: The layoutOrder of the component + UDim2 Position: The position of the component + number SnapIncrement: Incremental points that the slider's knob will snap to. A "0" snap increment means no snapping. + number VerticalDragTolerance: A vertical pixel height for allowing a pressed mouse to drag knobs on outside the component's size. + +]] +local Library = script.Parent.Parent.Parent +local Roact = require(Library.Parent.Parent.Roact) + +local Theming = require(Library.Theming) +local withTheme = Theming.withTheme + +local PROGRESS_BAR_HEIGHT = 6 +local knobSize = Vector2.new(15, 15) + +local MediaProgressBar = Roact.PureComponent:extend("MediaProgressBar") + +local function isUserInputTypeClick(inputType) + return (inputType == Enum.UserInputType.Touch) or (inputType == Enum.UserInputType.MouseButton1) +end + +MediaProgressBar.defaultProps = { + Disabled = false, + Size = UDim2.new(1, 0, 1, 0), + SnapIncrement = 0, + VerticalDragTolerance = 300, +} + +function MediaProgressBar:init() + self.sliderFrameRef = Roact.createRef() + + self.state = { + pressed = false + } + + self.getTotalRange = function() + return self.props.Max - self.props.Min + end + + self.getSnappedValue = function(value) + local snapIncrement = self.props.SnapIncrement + local min = self.props.Min + local max = self.props.Max + + if snapIncrement > 0 then + local prevSnap = math.max(snapIncrement * math.floor(value / snapIncrement), min) + local nextSnap = math.min(prevSnap + snapIncrement, max) + return math.abs(prevSnap-value) < math.abs(nextSnap-value) and prevSnap or nextSnap + end + + return math.clamp(value, min, max) + end + + self.getMouseClickValue = function(input) + local sliderFrameRef = self.sliderFrameRef.current + local inputHorizontalOffsetNormalized = (input.Position.X - sliderFrameRef.AbsolutePosition.X) / sliderFrameRef.AbsoluteSize.X + inputHorizontalOffsetNormalized = math.clamp(inputHorizontalOffsetNormalized, 0, 1) + local valueBeforeSnapping = self.props.Min + (inputHorizontalOffsetNormalized * self.getTotalRange()) + + return self.getSnappedValue(valueBeforeSnapping) + end + + self.setValuesFromInput = function(input) + local mouseClickValue = self.getMouseClickValue(input) + local clampedValue = math.clamp(mouseClickValue, self.props.Min, self.props.Max) + + self.props.OnValuesChanged(clampedValue) + end + + self.onInputBegan = function(rbx, input) + if self.props.Disabled then + return + + elseif isUserInputTypeClick(input.UserInputType) then + self:setState({ + pressed = true, + }) + self.setValuesFromInput(input) + end + end + + self.onInputChanged = function(rbx, input) + if self.props.Disabled then + return + + elseif self.state.pressed and input.UserInputType == Enum.UserInputType.MouseMovement then + self.setValuesFromInput(input) + end + end + + self.onInputEnded = function(rbx, input) + if not self.props.Disabled and isUserInputTypeClick(input.UserInputType) then + self.props.OnInputEnded() + self:setState({ + pressed = false, + }) + end + end +end + +function MediaProgressBar:render() + return withTheme(function(theme) + local audioPreviewTheme = theme.assetPreview.audioPreview + + local anchorPoint = self.props.AnchorPoint + local isDisabled = self.props.Disabled + local currentValue = self.props.CurrentValue + local layoutOrder = self.props.LayoutOrder + local min = self.props.Min + local position = self.props.Position + local verticalDragBuffer = self.props.VerticalDragTolerance + + local lowerFillPercent = (currentValue - min) / self.getTotalRange() + + return Roact.createElement("Frame", { + AnchorPoint = anchorPoint, + BackgroundTransparency = 1, + LayoutOrder = layoutOrder, + Position = position, + Size = UDim2.new(1, 0, 0, knobSize.X), + + [Roact.Ref] = self.sliderFrameRef, + }, { + ProgressBarBackground = Roact.createElement("Frame", { + AnchorPoint = Vector2.new(0, 0.5), + BackgroundColor3 = audioPreviewTheme.progressBar_BG_Color, + BackgroundTransparency = 0, + BorderSizePixel = 0, + Position = UDim2.new(0, 0, 0.5, 0), + Size = UDim2.new(1, 0, 0, PROGRESS_BAR_HEIGHT), + }, { + ProgressBarForeground = Roact.createElement("Frame", { + BackgroundColor3 = audioPreviewTheme.progressBar, + BackgroundTransparency = 0, + BorderSizePixel = 0, + Size = UDim2.new(lowerFillPercent, 0, 1, 0), + }), + }), + + Knob = Roact.createElement("ImageButton", { + AutoButtonColor = false, + AnchorPoint = Vector2.new(0, 0.5), + BackgroundTransparency = 1, + Image = audioPreviewTheme.progressKnob, + ImageColor3 = audioPreviewTheme.progressKnobColor, + Position = UDim2.new(lowerFillPercent, 0, 0.5, 0), + Size = UDim2.new(0, knobSize.X, 0, knobSize.Y), + ZIndex = 3, + }), + + ClickHandler = (not isDisabled) and Roact.createElement("ImageButton", { + AnchorPoint = Vector2.new(0.5, 0.5), + BackgroundTransparency = 1, + Position = UDim2.new(0.5, 0, 0.5, 0), + Size = UDim2.new(1, knobSize.X, 1, self.state.pressed and verticalDragBuffer or 0), + ZIndex = 4, + + [Roact.Event.InputBegan] = self.onInputBegan, + [Roact.Event.InputChanged] = self.onInputChanged, + [Roact.Event.InputEnded] = self.onInputEnded, + }), + }) + end) +end + +return MediaProgressBar \ No newline at end of file diff --git a/BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Components/Preview/MediaProgressBar.spec.lua b/BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Components/Preview/MediaProgressBar.spec.lua new file mode 100644 index 0000000000..d83b361d03 --- /dev/null +++ b/BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Components/Preview/MediaProgressBar.spec.lua @@ -0,0 +1,30 @@ +return function() + local Library = script.Parent.Parent.Parent + local Roact = require(Library.Parent.Parent.Roact) + + local MockWrapper = require(Library.MockWrapper) + + local MediaProgressBar = require(Library.Components.Preview.MediaProgressBar) + + local function createTestAsset(container, name) + local emptyFunc = function() end + + local element = Roact.createElement(MockWrapper, {}, { + Roact.createElement(MediaProgressBar, { + CurrentValue = 0, + LayoutOrder = 2, + Min = 0, + Max = 1, + OnValuesChanged = emptyFunc, + OnInputEnded = emptyFunc, + }) + }) + + return Roact.mount(element, container or nil, name or "") + end + + it("should create and destroy without errors", function() + local instance = createTestAsset() + Roact.unmount(instance) + end) +end \ No newline at end of file diff --git a/BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Components/Preview/ModelPreview.lua b/BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Components/Preview/ModelPreview.lua new file mode 100644 index 0000000000..8073f9373f --- /dev/null +++ b/BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Components/Preview/ModelPreview.lua @@ -0,0 +1,216 @@ +--[[ + This component handles the model rendering and input support for model preview. + + Necessary properties: + Position = UDim2 + Size = UDim2 + TargetModel = Model, the model is only for previewing. So, it contains the assetInstance with all he + scripts being disabled. + + Optional properties: + OnModelPreviewFrameEntered = callBack, we use those function to make sure input will be captured if + mouse is within the area of the frame. + OnModelPreviewFrameLeft = callBack +]] + +local Library = script.Parent.Parent.Parent + +local Roact = require(Library.Parent.Parent.Roact) + +local getCamera = require(Library.Camera).getCamera + +local Theming = require(Library.Theming) +local withTheme = Theming.withTheme + +local ModelPreview = Roact.PureComponent:extend("ModelPreview") + +-- Used for the inital camera position +local INSERT_CAMERA_DIST_MULT = 0.8 +local PAN_CAMERA_DIST_MULT = 0.1 +local DOUBLE_CLICK_TIME = 0.25 + +function ModelPreview:init() + self.orbitDrag = false + self.panDrag = false + self.doubleClickTimestamp = tick() + + -- Need reference to ViewportFrame so I can set the preview model to it. + self.VFRef = Roact.createRef() + + self.modelPreviewCamera = getCamera(self) + + -- This is the model that will be displayed on the ViewportFrame. + self.VFModel = nil + + self.onInputBegan = function(_, input) + if input.UserInputType == Enum.UserInputType.MouseButton1 then + self.orbitDrag = true + end + + if input.UserInputType == Enum.UserInputType.MouseButton3 or + input.UserInputType == Enum.UserInputType.MouseButton2 then + self.panDrag = true + end + end + + self.onInputChanged = function(_, input) + if input.UserInputType == Enum.UserInputType.MouseMovement + and self.orbitDrag or self.panDrag then + local camera = self.modelPreviewCamera + local targetFocus = camera.Focus + local targetCF = targetFocus:ToObjectSpace(camera.CFrame) + + if self.orbitDrag then + targetCF = CFrame.fromAxisAngle(targetCF.RightVector, input.Delta.y * -0.01) * targetCF + targetCF = CFrame.fromAxisAngle(Vector3.new(0, 1, 0), input.Delta.x * -0.01) * targetCF + elseif self.panDrag then + local dist = (targetCF.p - targetFocus.p).magnitude + dist = dist ~= dist and 0 or dist -- NaN check + local distanceFactor = PAN_CAMERA_DIST_MULT * (dist * 0.1) + local yOffset = targetCF.upVector.Unit * input.Delta.y * distanceFactor + local xOffset = -targetCF.rightVector.Unit * input.Delta.x * distanceFactor + targetCF = targetCF + yOffset + xOffset + targetFocus = targetFocus + yOffset + xOffset + end + + camera.CFrame = camera.Focus:ToWorldSpace(targetCF) + camera.Focus = targetFocus + end + end + + self.onInputEnded = function(_, input) + if input.UserInputType == Enum.UserInputType.MouseButton1 then + self.orbitDrag = false + if tick() < self.doubleClickTimestamp + DOUBLE_CLICK_TIME then + self.centerCamera() + end + self.doubleClickTimestamp = tick() + end + + if input.UserInputType == Enum.UserInputType.MouseButton3 or + input.UserInputType == Enum.UserInputType.MouseButton2 then + self.panDrag = false + end + end + + self.zoomCamera = function(zoomIn) + local zoomFactor = zoomIn and -1 or 1 + local camera = self.modelPreviewCamera + local current = camera.CFrame + local focus = camera.Focus + + local dist = (current.p - focus.p).magnitude + dist = dist ~= dist and 0 or dist -- NaN check + + local moveAmount = math.max(dist * 0.1, 0.1) + local targetCF = current * CFrame.new(0, 0, zoomFactor * moveAmount) + + camera.CFrame = targetCF + end + + self.onMouseWheelBackward = function() + self.zoomCamera(false) + end + + self.onMouseWheelForward = function() + self.zoomCamera(true) + end + + self.centerCamera = function() + local currentPreviewCopy = self.VFModel + local camera = self.modelPreviewCamera + + -- Move the model/part in front of the camera + local success, modelCf, size = pcall(function() return currentPreviewCopy:GetBoundingBox() end) + if not success then + size = currentPreviewCopy.Size + currentPreviewCopy.CFrame = currentPreviewCopy.CFrame - currentPreviewCopy.CFrame.p + else + currentPreviewCopy:TranslateBy(-modelCf.p) + end + + local cameraDistAway = size.magnitude * INSERT_CAMERA_DIST_MULT + local dir = Vector3.new(1, 1, 1).unit + camera.Focus = CFrame.new() + camera.CFrame = CFrame.new(cameraDistAway * dir, camera.Focus.p) + end + + -- Because we are using refs and not using state, this component will not + -- re-render unless we are viewing a different model. Every call to this + -- function assumes that a different model has been selected. + self.tryRenderModel = function() + local myVRFrame = self.VFRef.current + local currentPreviewCopy = self.VFModel + myVRFrame:ClearAllChildren() + currentPreviewCopy.Parent = myVRFrame + + self.centerCamera() + end +end + +function ModelPreview:makeViewportModel() + local currentPreview = self.props.TargetModel + if currentPreview:IsA("Model") or currentPreview:IsA("BasePart") then + self.VFModel = currentPreview:Clone() + else + self.VFModel = Instance.new("Model") + currentPreview:Clone().Parent = self.VFModel + end +end + +function ModelPreview:didMount() + self:makeViewportModel() + self.tryRenderModel() +end + +function ModelPreview:didUpdate() + self.tryRenderModel() +end + +function ModelPreview:willUnmount() + if self.VFModel then + self.VFModel:Destroy() + end +end + +function ModelPreview:render() + return withTheme(function(theme) + local props = self.props + local position = props.Position + local size = props.Size or UDim2.new(1, 0, 1, 0) + local ModelPreviewCamera = self.modelPreviewCamera + + local OnModelPreviewFrameEntered = props.OnModelPreviewFrameEntered + local OnModelPreviewFrameLeft = props.OnModelPreviewFrameLeft + + local layoutOrder = props.LayoutOrder + + self:makeViewportModel() + + -- The element we return is determined by object we receive. + return Roact.createElement("ViewportFrame", { + Position = position, -- We should avoid using relative position and size. + Size = size, + + BorderSizePixel = 0, + BackgroundTransparency = 0, + BackgroundColor3 = theme.assetPreview.modelPreview.background, + + CurrentCamera = ModelPreviewCamera, + [Roact.Ref] = self.VFRef, + + [Roact.Event.MouseEnter] = OnModelPreviewFrameEntered, + [Roact.Event.MouseLeave] = OnModelPreviewFrameLeft, + [Roact.Event.MouseWheelForward] = self.onMouseWheelForward, + [Roact.Event.MouseWheelBackward] = self.onMouseWheelBackward, + [Roact.Event.InputBegan] = self.onInputBegan, + [Roact.Event.InputEnded] = self.onInputEnded, + [Roact.Event.InputChanged] = self.onInputChanged, + [Roact.Event.TouchPinch] = self.onTouchPinch, + + LayoutOrder = layoutOrder, + }) + end) +end + +return ModelPreview diff --git a/BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Components/Preview/ModelPreview.spec.lua b/BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Components/Preview/ModelPreview.spec.lua new file mode 100644 index 0000000000..6a0d95fe7d --- /dev/null +++ b/BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Components/Preview/ModelPreview.spec.lua @@ -0,0 +1,28 @@ +return function() + local Library = script.Parent.Parent.Parent + local Roact = require(Library.Parent.Parent.Roact) + + local MockWrapper = require(Library.MockWrapper) + + local ModelPreview = require(Library.Components.Preview.ModelPreview) + + local function createTestAsset(container, name) + local testModel = Instance.new("Model") + + local element = Roact.createElement(MockWrapper, {}, { + ModelPreview = Roact.createElement(ModelPreview, { + TargetModel = testModel, + + Position = UDim2.new(1, 0, 1, 0), + Size = UDim2.new(1, 0, 1, 0), + }), + }) + + return Roact.mount(element, container or nil, name or "") + end + + it("should create and destroy without errors", function() + local instance = createTestAsset() + Roact.unmount(instance) + end) +end \ No newline at end of file diff --git a/BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Components/Preview/PreviewController.lua b/BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Components/Preview/PreviewController.lua new file mode 100644 index 0000000000..1c2ac3c7ed --- /dev/null +++ b/BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Components/Preview/PreviewController.lua @@ -0,0 +1,350 @@ +--!nolint DeprecatedGlobal +--^ enables migration with FFlagPreviewControllerUseOsClock; remove with that flag. + +--[[ + This component is used to render both mainPreview and TreeView. + MainView can be modelPreview, soundPreview, scriptPreview, imagePreview, otherPreview and audioPlay. + Note soundPreview and audioPlay are different here. Sound preview is uesed to preview sound object comes with + the model, audioPlay is the audioPlayer we made for the audio asset. Which actually supports play, pause. + + Required Props: + Width = number, + + CurrentPreview = Instance, this is the instance that is currently displayed in the preview. + PreviewModel = Instance, this is the top level asset that will be displayed in the InstanceTreeView + AssetPreviewType = AssetType.TYPES, custom category that will inform which preview will be displayed. + AssetId = number, + PutTreeViewOnBottom = boolean, this determines whether the TreeView will be displayed on the right or bottom. + + OnTreeItemClicked = callback, which sets the preview to show the new Tree Item, the callback takes 1 parameter of type instance. + OnModelPreviewFrameEntered = callback, this is a callback that disables the scrollbar + OnModelPreviewFrameLeft = callback, this is a callback that re-enables teh scollbar + + LayoutOrder = number, +]] +local FFlagStudioMinorFixesForAssetPreview = settings():GetFFlag("StudioMinorFixesForAssetPreview") +local FFlagHideOneChildTreeviewButton = game:GetFastFlag("HideOneChildTreeviewButton") +local FFlagStudioFixTreeViewForFlatList = settings():GetFFlag("StudioFixTreeViewForFlatList") +local FFlagStudioAssetPreviewTreeFix2 = game:DefineFastFlag("StudioAssetPreviewTreeFix2", false) +local FFlagEnableToolboxVideos = game:GetFastFlag("EnableToolboxVideos") + +local FFlagPreviewControllerUseOsClock = game:DefineFastFlag("PreviewControllerUseOsClock", false) + +local Library = script.Parent.Parent.Parent + +local Roact = require(Library.Parent.Parent.Roact) + +local ModelPreview = require(Library.Components.Preview.ModelPreview) +local ImagePreview = require(Library.Components.Preview.ImagePreview) +local ThumbnailIconPreview = require(Library.Components.Preview.ThumbnailIconPreview) +local TreeViewButton = require(Library.Components.Preview.TreeViewButton) +local AssetType = require(Library.Utils.AssetType) +local AudioPreview = require(Library.Components.Preview.AudioPreview) +local VideoPreview = require(Library.Components.Preview.VideoPreview) + +local TreeViewItem = require(Library.Components.Preview.InstanceTreeViewItem) +local TreeView = require(Library.Components.TreeView) + +local LoadingIndicator = require(Library.Components.LoadingIndicator) + +local Theming = require(Library.Theming) +local withTheme = Theming.withTheme + +local Urls = require(Library.Utils.Urls) + +local TREEVIEW_WIDTH = 242 +local PREVIEW_HEIGHT = 242 +local TREEVIEW_BOTTOM_HEIGHT = 120 +local MAINVIEW_BUTTONS_X_OFFSET = -7 +local MAINVIEW_BUTTONS_Y_OFFSET = -7 + +local MODAL_MIN_WIDTH = 235 + +local PreviewController = Roact.PureComponent:extend("PreviewController") + +local function getImage(instance) + if FFlagStudioMinorFixesForAssetPreview then + if typeof(instance) ~= "Instance" then + return nil + end + end + + if instance:IsA("Decal") or instance:IsA("Texture") then + return instance.Texture + elseif instance:IsA("Sky") then + return instance.SkyboxFt + else + return instance.Image + end +end + +local function getImageScaleType(instance) + if FFlagStudioMinorFixesForAssetPreview then + if typeof(instance) ~= "Instance" then + return Enum.ScaleType.Fit + end + end + if instance:IsA("Sky") then + return Enum.ScaleType.Crop + else + return Enum.ScaleType.Fit + end +end + +function PreviewController:createTreeView(previewModel, size) + return withTheme(function(theme) + local onTreeviewEntered = self.onTreeviewEntered + local onTreeviewLeft = self.onTreeviewLeft + + local dataTree + if FFlagStudioAssetPreviewTreeFix2 then + dataTree = previewModel + else + dataTree = FFlagStudioFixTreeViewForFlatList and self.props.CurrentPreview or previewModel + end + + return Roact.createElement("ImageButton", { + Size = size, + + BackgroundTransparency = 0, + BackgroundColor3 = theme.instanceTreeView.background, + BorderSizePixel = 0, + AutoButtonColor = false, + + [Roact.Event.MouseEnter] = onTreeviewEntered, + [Roact.Event.MouseLeave] = onTreeviewLeft, + + LayoutOrder = 2 + },{ + TreeViewFrame = Roact.createElement(TreeView, { + dataTree = dataTree, + onSelectionChanged = self.onTreeItemClicked, + + createFlatList = FFlagStudioFixTreeViewForFlatList and true or false, + + getChildren = function(instance) + return instance:GetChildren() + end, + + renderElement = function(properties) + return Roact.createElement(TreeViewItem, properties) + end, + }) + }) + end) +end + +function PreviewController:init(props) + self.state = { + -- This controls the treeview and full screen button's status + showTreeView = false, + } + + self.inModelPreview = false + self.inTreeview = false + self.totalTimeSpent = 0 + self.cacheTime = 0 + + self.onTreeviewStatusToggle = function(newStatus) + self:setState({ + showTreeView = newStatus, + }) + end + + self.onModelPreviewFrameEntered = function(...) + self.props.OnModelPreviewFrameEntered(...) + + self.onPreviewStatusChange(true, self.inTreeview) + end + + self.onModelPreviewFrameLeft = function(...) + self.props.OnModelPreviewFrameLeft(...) + + self.onPreviewStatusChange(false, self.inTreeview) + end + + self.onTreeviewEntered = function() + self.onPreviewStatusChange(self.inModelPreview, true) + end + + self.onTreeviewLeft = function() + self.onPreviewStatusChange(self.inModelPreview, false) + end + + self.onPreviewStatusChange = function(newModelStatus, newTreeStatus) + if (not self.inModelPreview and not self.inTreeview) and + (newModelStatus or newTreeStatus) then + -- Time to start the timer + self.cacheTime = FFlagPreviewControllerUseOsClock and os.clock() or elapsedTime() + end + + if (not newModelStatus and not newTreeStatus) and + (self.inModelPreview or self.inTreeview) then + local currentTime = FFlagPreviewControllerUseOsClock and os.clock() or elapsedTime() + local newTimeSpent = currentTime - self.cacheTime + if newTimeSpent > 0 then + self.totalTimeSpent = self.totalTimeSpent + math.floor(newTimeSpent * 1000) + end + end + + self.inModelPreview = newModelStatus + self.inTreeview = newTreeStatus + end + + self.onTreeItemClicked = function(instances) + if instances[1] then + self.props.OnTreeItemClicked(instances[1]) + end + end +end + +function PreviewController:render() + local props = self.props + local state = self.state + + local currentPreview = props.CurrentPreview + local previewModel = props.PreviewModel + local assetPreviewType = props.AssetPreviewType + local assetId = props.AssetId + local putTreeviewOnBottom = props.PutTreeviewOnBottom + local width = props.Width + local layoutOrder = props.LayoutOrder + + local isShowVideoPreview = FFlagEnableToolboxVideos and AssetType:isVideo(assetPreviewType) + local videoId + if isShowVideoPreview then + videoId = currentPreview.Video + end + + local showTreeView = state.showTreeView + local previewSize + local treeViewSize + local height + if showTreeView then + height = putTreeviewOnBottom and PREVIEW_HEIGHT + TREEVIEW_BOTTOM_HEIGHT or PREVIEW_HEIGHT + previewSize = putTreeviewOnBottom and UDim2.new(1, 0, 0, PREVIEW_HEIGHT) + or UDim2.new(1, -TREEVIEW_WIDTH, 0, PREVIEW_HEIGHT) + treeViewSize = putTreeviewOnBottom and UDim2.new(1, 0, 0, TREEVIEW_BOTTOM_HEIGHT) + or UDim2.new(0, TREEVIEW_WIDTH, 0, PREVIEW_HEIGHT) + else + height = PREVIEW_HEIGHT + previewSize = UDim2.new(1, 0, 0, PREVIEW_HEIGHT) + treeViewSize = UDim2.new() + end + + local showTreeViewButton = (not AssetType:isPlugin(assetPreviewType)) + if FFlagHideOneChildTreeviewButton then + local dataTree + if FFlagStudioAssetPreviewTreeFix2 then + dataTree = previewModel + else + dataTree = FFlagStudioFixTreeViewForFlatList and self.props.CurrentPreview or previewModel + end + local hasMultiplechildren = dataTree and (#dataTree:GetChildren() > 0) or false + showTreeViewButton = showTreeViewButton and hasMultiplechildren + end + + local onModelPreviewFrameEntered = self.onModelPreviewFrameEntered + local onModelPreviewFrameLeft = self.onModelPreviewFrameLeft + + local THUMBNAIL_HEIGHT = PREVIEW_HEIGHT < MODAL_MIN_WIDTH and PREVIEW_HEIGHT or MODAL_MIN_WIDTH + local showThumbnail = AssetType:isScript(assetPreviewType) or AssetType:isOtherType(assetPreviewType) + + local soundId + if AssetType:isAudio(assetPreviewType) and currentPreview then + -- It's wrong to get SoundId from currenttPreview, it should be previewModel. + soundId = currentPreview.SoundId + end + + local reportPlay = props.reportPlay + local reportPause = props.reportPause + + local isShowAudioPreview = AssetType:isAudio(assetPreviewType) + local mainViewButtonYOffset + if isShowAudioPreview then + mainViewButtonYOffset = 3 + else + mainViewButtonYOffset = MAINVIEW_BUTTONS_Y_OFFSET + end + + return Roact.createElement("Frame", { + Size = UDim2.new(1, width, 0, height), + + BackgroundTransparency = 1, + BorderSizePixel = 0, + + LayoutOrder = layoutOrder, + }, { + UIListLayout = Roact.createElement("UIListLayout", { + FillDirection = putTreeviewOnBottom and Enum.FillDirection.Vertical or Enum.FillDirection.Horizontal, + HorizontalAlignment = Enum.HorizontalAlignment.Left, + VerticalAlignment = Enum.VerticalAlignment.Top, + SortOrder = Enum.SortOrder.LayoutOrder, + }), + + MainView = Roact.createElement("Frame", { + Size = previewSize, + + BackgroundTransparency = 1, + BorderSizePixel = 0, + + LayoutOrder = 1, + }, { + PreviewLoading = AssetType:isLoading(assetPreviewType) and Roact.createElement(LoadingIndicator,{ + AnchorPoint = Vector2.new(0.5, 0.5), + Position = UDim2.new(0.5, 0, 0.5, 0), + }), + + ModelPreview = AssetType:isModel(assetPreviewType) and Roact.createElement(ModelPreview, { + TargetModel = currentPreview, + + OnModelPreviewFrameEntered = onModelPreviewFrameEntered, + OnModelPreviewFrameLeft = onModelPreviewFrameLeft, + }), + + ImagePreview = AssetType:isImage(assetPreviewType) and Roact.createElement(ImagePreview, { + ImageContent = getImage(currentPreview), + ScaleType = getImageScaleType(currentPreview), + }), + + AudioPreview = isShowAudioPreview and Roact.createElement(AudioPreview, { + SoundId = soundId or Urls.constructAssetIdString(assetId), + AssetId = assetId, + ShowTreeView = showTreeView, + ReportPlay = reportPlay, + ReportPause = reportPause, + }), + + VideoPreview = isShowVideoPreview and Roact.createElement(VideoPreview, { + VideoId = videoId or Urls.constructAssetIdString(assetId), + ShowTreeView = showTreeView, + }), + + PluginPreview = AssetType:isPlugin(assetPreviewType) and Roact.createElement("ImageLabel", { + Image = Urls.constructAssetThumbnailUrl(assetId, 420, 420), + Size = UDim2.new(0,THUMBNAIL_HEIGHT,0,THUMBNAIL_HEIGHT), + Position = UDim2.new(0.5,0,0,0), + AnchorPoint = Vector2.new(0.5,0), + }), + + -- Let the script and other share the same component for now + ThumbnailIconPreview = showThumbnail and Roact.createElement(ThumbnailIconPreview, { + TargetInstance = currentPreview, + AssetId = assetId, + ElementName = currentPreview.Name, + }), + + TreeViewButton = showTreeViewButton and Roact.createElement(TreeViewButton, { + Position = UDim2.new(1, MAINVIEW_BUTTONS_X_OFFSET, 1, mainViewButtonYOffset), + ZIndex = 2, + + ShowTreeView = state.showTreeView, + OnTreeviewStatusToggle = self.onTreeviewStatusToggle, + }) + }), + + TreeView = showTreeView and self:createTreeView(previewModel, treeViewSize) + }) +end + +return PreviewController diff --git a/BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Components/Preview/PreviewController.spec.lua b/BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Components/Preview/PreviewController.spec.lua new file mode 100644 index 0000000000..a8d1349fde --- /dev/null +++ b/BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Components/Preview/PreviewController.spec.lua @@ -0,0 +1,36 @@ +return function() + local Library = script.Parent.Parent.Parent + local Roact = require(Library.Parent.Parent.Roact) + + local MockWrapper = require(Library.MockWrapper) + + local PreviewController = require(Library.Components.Preview.PreviewController) + + local AssetType = require(Library.Utils.AssetType) + + -- PineTree + -- rbxassetid://183435411 + local function createTestAsset(container, name) + local assetId = 183435411 + local previewModel = Instance.new("Model") + + local element = Roact.createElement(MockWrapper, {}, { + PreviewController = Roact.createElement(PreviewController, { + width = 40, + + currentPreview = previewModel, + previewModel = previewModel, + assetPreviewType = AssetType.TYPES.ModelType, + assetId = assetId, + putTreeviewOnBottom = true, + }) + }) + + return Roact.mount(element, container or nil, name or "") + end + + it("should create and destroy without errors", function() + local instance = createTestAsset() + Roact.unmount(instance) + end) +end \ No newline at end of file diff --git a/BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Components/Preview/SearchLinkText.lua b/BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Components/Preview/SearchLinkText.lua new file mode 100644 index 0000000000..5a24fb81df --- /dev/null +++ b/BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Components/Preview/SearchLinkText.lua @@ -0,0 +1,141 @@ +--[[ + A component used for a link text with an animated search icon, + that appears when the user hovers over the link text. + + Necessary Properties: + Position = UDim2, the positon of this component. + AnchorPoint = Vector2, the centering of the component relative to its parent. + + Text = string, the Creator name to be shown in the text label. + OnClick = callback, A callback for when the link is clicked. + + Optional Properties: + number TweenTime = The time in seconds to play the hover animation. +]] + +local TweenService = game:GetService("TweenService") +local DEFAULT_TWEEN_TIME = 0.2 + +local Library = script.Parent.Parent.Parent + +local Roact = require(Library.Parent.Parent.Roact) + +local GetTextSize = require(Library.Utils.GetTextSize) + +local Theming = require(Library.Theming) +local withTheme = Theming.withTheme + +local SearchLinkText = Roact.PureComponent:extend("SearchLinkText") + +function SearchLinkText:init(props) + assert(type(props.OnClick) == "function", "SearchLinkText expects an 'OnClick' function.") + self.tweenInfo = TweenInfo.new(props.TweenTime or DEFAULT_TWEEN_TIME) + + self.textRef = Roact.createRef() + self.iconRef = Roact.createRef() + self.tweens = {} + + self.mouseEnter = function() + if self.tweens.TextEnter then + self.tweens.TextEnter:Play() + end + if self.tweens.IconEnter then + self.tweens.IconEnter:Play() + end + end + + self.mouseLeave = function() + if self.tweens.TextLeave then + self.tweens.TextLeave:Play() + end + if self.tweens.IconLeave then + self.tweens.IconLeave:Play() + end + end +end + +function SearchLinkText:didMount() + local text = self.textRef:getValue() + local icon = self.iconRef:getValue() + self.tweens.TextEnter = TweenService:Create(text, self.tweenInfo, { + Position = UDim2.fromScale(0, 0.5), + AnchorPoint = Vector2.new(0, 0.5), + }) + self.tweens.TextLeave = TweenService:Create(text, self.tweenInfo, { + Position = UDim2.fromScale(1, 0.5), + AnchorPoint = Vector2.new(1, 0.5), + }) + self.tweens.IconEnter = TweenService:Create(icon, self.tweenInfo, { + ImageTransparency = 0, + AnchorPoint = Vector2.new(1, 0.5), + }) + self.tweens.IconLeave = TweenService:Create(icon, self.tweenInfo, { + ImageTransparency = 1, + AnchorPoint = Vector2.new(0, 0.5), + }) +end + +function SearchLinkText:willUnmount() + for _, tween in pairs(self.tweens) do + tween:Cancel() + tween:Destroy() + end +end + +function SearchLinkText:render() + return withTheme(function(theme) + local props = self.props + local text = props.Text + local position = props.Position + local anchorPoint = props.AnchorPoint + + local searchLinkTextTheme = theme.assetPreview.description + + local textDimensions + local textExtents = GetTextSize(text, theme.assetPreview.textSizeLarge) + textDimensions = UDim2.fromOffset(textExtents.X, textExtents.Y) + + local fullWidth = textExtents.X + searchLinkTextTheme.searchBarIconSize + + searchLinkTextTheme.padding + + return Roact.createElement("TextButton", { + Size = UDim2.new(0, fullWidth, 1, 0), + Position = position, + AnchorPoint = anchorPoint, + BackgroundTransparency = 1, + + Text = "", + [Roact.Event.Activated] = props.OnClick, + [Roact.Event.MouseEnter] = self.mouseEnter, + [Roact.Event.MouseLeave] = self.mouseLeave, + }, { + Text = Roact.createElement("TextLabel", { + Size = textDimensions, + Position = UDim2.fromScale(1, 0.5), + AnchorPoint = Vector2.new(1, 0.5), + BackgroundTransparency = 1, + + Text = text, + Font = theme.assetPreview.font, + TextColor3 = searchLinkTextTheme.rightTextColor, + TextSize = theme.assetPreview.textSizeLarge, + [Roact.Ref] = self.textRef, + }), + + SearchIcon = Roact.createElement("ImageLabel", { + Size = UDim2.new(0, searchLinkTextTheme.searchBarIconSize, + 0, searchLinkTextTheme.searchBarIconSize), + Position = UDim2.fromScale(1, 0.5), + AnchorPoint = Vector2.new(0, 0.5), + BackgroundTransparency = 1, + ImageTransparency = 1, + + ImageColor3 = searchLinkTextTheme.rightTextColor, + Image = searchLinkTextTheme.images.searchIcon, + [Roact.Ref] = self.iconRef, + }), + }) + end) +end + +return SearchLinkText diff --git a/BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Components/Preview/SearchLinkText.spec.lua b/BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Components/Preview/SearchLinkText.spec.lua new file mode 100644 index 0000000000..c257b03a3e --- /dev/null +++ b/BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Components/Preview/SearchLinkText.spec.lua @@ -0,0 +1,51 @@ +return function() + local Library = script.Parent.Parent.Parent + local Roact = require(Library.Parent.Parent.Roact) + + local MockWrapper = require(Library.MockWrapper) + + local SearchLinkText = require(Library.Components.Preview.SearchLinkText) + + it("should expect an OnClick function", function() + local element = Roact.createElement(MockWrapper, {}, { + LinkText = Roact.createElement(SearchLinkText, { + Text = "Test", + }), + }) + expect(function() + local instance = Roact.mount(element) + Roact.unmount(instance) + end).to.throw() + end) + + it("should create and destroy without errors", function() + local element = Roact.createElement(MockWrapper, {}, { + LinkText = Roact.createElement(SearchLinkText, { + Text = "Test", + OnClick = function() + end, + }), + }) + local instance = Roact.mount(element) + Roact.unmount(instance) + end) + + it("should render correctly", function() + local element = Roact.createElement(MockWrapper, {}, { + LinkText = Roact.createElement(SearchLinkText, { + Text = "Test", + OnClick = function() + end, + }), + }) + + local container = Instance.new("Folder") + local instance = Roact.mount(element, container) + + local main = container:FindFirstChildOfClass("TextButton") + expect(main.Text).to.be.ok() + expect(main.SearchIcon).to.be.ok() + + Roact.unmount(instance) + end) +end diff --git a/BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Components/Preview/ThumbnailIconPreview.lua b/BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Components/Preview/ThumbnailIconPreview.lua new file mode 100644 index 0000000000..9770397bb2 --- /dev/null +++ b/BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Components/Preview/ThumbnailIconPreview.lua @@ -0,0 +1,91 @@ +--[[ + This component is the default shown for the 3D view in asset preview. This component shows the class + icon for that instance and the name of the element. + + Necessary properties: + Postion = UDim2 + Size = UDim2, this property determines the size of the preview. + ElementName = String, the name of the asset, this will be displayed below the icon. + TargetInstance = The instance to preview. + + Optional properties: + IconSize = number, will default to 16 unless otherwise specified, + this affects the dimensions of the icon representing the asset. + TextLabelHeight = number +]] + +local Library = script.Parent.Parent.Parent + +local Roact = require(Library.Parent.Parent.Roact) + +local Theming = require(Library.Theming) +local withTheme = Theming.withTheme + +local GetClassIcon = require(Library.Utils.GetClassIcon) + +local ThumbnailIconPreview = Roact.PureComponent:extend("ThumbnailIconPreview") + +function ThumbnailIconPreview:render() + return withTheme(function(theme) + local props = self.props + + local elementName = props.ElementName or "" + local instance = props.TargetInstance + local iconInfo = GetClassIcon(instance) + + local position = props.Position + local size = props.Size or UDim2.new(1, 0, 1, 0) + local thumbnailIconPreviewTheme = theme.assetPreview.thumbnailIconPreview + local iconSize = props.IconSize or thumbnailIconPreviewTheme.iconSize + local padding = thumbnailIconPreviewTheme.textLabelPadding + local textLabelHeight = props.TextLabelHeight or thumbnailIconPreviewTheme.defaultTextLabelHeight + + local layoutOrder = props.LayoutOrder + + return Roact.createElement("Frame", { + Position = position, + Size = size, + + BackgroundTransparency = 0, + BackgroundColor3 = thumbnailIconPreviewTheme.background, + BorderSizePixel = 0, + + LayoutOrder = layoutOrder, + },{ + UIListLayout = Roact.createElement("UIListLayout", { + SortOrder = Enum.SortOrder.LayoutOrder, + FillDirection = Enum.FillDirection.Vertical, + HorizontalAlignment = Enum.HorizontalAlignment.Center, + VerticalAlignment = Enum.VerticalAlignment.Center, + }), + + ImageContent = Roact.createElement("ImageLabel", { + Size = UDim2.new(0, iconSize, 0, iconSize), + + BackgroundTransparency = 1, + + Image = iconInfo.Image, + ImageRectSize = iconInfo.ImageRectSize, + ImageRectOffset = iconInfo.ImageRectOffset, + LayoutOrder = 1, + }), + + TextContent = Roact.createElement("TextLabel", { + Size = UDim2.new(1, -2 * padding, 0, textLabelHeight), + + Text = tostring(elementName), + TextColor3 = thumbnailIconPreviewTheme.textColor, + Font = theme.assetPreview.font, + TextSize = theme.assetPreview.textSize, + TextXAlignment = Enum.TextXAlignment.Center, + + BackgroundTransparency = 1, + LayoutOrder = 2, + }) + }) + end) +end + +return ThumbnailIconPreview + + diff --git a/BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Components/Preview/ThumbnailIconPreview.spec.lua b/BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Components/Preview/ThumbnailIconPreview.spec.lua new file mode 100644 index 0000000000..bbe29f9b00 --- /dev/null +++ b/BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Components/Preview/ThumbnailIconPreview.spec.lua @@ -0,0 +1,27 @@ +return function() + local Library = script.Parent.Parent.Parent + local Roact = require(Library.Parent.Parent.Roact) + + local MockWrapper = require(Library.MockWrapper) + + local ThumbnailIconPreview = require(Library.Components.Preview.ThumbnailIconPreview) + + local function createTestAsset(container, name) + local targetInstance = Instance.new("Script") + local element = Roact.createElement(MockWrapper, {}, { + Roact.createElement(ThumbnailIconPreview, { + TargetInstance = targetInstance, + TextContent = "ThumbnailIconPreviewTest", + Position = UDim2.new(1, 0, 1, 0), + Size = UDim2.new(0, 100, 0, 20) + }) + }) + + return Roact.mount(element, container or nil, name or "") + end + + it("should create and destroy without errors", function() + local instance = createTestAsset() + Roact.unmount(instance) + end) +end \ No newline at end of file diff --git a/BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Components/Preview/TreeViewButton.lua b/BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Components/Preview/TreeViewButton.lua new file mode 100644 index 0000000000..5a5688eb8f --- /dev/null +++ b/BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Components/Preview/TreeViewButton.lua @@ -0,0 +1,143 @@ +--[[ + This is a button component styled to represent toggling a tree view for Asset Preview + + Necessary properties: + Position = UDim2 + ZIndex = number, + ShowTreeView = boolean, represents whether or not the button is selected. + OnTreeviewStatusToggle = callback, this is thefunction that should be invoked by this button. + + Optionlal properties: + Size = number, This is the length and width of the button. +]] + +local Library = script.Parent.Parent.Parent + +local Roact = require(Library.Parent.Parent.Roact) + +local RoundButton = require(Library.Components.RoundFrame) + +local Theming = require(Library.Theming) +local withTheme = Theming.withTheme + +local MainViewButtons = Roact.PureComponent:extend("MainViewButtons") + +-- Determined by how many buttons we have and the padding. +local TOTAL_WIDTH = 28 +local TOTAL_HEIGHT = 28 + +local INLINE_PADDING = 6 + +local BUTTON_STATUS = { + default = 0, + hovered = 1, + disabled = 2, +} + +function MainViewButtons:init(props) + self.state = { + treeViewButtonStatus = BUTTON_STATUS.default, + } + + self.onTreeViewButtonActivated = function() + local newTreeViewStatus = not self.props.ShowTreeView + + self.props.OnTreeviewStatusToggle(newTreeViewStatus) + end + + self.onTreeViewButtonEnter = function() + self:setState({ + treeViewButtonStatus = BUTTON_STATUS.hovered + }) + end + + self.onTreeViewButtonLeave = function() + self:setState({ + treeViewButtonStatus = BUTTON_STATUS.default + }) + end +end + +local function getButtonBGColorAndTrans(buttonsTheme, buttonStatus, toggleStatus) + local buttonBGColor + local buttonTrans + + local defaultTrans = buttonsTheme.backgroundTrans + if toggleStatus then + defaultTrans = defaultTrans + 0.3 + end + + if buttonStatus == BUTTON_STATUS.default then + buttonBGColor = buttonsTheme.backgroundColor + buttonTrans = defaultTrans + elseif buttonStatus == BUTTON_STATUS.hovered then + buttonBGColor = buttonsTheme.backgroundColor + buttonTrans = defaultTrans + 0.3 + else -- BUTTON_STATUS.disabled + buttonBGColor = buttonsTheme.backgroundDisabledColor + buttonTrans = defaultTrans + end + + return buttonBGColor, buttonTrans +end + +function MainViewButtons:render() + return withTheme(function(theme) + local props = self.props + local state = self.state + + local buttonsTheme = theme.assetPreview.treeViewButton + + local position = props.Position or UDim2.new(1, 0, 1, 0) + local treeViewButtonSize = props.TreeViewButtonSize or buttonsTheme.buttonSize + + local treeViewButtonBGColor, treeViewbButtonTrans = getButtonBGColorAndTrans(buttonsTheme, + state.treeViewButtonStatus, + props.showTreeView) + + return Roact.createElement("Frame", { + Position = position, + AnchorPoint = Vector2.new(1, 1), + Size = UDim2.new(0, TOTAL_WIDTH, 0, TOTAL_HEIGHT), + ZIndex = props.ZIndex or 1, + + BackgroundTransparency = 1, + BorderSizePixel = 0, + },{ + UIListLayout = Roact.createElement("UIListLayout", { + FillDirection = Enum.FillDirection.Horizontal, + HorizontalAlignment = Enum.HorizontalAlignment.Right, + VerticalAlignment = Enum.VerticalAlignment.Center, + + SortOrder = Enum.SortOrder.LayoutOrder, + Padding = UDim.new(0, INLINE_PADDING), + }), + + TreeViewBGButton = Roact.createElement(RoundButton, { + Size = UDim2.new(0, treeViewButtonSize, 0, treeViewButtonSize), + + BackgroundTransparency = treeViewbButtonTrans, + BackgroundColor3 = treeViewButtonBGColor, + BorderSizePixel = 0, + + LayoutOrder = 1, + AutoButtonColor = false, + + OnActivated = self.onTreeViewButtonActivated, + OnMouseEnter = self.onTreeViewButtonEnter, + OnMouseLeave = self.onTreeViewButtonLeave, + }, { + TreeViewImageLabel = Roact.createElement("ImageLabel", { + AnchorPoint = Vector2.new(0.5, 0.5), + Position = UDim2.new(0.5, 0, 0.5, 0), + Size = UDim2.new(0, 16, 0, 16), + + Image = buttonsTheme.hierarchy, + BackgroundTransparency = 1, + }) + }), + }) + end) +end + +return MainViewButtons \ No newline at end of file diff --git a/BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Components/Preview/TreeViewButton.spec.lua b/BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Components/Preview/TreeViewButton.spec.lua new file mode 100644 index 0000000000..4906ad5a24 --- /dev/null +++ b/BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Components/Preview/TreeViewButton.spec.lua @@ -0,0 +1,26 @@ +return function() + local Library = script.Parent.Parent.Parent + local Roact = require(Library.Parent.Parent.Roact) + + local MockWrapper = require(Library.MockWrapper) + + local TreeViewButton = require(Library.Components.Preview.TreeViewButton) + + local function createTestAsset(container, name) + local element = Roact.createElement(MockWrapper, {}, { + Roact.createElement(TreeViewButton, { + Position = UDim2.new(1, 0, 1, 0), + + ShowTreeView = false, + OnTreeviewStatusToggle = nil, + }) + }) + + return Roact.mount(element, container or nil, name or "") + end + + it("should create and destroy without errors", function() + local instance = createTestAsset() + Roact.unmount(instance) + end) +end \ No newline at end of file diff --git a/BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Components/Preview/VideoPreview.lua b/BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Components/Preview/VideoPreview.lua new file mode 100644 index 0000000000..c27692db58 --- /dev/null +++ b/BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Components/Preview/VideoPreview.lua @@ -0,0 +1,271 @@ +--[[ + Provides logic for a VideoFrame and it's play/pause button, progress bar, and a time label. + + Required Props: + string VideoId: the Content string for VideoFrames. Should be formatted as a Content string "rbxassetid://123456". + boolean ShowTreeView: whether or not to show the TreeView button. It is used to + adjust the position of the time label and progress bar. + + Optional Props: + UDim2 LayoutOrder: The LayoutOrder of the component + UDim2 Position: The Position of the component + UDim2 Size: The Size of the component + callback OnPlay: Optional analytics call when clicking the play button + callback OnPause: Optional analytics call when clicking the pause button + + Props automatically received from wrapDraggableMedia(): + callback OnSliderInputChanged: Called when the progressbar slider input is changed. + callback OnSliderInputEnded: Called when the progressbar slider input ends. + + Props automatically received from wrapMedia(): + number _CurrentTime: The time in seconds that the media's TimePosition should currently be. + boolean _IsPlaying: Whether or not the Sound or VideoFrame should be currently playing. + callback _MediaPlayingUpdateSignal: Called when the media's Changed event is fired. Sets the isPlaying state. + callback _OnMediaEnded: Called when the media's Ended event is fired. Resets the currentTime & stops playing. + callback _Pause: Called when clicking the pause button. + callback _Play: Called when clicking the play button. + callBack _SetCurrentTime: Called if the currentTime has been changed, such as when moving a progressbar slider. +]] +local Library = script.Parent.Parent.Parent +local Roact = require(Library.Parent.Parent.Roact) + +local Theming = require(Library.Theming) +local withTheme = Theming.withTheme + +local wrapDraggableMedia = require(script.Parent.wrapDraggableMedia) + +local MediaControl = require(Library.Components.Preview.MediaControl) +local MediaProgressBar = require(Library.Components.Preview.MediaProgressBar) + +local PROGRESS_BAR_TOTAL_HEIGHT = 30 +local AUDIO_CONTROL_HEIGHT = 35 +local ICON_SIZE = 30 + +local VideoPreview = Roact.PureComponent:extend("VideoPreview") + +VideoPreview.defaultProps = { + Size = UDim2.new(1, 0, 1, 0), +} + +function VideoPreview:init() + self.layoutRef = Roact.createRef() + self.videoRef = Roact.createRef() + self.videoContainerRef = Roact.createRef() + + self.state = { + timeLength = 0, + isLoaded = false, + showOverlayPlayIcon = true, + resolution = Vector2.new(4, 3), + } + + self.dispatchMediaPlayingUpdate = function(updateType) + local videoObj = self.videoRef.current + if videoObj then + if updateType == "PLAY" then + videoObj:Play() + if self.props._OnPlay then + self.props._OnPlay() + end + elseif updateType == "PAUSE" then + videoObj:Pause() + if self.props._OnPause then + self.props._OnPause() + end + elseif updateType == "END" then + videoObj.Playing = false + videoObj.TimePosition = 0 + end + end + end + + self.onVideoPropertyChanged = function(rbx, property) + local videoObj = self.videoRef.current + if not videoObj or not self.isMounted then + return + end + if property == "TimeLength" then + self:setState({ + isLoaded = videoObj.IsLoaded, + timeLength = videoObj.TimeLength, + }) + self.props._SetTimeLength(videoObj.TimeLength) + elseif videoObj.IsLoaded ~= self.state.isLoaded then + self:setState({ + isLoaded = videoObj.IsLoaded, + }) + elseif property == "Resolution" then + self:setState({ + resolution = videoObj.Resolution, + }) + self.onResize() + end + end + + self.onResize = function() + local currentLayout = self.layoutRef.current + local videoFrame = self.videoRef.current + local videoContainer = self.videoContainerRef.current + if not videoFrame or not currentLayout or not videoContainer then + return + end + + local resolution = self.state.resolution + local height = videoContainer.AbsoluteSize.Y + local width = height * resolution.X / resolution.Y + if (currentLayout.AbsoluteContentSize.X < width) then + width = currentLayout.AbsoluteContentSize.X + height = width * resolution.Y / resolution.X + end + videoFrame.Size = UDim2.new(UDim.new(0, width), UDim.new(0, height)) + end + + self.onSliderInputChanged = function(newValue) + local videoFrame = self.videoRef.current + videoFrame.TimePosition = newValue or 0 + videoFrame.Playing = false + self:setState({ + showOverlayPlayIcon = false, + }) + + self.props._OnSliderInputChanged(newValue) + end + + self.onSliderInputEnded = function() + local videoFrameObj = self.videoRef.current + videoFrameObj.Playing = self.props._IsPlaying + self:setState({ + showOverlayPlayIcon = true, + }) + + self.props._OnSliderInputEnded() + end + + self.togglePlay = function() + if self.props._IsPlaying then + self.props._Pause() + else + self.props._Play() + end + end +end + +function VideoPreview:didMount() + self.isMounted = true + self.onResize() + self.mediaPlayingUpdateConnection = self.props._MediaPlayingUpdateSignal:connect(self.dispatchMediaPlayingUpdate) +end + +function VideoPreview:willUnmount() + self.isMounted = false + if self.mediaPlayingUpdateConnection then + self.mediaPlayingUpdateConnection:disconnect() + self.mediaPlayingUpdateConnection = nil + end +end + +function VideoPreview:render() + return withTheme(function(theme) + local VideoPreviewTheme = theme.assetPreview.videoPreview + + local props = self.props + local state = self.state + + local isLoaded = state.isLoaded + local timeLength = state.timeLength + local showOverlayPlayIcon = state.showOverlayPlayIcon + + local layoutOrder = props.LayoutOrder + local position = props.Position + local size = props.Size + local showTreeView = props.ShowTreeView + local videoId = props.VideoId + + -- Props passed from wrapDraggableMedia() and wrapMedia() + local currentTime = props._CurrentTime + local isPlaying = props._IsPlaying + local onMediaEnded = props._OnMediaEnded + local pause = props._Pause + local play = props._Play + + return Roact.createElement("Frame", { + BackgroundColor3 = VideoPreviewTheme.backgroundColor, + BackgroundTransparency = 0, + BorderSizePixel = 0, + LayoutOrder = layoutOrder, + Position = position, + Size = size, + }, { + UIListLayout = Roact.createElement("UIListLayout", { + FillDirection = Enum.FillDirection.Vertical, + SortOrder = Enum.SortOrder.LayoutOrder, + VerticalAlignment = Enum.VerticalAlignment.Center, + Padding = UDim.new(0, 10), + + [Roact.Change.AbsoluteContentSize] = self.onResize, + [Roact.Ref] = self.layoutRef, + }), + + VideoFrameButton = Roact.createElement("TextButton", { + AutoButtonColor = false, + BackgroundTransparency = 1, + BackgroundColor3 = VideoPreviewTheme.videoBackgroundColor, + LayoutOrder = 1, + Size = UDim2.new(1, 0, 1, -PROGRESS_BAR_TOTAL_HEIGHT - AUDIO_CONTROL_HEIGHT), + Text = "", + + [Roact.Ref] = self.videoContainerRef, + [Roact.Event.Activated] = self.togglePlay, + }, { + VideoFrameObj = Roact.createElement("VideoFrame", { + AnchorPoint = Vector2.new(0.5, 0), + BackgroundTransparency = 1, + Looped = false, + Position = UDim2.new(0.5, 0, 0, 0), + Size = UDim2.new(1, 0, 1, 0), + Video = videoId, + + [Roact.Ref] = self.videoRef, + [Roact.Event.Changed] = self.onVideoPropertyChanged, + [Roact.Event.Ended] = onMediaEnded, + }, { + PauseOverlay = (not isPlaying) and Roact.createElement("Frame", { + BackgroundColor3 = VideoPreviewTheme.pauseOverlayColor, + BackgroundTransparency = VideoPreviewTheme.pauseOverlayTransparency, + Size = UDim2.new(1, 0, 1, 0), + }, { + PlayVideoIcon = showOverlayPlayIcon and Roact.createElement("ImageLabel", { + AnchorPoint = Vector2.new(0.5, 0.5), + BackgroundTransparency = 1, + Image = VideoPreviewTheme.playButton, + Position = UDim2.new(0.5, 0, 0.5, 0), + Size = UDim2.new(0, ICON_SIZE, 0, ICON_SIZE), + }), + }), + }), + }), + + ProgressBar = Roact.createElement(MediaProgressBar, { + CurrentValue = currentTime, + LayoutOrder = 2, + Min = 0, + Max = timeLength, + OnValuesChanged = self.onSliderInputChanged, + OnInputEnded = self.onSliderInputEnded, + }), + + VideoControl = Roact.createElement(MediaControl, { + LayoutOrder = 3, + ShowTreeView = showTreeView, + IsPlaying = isPlaying, + IsLoaded = isLoaded, + OnPause = pause, + OnPlay = play, + TimeLength = timeLength, + TimePassed = currentTime, + }), + }) + end) +end + +return wrapDraggableMedia(VideoPreview) \ No newline at end of file diff --git a/BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Components/Preview/VideoPreview.spec.lua b/BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Components/Preview/VideoPreview.spec.lua new file mode 100644 index 0000000000..3878a7c0e5 --- /dev/null +++ b/BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Components/Preview/VideoPreview.spec.lua @@ -0,0 +1,26 @@ +return function() + local Library = script.Parent.Parent.Parent + local Roact = require(Library.Parent.Parent.Roact) + + local MockWrapper = require(Library.MockWrapper) + + local VideoPreview = require(Library.Components.Preview.VideoPreview) + + local function createTestAsset(container, name) + local element = Roact.createElement(MockWrapper, {}, { + Roact.createElement(VideoPreview, { + VideoId = 123, + ShowTreeView = false, + OnPlay = function() end, + OnPause = function() end, + }) + }) + + return Roact.mount(element, container or nil, name or "") + end + + it("should create and destroy without errors", function() + local instance = createTestAsset() + Roact.unmount(instance) + end) +end \ No newline at end of file diff --git a/BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Components/Preview/Vote.lua b/BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Components/Preview/Vote.lua new file mode 100644 index 0000000000..2f05a24b7b --- /dev/null +++ b/BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Components/Preview/Vote.lua @@ -0,0 +1,272 @@ +--[[ + This is the Vote component for AssetPreview component. + + Necessary properties: + Voting = table, a table contains the voting data + AssetId = num + OnVoteUpButtonActivated = callback, for the behavior when the Vote Up Button is clicked. + OnVoteDownButtonActivated = callback, for the behavior when the Vote Down Button is clicked. + + Optionlal properties: + Size = UDim2, + Position = UDim2, + layoutOrder = num +]] + +local Library = script.Parent.Parent.Parent + +local Roact = require(Library.Parent.Parent.Roact) + +local RoundButton = require(Library.Components.RoundFrame) +local RoundFrame = require(Library.Components.RoundFrame) + +local Theming = require(Library.Theming) +local withTheme = Theming.withTheme + +local Vote = Roact.PureComponent:extend("Vote") + +local INLINE_PADDING = 6 + +local BUTTON_STATUS = { + default = 0, + hovered = 1, +} + +function Vote:init(props) + self.state = { + voteUpStatus = BUTTON_STATUS.default, + voteDownStatus = BUTTON_STATUS.default, + } + + self.onVoteUpButtonActivated = function() + self.props.OnVoteUpButtonActivated(self.props.AssetId, self.props.Voting) + end + + self.onVoteDownButtonActivated = function() + self.props.OnVoteDownButtonActivated(self.props.AssetId, self.props.Voting) + end + + self.onVoteUpEnter = function() + self:setState({ + voteUpStatus = BUTTON_STATUS.hovered + }) + end + + self.onVoteUpLeave = function() + self:setState({ + voteUpStatus = BUTTON_STATUS.default + }) + end + + self.onVoteDownEnter = function() + self:setState({ + voteDownStatus = BUTTON_STATUS.hovered + }) + end + + self.onVoteDownLeave = function() + self:setState({ + voteDownStatus = BUTTON_STATUS.default + }) + end +end + +function Vote:render() + return withTheme(function(theme) + local props = self.props + local state = self.state + + local position = props.Position or UDim2.new(1, 0, 1, 0) + local size = props.Size or UDim2.new(0, 100, 0, 20) + + local voting = props.Voting + local canVote = voting.CanVote + local hasVoted = voting.HasVoted + local userVote = voting.UserVote + local upVotes = voting.UpVotes or 0 + local downVotes = voting.DownVotes or 0 + local totalVotes = 0 + local upVoteRate = 0 + + local voteTheme = theme.assetPreview.vote + + local voteUpBGColor = voteTheme.button.backgroundColor + local voteDownBGColor = voteTheme.button.backgroundColor + local voteUpBGTransparancy = voteTheme.button.backgroundTrans + local voteDownBGTransparancy = voteTheme.button.backgroundTrans + local voteUpStatus = state.voteUpStatus + local voteDownStatus = state.voteDownStatus + if not canVote then + voteUpBGColor = voteTheme.button.disabledColor + voteDownBGColor = voteTheme.button.disabledColor + else + if voteUpStatus == BUTTON_STATUS.hovered then + voteUpBGTransparancy = voteUpBGTransparancy + 0.3 + end + if voteDownStatus == BUTTON_STATUS.hovered then + voteDownBGTransparancy = voteDownBGTransparancy + 0.3 + end + + if hasVoted then + if userVote then + voteUpBGColor = voteTheme.voteUp.backgroundColor + else + voteDownBGColor = voteTheme.voteDown.backgroundColor + end + end + + totalVotes = upVotes + downVotes + if totalVotes > 0 then + upVoteRate = (upVotes / totalVotes) * 100 + end + end + + local layoutOrder = props.LayoutOrder + + return Roact.createElement(RoundFrame, { + Position = position, + Size = size, + + BackgroundTransparency = voteTheme.backgroundTrans, + BackgroundColor3 = voteTheme.background, + BorderColor3 = voteTheme.boderColor, + + LayoutOrder = layoutOrder, + },{ + VoteButtons = Roact.createElement("Frame", { + Position = UDim2.new(1, -64, 0, 0), + Size = UDim2.new(0, 64, 1, 0), + + BackgroundTransparency = 1, + BorderSizePixel = 0, + }, { + UIPadding = Roact.createElement("UIPadding", { + PaddingBottom = UDim.new(0, 0), + PaddingLeft = UDim.new(0, 0), + PaddingRight = UDim.new(0, INLINE_PADDING), + PaddingTop = UDim.new(0, 0), + }), + + UIListLayout = Roact.createElement("UIListLayout", { + FillDirection = Enum.FillDirection.Horizontal, + HorizontalAlignment = Enum.HorizontalAlignment.Right, + VerticalAlignment = Enum.VerticalAlignment.Center, + + SortOrder = Enum.SortOrder.LayoutOrder, + Padding = UDim.new(0, INLINE_PADDING), + }), + + + VoteDownButton = Roact.createElement(RoundButton, { + Active = canVote, + Size = UDim2.new(0, 28, 0, 28), + + BackgroundTransparency = voteDownBGTransparancy, + BackgroundColor3 = voteDownBGColor, + BorderColor3 = voteTheme.voteDown.borderColor, + + LayoutOrder = 1, + AutoButtonColor = false, + + OnActivated = self.onVoteDownButtonActivated, + OnMouseEnter = self.onVoteDownEnter, + OnMouseLeave = self.onVoteDownLeave, + }, { + VoteDownImageLabel = Roact.createElement("ImageLabel", { + Size = UDim2.new(1, 0, 1, 0), + + Image = voteTheme.images.voteDown, + BackgroundTransparency = 1, + }) + }), + + VoteUpButton = Roact.createElement(RoundButton, { + Active = canVote, + + Size = UDim2.new(0, 28, 0, 28), + + BackgroundTransparency = voteUpBGTransparancy, + BackgroundColor3 = voteUpBGColor, + BorderColor3 = voteTheme.voteUp.borderColor, + + LayoutOrder = 2, + AutoButtonColor = false, + + OnActivated = self.onVoteUpButtonActivated, + OnMouseEnter = self.onVoteUpEnter, + OnMouseLeave = self.onVoteUpLeave, + }, { + VoteUpImageLabel = Roact.createElement("ImageLabel", { + Size = UDim2.new(1, 0, 1, 0), + + Image = voteTheme.images.voteUp, + BackgroundTransparency = 1, + }) + }), + }), + + VoteInformations = Roact.createElement("Frame", { + Position = UDim2.new(0, INLINE_PADDING, 0, 0), + Size = UDim2.new(1, -64, 1, 0), + + BackgroundTransparency = 1, + BorderSizePixel = 0, + }, { + UIPadding = Roact.createElement("UIPadding", { + PaddingLeft = UDim.new(0, 6), + }), + + UIListLayout = Roact.createElement("UIListLayout", { + FillDirection = Enum.FillDirection.Horizontal, + HorizontalAlignment = Enum.HorizontalAlignment.Left, + VerticalAlignment = Enum.VerticalAlignment.Center, + + SortOrder = Enum.SortOrder.LayoutOrder, + Padding = UDim.new(0, 8), + }), + + VoteIcon = Roact.createElement("ImageLabel", { + Size = UDim2.new(0, 17, 0, 20), + BackgroundTransparency = 1, + Image = voteTheme.images.thumbUp, + }), + + VoteRatio = Roact.createElement("TextLabel", { + Size = UDim2.new(0, 28, 1, 0), + + TextXAlignment = Enum.TextXAlignment.Left, + TextYAlignment = Enum.TextYAlignment.Center, + + BackgroundTransparency = 1, + BorderSizePixel = 0, + + Text = ("%d%%"):format(upVoteRate), + TextSize = theme.assetPreview.textSizeLarge, + Font = theme.assetPreview.font, + TextColor3 = voteTheme.textColor, + + LayoutOrder = 1, + }), + + TotalVotes = Roact.createElement("TextLabel", { + Size = UDim2.new(1, 0, 1, 0), + + TextXAlignment = Enum.TextXAlignment.Left, + TextYAlignment = Enum.TextYAlignment.Center, + + BackgroundTransparency = 1, + BorderSizePixel = 0, + + Text = ("%d VOTES"):format(tostring(totalVotes)), + TextSize = theme.assetPreview.textSize, + Font = theme.assetPreview.font, + TextColor3 = voteTheme.subTextColor, + + LayoutOrder = 2, + }), + }), + }) + end) +end + +return Vote \ No newline at end of file diff --git a/BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Components/Preview/Vote.spec.lua b/BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Components/Preview/Vote.spec.lua new file mode 100644 index 0000000000..3c6cf34a54 --- /dev/null +++ b/BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Components/Preview/Vote.spec.lua @@ -0,0 +1,32 @@ +return function() + local Library = script.Parent.Parent.Parent + local Roact = require(Library.Parent.Parent.Roact) + + local MockWrapper = require(Library.MockWrapper) + + local Vote = require(Library.Components.Preview.Vote) + + local function createTestAsset(container, name) + local voting = { + CanVote = false, + HasVoted = false, + UserVote = false, + } + + local element = Roact.createElement(MockWrapper, {}, { + Vote = Roact.createElement(Vote, { + Position = UDim2.new(1, 0, 1, 0), + Size = UDim2.new(0, 100, 0, 20), + AssetId = 183435411, + Voting = voting, + }) + }) + + return Roact.mount(element, container or nil, name or "") + end + + it("should create and destroy without errors", function() + local instance = createTestAsset() + Roact.unmount(instance) + end) +end \ No newline at end of file diff --git a/BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Components/Preview/wrapDraggableMedia.lua b/BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Components/Preview/wrapDraggableMedia.lua new file mode 100644 index 0000000000..2d4cf2dc27 --- /dev/null +++ b/BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Components/Preview/wrapDraggableMedia.lua @@ -0,0 +1,67 @@ +--[[ + Wraps a component with logic required for a media's time interactable progressbar, play/pause button, + and time countdown. The wrapped component passes all props that wrapMedia in addition to interactable slider logic. + + Props automatically received from wrapMedia(): + boolean IsPlaying: Whether or not the Sound or VideoFrame is currently playing. + callback Pause: Called when clicking the pause button. + callback Play: Called when clicking the play button. + callBack SetCurrentTime: Called if the currentTime has been changed, such as when moving a progressbar slider. + + Returns: + callback _OnSliderInputChanged: Called when the progressbar slider input is changed. + callback _OnSliderInputEnded: Called when the progressbar slider input ends. + + number _CurrentTime: The time in seconds that the media's TimePosition should currently be. + boolean _IsPlaying: Whether or not the Sound or VideoFrame should be currently playing. + callback _MediaPlayingUpdateSignal: Called when the media's Changed event is fired. Sets the isPlaying state. + callback _OnMediaEnded: Called when the media's Ended event is fired. Resets the currentTime & stops playing. + callback _Pause: Called when clicking the pause button. + callback _Play: Called when clicking the play button. + callBack _SetCurrentTime: Called if the currentTime has been changed, such as when moving a progressbar slider. +]] +local Library = script.Parent.Parent.Parent +local Roact = require(Library.Parent.Parent.Roact) +local wrapMedia = require(script.Parent.wrapMedia) +local Immutable = require(Library.Utils.Immutable) + +local function wrapDraggableMedia(wrappedComponent) + local componentName = wrappedComponent and wrappedComponent.component and tostring(wrappedComponent.component) or "" + local DraggableMediaWrapper = Roact.PureComponent:extend(("DraggableMediaWrapper(%s)"):format(componentName)) + + function DraggableMediaWrapper:init() + self.isPlayingBeforeDrag = nil + + self.onSliderInputChanged = function(newValue) + local isPlaying = self.props._IsPlaying + if self.isPlayingBeforeDrag == nil then + self.isPlayingBeforeDrag = isPlaying + end + + if isPlaying then + self.props._Pause() + end + + self.props._SetCurrentTime(newValue) + end + + self.onSliderInputEnded = function() + if self.isPlayingBeforeDrag then + self.props._Play() + end + self.isPlayingBeforeDrag = nil + end + end + + function DraggableMediaWrapper:render() + local props = Immutable.JoinDictionaries(self.props, { + _OnSliderInputChanged = self.onSliderInputChanged, + _OnSliderInputEnded = self.onSliderInputEnded, + }) + return Roact.createElement(wrappedComponent, props) + end + + return wrapMedia(DraggableMediaWrapper) +end + +return wrapDraggableMedia \ No newline at end of file diff --git a/BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Components/Preview/wrapDraggableMedia.spec.lua b/BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Components/Preview/wrapDraggableMedia.spec.lua new file mode 100644 index 0000000000..af394c3897 --- /dev/null +++ b/BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Components/Preview/wrapDraggableMedia.spec.lua @@ -0,0 +1,38 @@ +return function() + local Library = script.Parent.Parent.Parent + local Roact = require(Library.Parent.Parent.Roact) + + local wrapDraggableMedia = require(script.Parent.wrapDraggableMedia) + + local function createTestComponent() + local TEST_WRAPPER = Roact.PureComponent:extend("TEST_WRAPPER") + function TEST_WRAPPER:render() + return Roact.createElement("Frame") + end + return TEST_WRAPPER + end + + it("should create and destroy without errors", function() + local element = wrapDraggableMedia(createTestComponent()) + local component = Roact.createElement(element, {}, {}) + local instance = Roact.mount(component) + + Roact.unmount(instance) + end) + + it("should add props into the component parameter", function() + local hasNewProps + local testComponent = createTestComponent() + function testComponent:init() + hasNewProps = (self.props._OnSliderInputChanged ~= nil) + hasNewProps = hasNewProps and (self.props._OnSliderInputEnded ~= nil) + end + + local element = wrapDraggableMedia(testComponent) + local component = Roact.createElement(element, {}, {}) + local instance = Roact.mount(component) + + Roact.unmount(instance) + expect(hasNewProps).to.be.equal(true) + end) +end \ No newline at end of file diff --git a/BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Components/Preview/wrapMedia.lua b/BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Components/Preview/wrapMedia.lua new file mode 100644 index 0000000000..78426ecd6c --- /dev/null +++ b/BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Components/Preview/wrapMedia.lua @@ -0,0 +1,116 @@ +--[[ + Wraps a component with logic required for a media's time progressbar, play/pause button, and time countdown. + + Returns: + number _CurrentTime: The time in seconds that the media's TimePosition should currently be. + boolean _IsPlaying: Whether or not the Sound or VideoFrame should be currently playing. + callback _MediaPlayingUpdateSignal: Should be called when the media's Changed event is fired. Sets the isPlaying state. + callback _OnMediaEnded: Should be called when the media's Ended event is fired. Resets the currentTime & stops playing. + callback _Pause: Should be called when clicking the pause button. + callback _Play: Should be called when clicking the play button. + callBack _SetCurrentTime: Should be called if the currentTime has been changed, such as when moving a progressbar slider. + callBack _SetTimeLength: Should be called if the timeLnegth has been changed, such as when a new audio or video is loaded. +]] +local RunService = game:GetService("RunService") + +local Library = script.Parent.Parent.Parent +local Roact = require(Library.Parent.Parent.Roact) + +local Signal = require(Library.Utils.Signal) +local Immutable = require(Library.Utils.Immutable) + +local function wrapMedia(wrappedComponent) + local MediaWrapper = Roact.PureComponent:extend(("MediaWrapper(%s)"):format(tostring(wrappedComponent))) + + function MediaWrapper:init() + self.state = { + currentTime = 0, + isPlaying = false, + timeLength = 0, + } + + self.mediaPlayingUpdateSignal = Signal.new() + + self.play = function() + self.mediaPlayingUpdateSignal:fire("PLAY") + self:setState({ + isPlaying = true, + }) + end + + self.pause = function() + self.mediaPlayingUpdateSignal:fire("PAUSE") + self:setState({ + isPlaying = false, + }) + end + + self.onMediaEnded = function() + self:setState({ + currentTime = 0, + isPlaying = false, + }) + end + + self.setCurrentTime = function(currentTime) + self:setState({ + currentTime = currentTime, + }) + end + + self.setTimeLength = function(timeLength) + self:setState({ + timeLength = timeLength, + }) + end + + self.onRenderStepped = function(deltaTime) + if not self.isMounted or not self.state.isPlaying then + return + end + + local newTime = self.state.currentTime + deltaTime + + if newTime >= self.state.timeLength then + self.onMediaEnded() + self.mediaPlayingUpdateSignal:fire("END") + else + self:setState({ + currentTime = newTime, + }) + end + end + end + + function MediaWrapper:didMount() + self.isMounted = true + self.runServiceConnection = RunService.RenderStepped:Connect(self.onRenderStepped) + end + + function MediaWrapper:willUnmount() + self.isMounted = false + if self.runServiceConnection then + self.runServiceConnection:Disconnect() + self.runServiceConnection = nil + end + end + + function MediaWrapper:render() + local props = Immutable.JoinDictionaries(self.props, { + _CurrentTime = self.state.currentTime, + _IsPlaying = self.state.isPlaying, + _MediaPlayingUpdateSignal = self.mediaPlayingUpdateSignal, + _OnMediaEnded = self.onMediaEnded, + _Play = self.play, + _Pause = self.pause, + _SetCurrentTime = self.setCurrentTime, + _SetTimeLength = self.setTimeLength, + }) + + return Roact.createElement(wrappedComponent, props) + end + + return MediaWrapper +end + +return wrapMedia \ No newline at end of file diff --git a/BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Components/Preview/wrapMedia.spec.lua b/BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Components/Preview/wrapMedia.spec.lua new file mode 100644 index 0000000000..374ec29804 --- /dev/null +++ b/BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Components/Preview/wrapMedia.spec.lua @@ -0,0 +1,49 @@ +return function() + local Library = script.Parent.Parent.Parent + local Roact = require(Library.Parent.Parent.Roact) + + local wrapMedia = require(script.Parent.wrapMedia) + + local function createTestComponent() + local TEST_WRAPPER = Roact.PureComponent:extend("TEST_WRAPPER") + function TEST_WRAPPER:render() + return Roact.createElement("Frame") + end + return TEST_WRAPPER + end + + it("should create and destroy without errors", function() + local element = wrapMedia(createTestComponent()) + local component = Roact.createElement(element, {}, {}) + local instance = Roact.mount(component) + + Roact.unmount(instance) + end) + + it("should add props into the component parameter", function() + local hasNewProps + local testComponent = createTestComponent() + function testComponent:init() + local expectedProps = { + "_CurrentTime", + "_IsPlaying", + "_MediaPlayingUpdateSignal", + "_OnMediaEnded", + "_Pause", + "_Play", + "_SetCurrentTime", + } + hasNewProps = (self.props._CurrentTime ~= nil) + for _,value in pairs(expectedProps) do + hasNewProps = hasNewProps and (self.props[value] ~= nil) + end + end + + local element = wrapMedia(testComponent) + local component = Roact.createElement(element, {}, {}) + local instance = Roact.mount(component) + + Roact.unmount(instance) + expect(hasNewProps).to.be.equal(true) + end) +end \ No newline at end of file diff --git a/BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Components/RadioButtons.lua b/BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Components/RadioButtons.lua new file mode 100644 index 0000000000..bb2cfa5907 --- /dev/null +++ b/BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Components/RadioButtons.lua @@ -0,0 +1,140 @@ +--[[ + A set of an arbitrary number of Radio buttons. Automatically scales to fit + the number of buttons contained in this component. + + Props: + Table Buttons: A table of buttons to use. + Format: {{Key = "Key1", Text = "Text1"}, ...} + string Selected = The current button that is selected. + Enum.FillDirection FillDirection = if the buttons should be in a vertical or horizontal layout + int LayoutOrder = The layout order of the frame, if in a Layout. + + function onButtonClicked(string key) = A callback for when a user selects a button. +]] + +local NO_WRAP = Vector2.new(1000000, 50) +local BUTTON_HEIGHT_SCALE = 0.4 + +local TextService = game:GetService("TextService") + +local Library = script.Parent.Parent + +local Roact = require(Library.Parent.Parent.Roact) + +local createFitToContent = require(Library.Components.createFitToContent) + +local Theming = require(Library.Theming) +local withTheme = Theming.withTheme + +local RadioButtons = Roact.PureComponent:extend("RadioButtons") + +function RadioButtons:init() + self.layoutRef = Roact.createRef() + self.containerRef = Roact.createRef() + self.currentLayout = 0 + + self.onButtonClicked = function(key, index) + if self.props.onButtonClicked then + self.props.onButtonClicked(key, index) + end + end +end + +function RadioButtons:createButton(key, text, index, selected, theme) + local textWidth = TextService:GetTextSize(text, theme.radioButton.textSize, theme.radioButton.font, NO_WRAP).X + local buttonHeight = theme.radioButton.buttonHeight + + local buttonSize = UDim2.new(1, 0, 0, buttonHeight) + if self.props.FillDirection == Enum.FillDirection.Horizontal then + buttonSize = UDim2.new(0, textWidth + buttonHeight, 0, buttonHeight) + end + + return Roact.createElement("Frame", { + LayoutOrder = self:nextLayout(), + BackgroundTransparency = 1, + Size = buttonSize, + }, { + UIListLayout = Roact.createElement("UIListLayout", { + FillDirection = Enum.FillDirection.Horizontal, + VerticalAlignment = Enum.VerticalAlignment.Center, + SortOrder = Enum.SortOrder.LayoutOrder, + Padding = UDim.new(0, theme.radioButton.buttonPadding), + }), + + Background = Roact.createElement("ImageButton", { + LayoutOrder = 1, + Size = UDim2.new(0, buttonHeight, 0, buttonHeight), + BackgroundTransparency = 1, + ImageColor3 = theme.radioButton.radioButtonColor, + Image = theme.radioButton.radioButtonBackground, + + [Roact.Event.Activated] = function() + self.onButtonClicked(key, index) + end, + }, { + Highlight = selected and Roact.createElement("ImageLabel", { + Size = UDim2.new(BUTTON_HEIGHT_SCALE, 0, BUTTON_HEIGHT_SCALE, 0), + AnchorPoint = Vector2.new(0.5, 0.5), + Position = UDim2.new(0.5, 0, 0.5, 0), + BackgroundTransparency = 1, + Image = theme.radioButton.radioButtonSelected, + }), + }), + + Text = Roact.createElement("TextButton", { + LayoutOrder = 2, + Text = text, + Size = UDim2.new(0, textWidth, 1, 0), + BackgroundTransparency = 1, + Font = theme.radioButton.font, + TextSize = theme.radioButton.textSize, + TextColor3 = theme.radioButton.textColor, + TextXAlignment = Enum.TextXAlignment.Left, + + [Roact.Event.Activated] = function() + self.onButtonClicked(key, index) + end, + }), + }) +end + +function RadioButtons:resetLayout() + self.currentLayout = 0 +end + +function RadioButtons:nextLayout() + self.currentLayout = self.currentLayout + 1 + return self.currentLayout +end + +function RadioButtons:render() + return withTheme(function(theme) + local props = self.props + + local buttons = props.Buttons + local layoutOrder = props.LayoutOrder + local selected = props.Selected + local fillDirection = props.FillDirection + + local fitToContent = createFitToContent("Frame", "UIListLayout", { + FillDirection = fillDirection or Enum.FillDirection.Vertical, + Padding = UDim.new(0, theme.radioButton.contentPadding), + SortOrder = Enum.SortOrder.LayoutOrder, + }) + + self:resetLayout() + + local children = {} + for index, button in ipairs(buttons) do + children[button.Key] = self:createButton(button.Key, button.Text, index, + selected == button.Key, theme) + end + + return Roact.createElement(fitToContent, { + BackgroundTransparency = 1, + LayoutOrder = layoutOrder + }, children) + end) +end + +return RadioButtons diff --git a/BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Components/RadioButtons.spec.lua b/BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Components/RadioButtons.spec.lua new file mode 100644 index 0000000000..eb1f95c71e --- /dev/null +++ b/BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Components/RadioButtons.spec.lua @@ -0,0 +1,47 @@ +return function() + local Library = script.Parent.Parent + local Roact = require(Library.Parent.Parent.Roact) + + local MockWrapper = require(Library.MockWrapper) + + local RadioButtons = require(script.Parent.RadioButtons) + + it("should create and destroy without errors", function() + local element = Roact.createElement(MockWrapper, {}, { + Buttons = Roact.createElement(RadioButtons, { + Buttons = { + {Key = "Button1", Text = "Button 1"}, + {Key = "Button2", Text = "Button 2"}, + }, + Selected = "Button1", + }), + }) + local instance = Roact.mount(element) + Roact.unmount(instance) + end) + + it("should render correctly", function() + local element = Roact.createElement(MockWrapper, {}, { + Buttons = Roact.createElement(RadioButtons, { + Buttons = { + {Key = "Button1", Text = "Button 1"}, + {Key = "Button2", Text = "Button 2"}, + }, + Selected = "Button1", + }), + }) + + local container = Instance.new("Folder") + local instance = Roact.mount(element, container, "Buttons") + + local buttons = container.Buttons + expect(buttons.Layout).to.be.ok() + expect(buttons.Button1).to.be.ok() + expect(buttons.Button1.UIListLayout).to.be.ok() + expect(buttons.Button1.Background).to.be.ok() + expect(buttons.Button1.Background.Highlight).to.be.ok() + expect(buttons.Button1.Text).to.be.ok() + + Roact.unmount(instance) + end) +end diff --git a/BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Components/RoundFrame.lua b/BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Components/RoundFrame.lua new file mode 100644 index 0000000000..b781c5ab08 --- /dev/null +++ b/BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Components/RoundFrame.lua @@ -0,0 +1,107 @@ +--[[ + An element with rounded corners. + Designed to function almost identically to a standard roblox Frame. + + Props: + Color3 BackgroundColor3 = The background color of the frame. + float BackgroundTransparency = The transparency of the frame's background and border. + Color3 BorderColor = The border color of the frame. + int BorderSizePixel: + If == 0, the border will not render. If ~= 0, the border will render. + + UDim2 Size = The size of the frame. + UDim2 Position = The position of the frame. + Vector2 AnchorPoint = The center point of this frame. + int LayoutOrder = The layout order of the frame, if in a Layout. + int ZIndex = The draw index of the frame. + + function OnActivated = A callback fired when the user clicks the frame. + function OnMouseEnter = A callback fired when the mouse enters the frame. + function OnMouseLeave = A callback fired when the mouse leaves the frame. + + [Roact.Change.AbsoluteSize] = An event that fires when the frame's AbsoluteSize changes + [Roact.Change.AbsolutePosition] = An event that fires when the frame's AbsolutePosition changes +]] + +local Library = script.Parent.Parent + +local Roact = require(Library.Parent.Parent.Roact) + +local Theming = require(Library.Theming) +local withTheme = Theming.withTheme + +local ROUNDED_FRAME_SLICE = Rect.new(3, 3, 13, 13) +local DEFAULT_BORDER_COLOR = Color3.fromRGB(27, 42, 53) +local DEFAULT_SIZE = UDim2.new(0, 100, 0, 100) + +local RoundFrame = Roact.PureComponent:extend("RoundFrame") + +function RoundFrame:init(initialProps) + local isButton = initialProps.OnActivated ~= nil + self.elementType = isButton and "ImageButton" or "ImageLabel" +end + +function RoundFrame:render() + return withTheme(function(theme) + local props = self.props + local roundFrameTheme = theme.roundFrame + + local backgroundColor = props.BackgroundColor3 + local backgroundTransparency = props.BackgroundTransparency + local borderColor = props.BorderColor3 or DEFAULT_BORDER_COLOR + local borderSize = props.BorderSizePixel or 1 + local size = props.Size or DEFAULT_SIZE + local position = props.Position + local anchorPoint = props.AnchorPoint + local layoutOrder = props.LayoutOrder + local zindex = props.ZIndex + local activatedCallback = props.OnActivated + local mouseEnterCallback = props.OnMouseEnter + local mouseLeaveCallback = props.OnMouseLeave + + local borderTransparency + if borderSize == 0 then + borderTransparency = 1 + else + borderTransparency = backgroundTransparency + end + + return Roact.createElement(self.elementType, { + Size = size, + Position = position, + AnchorPoint = anchorPoint, + LayoutOrder = layoutOrder, + ZIndex = zindex, + + BackgroundTransparency = 1, + ImageColor3 = backgroundColor, + ImageTransparency = backgroundTransparency, + + Image = roundFrameTheme.backgroundImage, + ScaleType = Enum.ScaleType.Slice, + SliceCenter = ROUNDED_FRAME_SLICE, + + [Roact.Event.MouseEnter] = mouseEnterCallback, + [Roact.Event.MouseLeave] = mouseLeaveCallback, + [Roact.Event.Activated] = activatedCallback, + + [Roact.Change.AbsoluteSize] = props[Roact.Change.AbsoluteSize], + [Roact.Change.AbsolutePosition] = props[Roact.Change.AbsolutePosition], + }, { + Border = Roact.createElement("ImageLabel", { + Size = UDim2.new(1, 0, 1, 0), + + BackgroundTransparency = 1, + ImageColor3 = borderColor, + ImageTransparency = borderTransparency, + + Image = roundFrameTheme.borderImage, + ScaleType = Enum.ScaleType.Slice, + SliceCenter = ROUNDED_FRAME_SLICE, + SliceScale = borderSize, + }, props[Roact.Children]) + }) + end) +end + +return RoundFrame diff --git a/BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Components/RoundFrame.spec.lua b/BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Components/RoundFrame.spec.lua new file mode 100644 index 0000000000..b7c5babed6 --- /dev/null +++ b/BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Components/RoundFrame.spec.lua @@ -0,0 +1,79 @@ +return function() + local Library = script.Parent.Parent + local Roact = require(Library.Parent.Parent.Roact) + + local MockWrapper = require(Library.MockWrapper) + + local RoundFrame = require(script.Parent.RoundFrame) + + local function createTestRoundFrame(props, children) + return Roact.createElement(MockWrapper, {}, { + RoundFrame = Roact.createElement(RoundFrame, props, children) + }) + end + + it("should create and destroy without errors", function() + local element = createTestRoundFrame() + local instance = Roact.mount(element) + Roact.unmount(instance) + end) + + it("should render correctly", function () + local container = Instance.new("Folder") + local instance = Roact.mount(createTestRoundFrame(), container) + local frame = container:FindFirstChildOfClass("ImageLabel") + + expect(frame).to.be.ok() + expect(frame.Border).to.be.ok() + + Roact.unmount(instance) + end) + + it("should hide its border if BorderSizePixel == 0", function () + local container = Instance.new("Folder") + local instance = Roact.mount(createTestRoundFrame({ + BorderSizePixel = 0, + }), container) + local frame = container:FindFirstChildOfClass("ImageLabel") + + expect(frame.Border.ImageTransparency).to.equal(1) + + Roact.unmount(instance) + end) + + it("should be an ImageLabel if OnActivated is undefined", function () + local container = Instance.new("Folder") + local instance = Roact.mount(createTestRoundFrame(), container) + local frame = container:FindFirstChildOfClass("ImageLabel") + + expect(frame:IsA("ImageLabel")).to.equal(true) + + Roact.unmount(instance) + end) + + it("should be an ImageButton if OnActivated is defined", function () + local container = Instance.new("Folder") + local instance = Roact.mount(createTestRoundFrame({ + OnActivated = function() + end, + }), container) + local frame = container:FindFirstChildOfClass("ImageButton") + + expect(frame:IsA("ImageButton")).to.equal(true) + + Roact.unmount(instance) + end) + + it("should accept children", function () + local container = Instance.new("Folder") + local instance = Roact.mount(createTestRoundFrame({}, { + Child = Roact.createElement("Frame"), + }), container) + local frame = container:FindFirstChildOfClass("ImageLabel") + + expect(frame.Border).to.be.ok() + expect(frame.Border.Child).to.be.ok() + + Roact.unmount(instance) + end) +end \ No newline at end of file diff --git a/BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Components/RoundTextBox.lua b/BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Components/RoundTextBox.lua new file mode 100644 index 0000000000..2150859fde --- /dev/null +++ b/BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Components/RoundTextBox.lua @@ -0,0 +1,194 @@ +--[[ + A TextBox with rounded corners that allows single-line or multiline entry, + maximum character count, and error messages. + + Props: + bool Active = Whether this component can be interacted with. + int MaxLength = The maximum number of characters allowed in the TextBox. + bool Multiline = Whether this TextBox allows a single line of text or multiple. + int Height = The vertical size of this TextBox, in pixels. + int WidthOffset = the horizontal offset size of this TextBox, in pixels. + int LayoutOrder = The sort order of this component in a UIListLayout. + int TextSize = The size of text + + boolean ErrorBorder = puts red border around text box + string ErrorMessage = A general override message used to display an error. A non-nil ErrorMessage will border the TextBox in red. + + string Text = The text to display in the TextBox + string PlaceholderText = text to display when TextBox is empty/in default state + boolean ShowToolTip = do we want to show anything beneath the rounded text box (defaults to true) + boolean ShowErrors = do we want to show any error text beneath the rounded text box, or change the border to indicate an error (defaults to true) + function SetText(text) = Callback to tell parent that text has changed + function FocusChanged(focused) = Callback when this TextBox is focused. + function HoverChanged(hovering) = Callback when the mouse enters or leaves this TextBox. +]] + +local StudioUILibraryRoundTextBoxNoTooltip = settings():GetFFlag("StudioUILibraryRoundTextBoxNoTooltip") + +local DEFAULT_HEIGHT = 42 +local PADDING = UDim.new(0, 10) + +local Library = script.Parent.Parent +local Roact = require(Library.Parent.Parent.Roact) +local Theming = require(Library.Theming) +local withTheme = Theming.withTheme + +local TextEntry = require(Library.Components.TextEntry) +local MultilineTextEntry = require(Library.Components.MultilineTextEntry) + +local RoundTextBox = Roact.PureComponent:extend("RoundTextBox") + +function RoundTextBox:init() + self.state = { + Focused = false, + } + + self.focusChanged = function(focused) + if self.props.Active then + if self.props.FocusChanged then + self.props.FocusChanged(focused) + end + self:setState({ + Focused = focused, + }) + end + end + + self.mouseHoverChanged = function(hovering) + if self.props.Active then + if self.state.Focused and self.props.HoverChanged then + self.props.HoverChanged(hovering) + end + end + end +end + +function RoundTextBox:render() + return withTheme(function(theme) + local active = self.props.Active + local focused = self.state.Focused + local multiline = self.props.Multiline + local textLength = utf8.len(self.props.Text) + local pastMaxLength = self.props.MaxLength and textLength > self.props.MaxLength + local errorState = self.props.ErrorMessage + or pastMaxLength + + if StudioUILibraryRoundTextBoxNoTooltip then + errorState = errorState or self.props.ErrorBorder + end + + local size = self.props.Size or UDim2.new(1, self.props.WidthOffset or 0, 0, self.props.Height or DEFAULT_HEIGHT) + + local backgroundProps = { + -- Necessary to make the rounded background + BackgroundTransparency = 1, + Image = theme.roundFrame.backgroundImage, + ImageTransparency = 0, + ImageColor3 = active and theme.textBox.background or theme.textBox.disabled, + ScaleType = Enum.ScaleType.Slice, + SliceCenter = theme.roundFrame.slice, + + Position = UDim2.new(0, 0, 0, 0), + Size = size, + + LayoutOrder = self.props.LayoutOrder or 1, + } + + local showToolTip = true + if StudioUILibraryRoundTextBoxNoTooltip then + if nil ~= self.props.ShowToolTip then + showToolTip = self.props.ShowToolTip + end + end + + local tooltipText + if active then + if StudioUILibraryRoundTextBoxNoTooltip then + if showToolTip then + if errorState and self.props.ErrorMessage then + tooltipText = self.props.ErrorMessage + else + tooltipText = textLength .. "/" .. self.props.MaxLength + end + else + tooltipText = "" + end + else + if errorState and self.props.ErrorMessage then + tooltipText = self.props.ErrorMessage + else + tooltipText = textLength .. "/" .. self.props.MaxLength + end + end + else + tooltipText = "" + end + + local borderColor + if active then + if errorState then + borderColor = theme.textBox.error + elseif focused then + borderColor = theme.textBox.borderHover + else + borderColor = theme.textBox.borderDefault + end + else + borderColor = theme.textBox.borderDefault + end + + local textEntryProps = { + Visible = self.props.Active, + Text = self.props.Text, + PlaceholderText = self.props.PlaceholderText, + FocusChanged = self.focusChanged, + HoverChanged = self.mouseHoverChanged, + SetText = self.props.SetText, + TextColor3 = theme.textBox.text, + Font = theme.textBox.font, + TextSize = self.props.TextSize, + } + + local textEntry + if multiline then + textEntry = Roact.createElement(MultilineTextEntry, textEntryProps) + else + textEntry = Roact.createElement(TextEntry, textEntryProps) + end + + return Roact.createElement("ImageLabel", backgroundProps, { + Tooltip = Roact.createElement("TextLabel", { + BackgroundTransparency = 1, + Position = UDim2.new(0, 2, 1, 2), + Size = UDim2.new(1, 0, 0, 10), + + Font = Enum.Font.SourceSans, + TextSize = 16, + TextXAlignment = Enum.TextXAlignment.Left, + TextYAlignment = Enum.TextYAlignment.Top, + TextColor3 = (active and errorState and theme.textBox.error) or theme.textBox.tooltip, + Text = tooltipText, + Visible = (not StudioUILibraryRoundTextBoxNoTooltip) or showToolTip + }), + + Border = Roact.createElement("ImageLabel", { + Size = UDim2.new(1, 0, 1, 0), + BackgroundTransparency = 1, + Image = theme.roundFrame.borderImage, + ImageColor3 = borderColor, + ScaleType = Enum.ScaleType.Slice, + SliceCenter = theme.roundFrame.slice, + }, { + Padding = Roact.createElement("UIPadding", { + PaddingLeft = PADDING, + PaddingRight = PADDING, + PaddingTop = PADDING, + PaddingBottom = PADDING, + }), + Text = textEntry, + }), + }) + end) +end + +return RoundTextBox diff --git a/BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Components/RoundTextBox.spec.lua b/BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Components/RoundTextBox.spec.lua new file mode 100644 index 0000000000..356cfa3221 --- /dev/null +++ b/BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Components/RoundTextBox.spec.lua @@ -0,0 +1,71 @@ +return function() + local Library = script.Parent.Parent + local Roact = require(Library.Parent.Parent.Roact) + + local MockWrapper = require(Library.MockWrapper) + + local RoundTextBox = require(script.Parent.RoundTextBox) + + local function createTestRoundTextBox(active, errorMessage) + return Roact.createElement(MockWrapper, {}, { + RoundTextbox = Roact.createElement(RoundTextBox, { + Active = active, + MaxLength = 50, + Multiline = false, + Text = "Text", + ErrorMessage = errorMessage, + }), + }) + end + + it("should create and destroy without errors", function() + local element = createTestRoundTextBox(true) + local instance = Roact.mount(element) + Roact.unmount(instance) + end) + + it("should render correctly", function () + local container = Instance.new("Folder") + local instance = Roact.mount(createTestRoundTextBox(true), container) + local background = container:FindFirstChildOfClass("ImageLabel") + + expect(background.Border).to.be.ok() + expect(background.Border.Padding).to.be.ok() + expect(background.Border.Text).to.be.ok() + expect(background.Tooltip).to.be.ok() + + Roact.unmount(instance) + end) + + describe("Tooltip", function() + it("should show the correct length of the text", function () + local container = Instance.new("Folder") + local instance = Roact.mount(createTestRoundTextBox(true), container) + local background = container:FindFirstChildOfClass("ImageLabel") + + expect(background.Tooltip.Text).to.equal("4/50") + + Roact.unmount(instance) + end) + + it("should show an error message if one exists", function () + local container = Instance.new("Folder") + local instance = Roact.mount(createTestRoundTextBox(true, "Error"), container) + local background = container:FindFirstChildOfClass("ImageLabel") + + expect(background.Tooltip.Text).to.equal("Error") + + Roact.unmount(instance) + end) + + it("should be empty if component is inactive", function () + local container = Instance.new("Folder") + local instance = Roact.mount(createTestRoundTextBox(false), container) + local background = container:FindFirstChildOfClass("ImageLabel") + + expect(background.Tooltip.Text).to.equal("") + + Roact.unmount(instance) + end) + end) +end \ No newline at end of file diff --git a/BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Components/RoundTextButton.lua b/BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Components/RoundTextButton.lua new file mode 100644 index 0000000000..ccc20fade6 --- /dev/null +++ b/BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Components/RoundTextButton.lua @@ -0,0 +1,118 @@ +--[[ + A button with rounded corners. + + Supports one of two styles: + "Blue": A blue button with white text and no border. + "White": A white button with black text and a black border. + + Props: + bool Active = Whether or not this button can be clicked. + UDim2 Size = UDim2.new(0, Constants.BUTTON_WIDTH, 0, Constants.BUTTON_HEIGHT) + int LayoutOrder = The order this RoundTextButton will sort to when placed in a UIListLayout. + string Name = The text to display in this Button. + function OnClicked = The function that will be called when this button is clicked. + variant Value = Data that can be accessed from the OnClicked callback. + int TextSize = The size of text + table Style = { + ButtonColor, + ButtonColor_Hover, + ButtonColor_Disabled, + TextColor, + TextColor_Disabled, + BorderColor, + } +]] + +local Library = script.Parent.Parent +local Roact = require(Library.Parent.Parent.Roact) +local Theming = require(Library.Theming) +local withTheme = Theming.withTheme + +local BUTTON_WIDTH = 125 +local BUTTON_HEIGHT = 35 + +local RoundTextButton = Roact.PureComponent:extend("RoundTextButton") + +function RoundTextButton:init() + self.state = { + Hovering = false, + } + + self.mouseEnter = function() + self:setState({ + Hovering = true, + }) + end + + self.mouseLeave = function() + self:setState({ + Hovering = false, + }) + end +end + +function RoundTextButton:render() + return withTheme(function(theme) + local active = self.props.Active + local hovering = self.state.Hovering + local style = self.props.Style + local match = self.props.BorderMatchesBackground + local textSize = self.props.TextSize + + local backgroundProps = { + -- Necessary to make the rounded background + BackgroundTransparency = 1, + Image = theme.roundFrame.backgroundImage, + ImageTransparency = 0, + ScaleType = Enum.ScaleType.Slice, + SliceCenter = theme.roundFrame.slice, + + Position = self.props.Position or UDim2.new(0, 0, 0, 0), + Size = self.props.Size or UDim2.new(0, BUTTON_WIDTH, 0, BUTTON_HEIGHT), + AnchorPoint = self.props.AnchorPoint or Vector2.new(0, 0), + + LayoutOrder = self.props.LayoutOrder or 1, + ZIndex = self.props.ZIndex or 1, + + [Roact.Event.MouseEnter] = self.mouseEnter, + [Roact.Event.MouseLeave] = self.mouseLeave, + + [Roact.Event.Activated] = function() + if active then + self.props.OnClicked(self.props.Value) + end + end, + } + + if active then + backgroundProps.ImageColor3 = hovering and style.ButtonColor_Hover or style.ButtonColor + else + backgroundProps.ImageColor3 = style.ButtonColor_Disabled + end + + return Roact.createElement("ImageButton", backgroundProps, { + Border = Roact.createElement("ImageLabel", { + Size = UDim2.new(1, 0, 1, 0), + BackgroundTransparency = 1, + Image = theme.roundFrame.borderImage, + ImageColor3 = match and backgroundProps.ImageColor3 or style.BorderColor, + ScaleType = Enum.ScaleType.Slice, + SliceCenter = theme.roundFrame.slice, + ZIndex = self.props.ZIndex or 1, + }), + + Text = Roact.createElement("TextLabel", { + Size = UDim2.new(1, 0, 1, 0), + BackgroundTransparency = 1, + BorderSizePixel = 0, + Font = theme.textButton.font, + TextColor3 = active and style.TextColor or style.TextColor_Disabled, + TextSize = textSize, + Text = self.props.Name, + ZIndex = self.props.ZIndex or 1, + }), + }) + end) +end + +return RoundTextButton diff --git a/BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Components/RoundTextButton.spec.lua b/BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Components/RoundTextButton.spec.lua new file mode 100644 index 0000000000..cdd2496bb8 --- /dev/null +++ b/BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Components/RoundTextButton.spec.lua @@ -0,0 +1,35 @@ +return function() + local Library = script.Parent.Parent + local Roact = require(Library.Parent.Parent.Roact) + + local MockWrapper = require(Library.MockWrapper) + + local RoundTextButton = require(script.Parent.RoundTextButton) + + local function createTestRoundTextButton() + return Roact.createElement(MockWrapper, {}, { + RoundTextButton = Roact.createElement(RoundTextButton, { + Active = true, + Style = {}, + Name = "Name", + }), + }) + end + + it("should create and destroy without errors", function() + local element = createTestRoundTextButton() + local instance = Roact.mount(element) + Roact.unmount(instance) + end) + + it("should render correctly", function () + local container = Instance.new("Folder") + local instance = Roact.mount(createTestRoundTextButton(), container) + local button = container:FindFirstChildOfClass("ImageButton") + + expect(button.Border).to.be.ok() + expect(button.Text).to.be.ok() + + Roact.unmount(instance) + end) +end \ No newline at end of file diff --git a/BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Components/SearchBar.lua b/BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Components/SearchBar.lua new file mode 100644 index 0000000000..5f2673e5ce --- /dev/null +++ b/BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Components/SearchBar.lua @@ -0,0 +1,483 @@ +--[[ + Search Bar Component + + Implements a search bar component with a text box that dynamically moves as you type, and searches after a delay. + + Props: + UDim2 Size: size of the searchBar + number LayoutOrder = 0 : optional layout order for UI layouts + number TextSearchDelay : optional delay when text changes before requesting search, in ms + string DefaultText : default text to show in the empty search bar. + bool Enabled : searchbar is enabled or not + bool Rounded : searchbar has rounded corners + bool EnableFocus : if the searchbar borders becomes dark when it is selected + + callback OnSearchRequested(string searchTerm) : callback for when the user presses the enter key + or clicks the search button or types if search is live +]] +local FFlagAssetManagerLuaCleanup1 = settings():GetFFlag("AssetManagerLuaCleanup1") +local FFlagUXImprovementAddSearchBar = settings():GetFFlag("UXImprovementAddSearchBar") + +local Library = script.Parent.Parent +local Roact = require(Library.Parent.Parent.Roact) +local Theming = require(Library.Theming) +local withTheme = Theming.withTheme + +local LayoutOrderIterator = require(Library.Utils.LayoutOrderIterator) + +local TextService = game:GetService("TextService") + +local TEXT_SEARCH_DELAY = 500 + +local SearchBar = Roact.PureComponent:extend("SearchBar") + +local RoundFrame = require(Library.Components.RoundFrame) + +local function stripSearchTerm(searchTerm) + return searchTerm and searchTerm:gsub("\n", " ") or "" +end + +function SearchBar:init() + self.state = { + text = "", + + isFocused = false, + isContainerHovered = false, + isClearButtonHovered = false, + } + + self.textBoxRef = Roact.createRef() + + self.requestSearch = function() + if self.props.Enabled then + self.props.OnSearchRequested(self.state.text) + end + end + + self.onContainerHovered = function() + if self.props.Enabled then + self:setState({ + isContainerHovered = true, + }) + end + end + + self.onContainerHoverEnded = function() + if self.props.Enabled then + self:setState({ + isContainerHovered = false, + }) + end + end + + self.onTextChanged = function(rbx) + if self.props.Enabled then + local text = stripSearchTerm(rbx.Text) + local textBox = self.textBoxRef.current + if FFlagUXImprovementAddSearchBar then + if self.state.text ~= text and textBox ~= nil then + self:setState({ + text = text, + }) + + local textSearchDelay = self.props.TextSearchDelay or TEXT_SEARCH_DELAY + delay(textSearchDelay / 1000, function() + self.requestSearch() + end) + + local textBound = TextService:GetTextSize(text, textBox.TextSize, textBox.Font, Vector2.new(math.huge, math.huge)) + if textBound.x > textBox.AbsoluteSize.x then + textBox.TextXAlignment = Enum.TextXAlignment.Right + else + textBox.TextXAlignment = Enum.TextXAlignment.Left + end + end + else + if self.state.text ~= text then + self:setState({ + text = text, + }) + + local textSearchDelay = self.props.TextSearchDelay or TEXT_SEARCH_DELAY + delay(textSearchDelay / 1000, function() + self.requestSearch() + end) + + local textBound = TextService:GetTextSize(text, textBox.TextSize, textBox.Font, Vector2.new(math.huge, math.huge)) + if textBound.x > textBox.AbsoluteSize.x then + textBox.TextXAlignment = Enum.TextXAlignment.Right + else + textBox.TextXAlignment = Enum.TextXAlignment.Left + end + end + end + end + end + + self.onTextBoxFocused = function(rbx) + if self.props.Enabled then + self:setState({ + isFocused = true, + }) + end + end + + self.onTextBoxFocusLost = function(rbx, enterPressed, inputObject) + if self.props.Enabled then + self:setState({ + isFocused = false, + isContainerHovered = false, + }) + end + end + + self.onClearButtonHovered = function() + if self.props.Enabled then + self:setState({ + isClearButtonHovered = true, + }) + end + end + + self.onClearButtonHoverEnded = function() + if self.props.Enabled then + self:setState({ + isClearButtonHovered = false, + }) + end + end + + self.onClearButtonClicked = function() + if self.props.Enabled then + local textBox = self.textBoxRef.current + self:setState({ + isFocused = true, + isClearButtonHovered = false, + }) + + textBox.Text = "" + self.props.OnSearchRequested("") + textBox:CaptureFocus() + textBox.TextXAlignment = Enum.TextXAlignment.Left + end + end +end + +function SearchBar:render() + return withTheme(function(theme) + local props = self.props + local state = self.state + + local size = props.Size + local layoutOrder = props.LayoutOrder or 0 + local defaultText = props.DefaultText + local enabled = props.Enabled + local focusDisabled = props.FocusDisabled + + local onSearchRequested = props.OnSearchRequested + + local rounded = props.Rounded + + assert(size ~= nil, "Searchbar requires a size.") + assert(onSearchRequested ~= nil and type(onSearchRequested) == "function", + "Searchbar requires a OnSearchRequested function.") + + local text = state.text + + local isFocused = (FFlagUXImprovementAddSearchBar and not focusDisabled and state.isFocused) or (not FFlagUXImprovementAddSearchBar and state.isFocused) + local isContainerHovered = state.isContainerHovered + local isClearButtonHovered = state.isClearButtonHovered + + local showClearButton = #text > 0 + + --[[ + By default, TextBoxes let you keep typing infinitely and it will just go out of the bounds + (unless you set properties like ClipDescendants, TextWrapped) + Elsewhere, text boxes shift their contents to the left as you're typing past the bounds + So what you're typing is on the screen + + This is implemented here by: + - Set ClipsDescendants = true on the container + - Get the width of the container, subtracting any padding and the width of the button on the right + - Get the width of the text being rendered (this is calculated in the Roact.Change.Text event) + - If the text is shorter than the parent, then: + - Anchor the text label to the left side of the parent + - Set its width = container width + - Else + - Anchor the text label to the right side of the parent + - Sets its width = text width (with AnchorPoint = (1, 0), this grows to the left) + ]] + local searchBarTheme = theme.searchBar + + local buttonSize = searchBarTheme.buttons.size + + local textBoxOffset = #text > 0 and -buttonSize * 2 or -buttonSize + + local borderColor + if isFocused then + borderColor = searchBarTheme.border.selected.color + elseif isContainerHovered then + borderColor = searchBarTheme.border.hovered.color + else + borderColor = searchBarTheme.border.color + end + + local clearButtonImage = isClearButtonHovered and searchBarTheme.images.clear.hovered.image or searchBarTheme.images.clear.image + + local layoutIndex = LayoutOrderIterator.new() + + local Contents = Roact.createElement("Frame", { + Size = size, + BackgroundColor3 = searchBarTheme.backgroundColor, + BorderColor3 = borderColor, + BorderSizePixel = 1, + LayoutOrder = layoutOrder, + + [Roact.Event.MouseEnter] = self.onContainerHovered, + [Roact.Event.MouseLeave] = self.onContainerHoverEnded, + }, { + Background = Roact.createElement("Frame", { + Size = UDim2.new(1, 0, 1, 0), + BackgroundTransparency = 1, + BorderSizePixel = 0, + }, { + SearchBarLayout = Roact.createElement("UIListLayout", { + SortOrder = Enum.SortOrder.LayoutOrder, + Padding = UDim.new(0, 0), + FillDirection = Enum.FillDirection.Horizontal, + }), + + SearchImageFrame = Roact.createElement("Frame", { + BackgroundTransparency = 1, + Size = UDim2.new(0, buttonSize, 0, buttonSize), + LayoutOrder = layoutIndex:getNextOrder(), + } , { + SearchImage = Roact.createElement("ImageLabel", { + AnchorPoint = Vector2.new(0.5, 0.5), + Position = UDim2.new(0.5, 0, 0.5, 0), + Size = UDim2.new(0, searchBarTheme.buttons.iconSize, + 0, searchBarTheme.buttons.iconSize), + BackgroundTransparency = 1, + Image = searchBarTheme.images.search.image, + ImageColor3 = searchBarTheme.buttons.search.color, + }), + }), + + TextBox = Roact.createElement("TextBox", { + Size = UDim2.new(1, textBoxOffset, 0, buttonSize), + LayoutOrder = layoutIndex:getNextOrder(), + + BackgroundTransparency = 1, + ClipsDescendants = true, + + ClearTextOnFocus = false, + Font = searchBarTheme.font, + TextSize = searchBarTheme.textSize, + TextXAlignment = Enum.TextXAlignment.Left, + TextColor3 = searchBarTheme.text.color, + Text = text, + TextEditable = enabled, + + PlaceholderText = defaultText, + PlaceholderColor3 = searchBarTheme.text.placeholder.color, + + -- Get a reference to the text box so that clicking on the container can call :CaptureFocus() + [Roact.Ref] = self.textBoxRef, + + [Roact.Change.Text] = self.onTextChanged, + [Roact.Event.Focused] = self.onTextBoxFocused, + [Roact.Event.FocusLost] = self.onTextBoxFocusLost, + }), + + ClearButton = showClearButton and Roact.createElement("ImageButton", { + Size = UDim2.new(0, buttonSize, 0, buttonSize), + LayoutOrder = layoutIndex:getNextOrder(), + + BackgroundTransparency = 1, + + [Roact.Event.MouseEnter] = self.onClearButtonHovered, + [Roact.Event.MouseLeave] = self.onClearButtonHoverEnded, + [Roact.Event.MouseButton1Down] = self.onClearButtonClicked, + }, { + ClearImage = Roact.createElement("ImageLabel", { + AnchorPoint = Vector2.new(0.5, 0.5), + Position = UDim2.new(0.5, 0, 0.5, 0), + Size = UDim2.new(0, searchBarTheme.buttons.iconSize, + 0, searchBarTheme.buttons.iconSize), + BackgroundTransparency = 1, + Image = clearButtonImage, + ImageColor3 = searchBarTheme.buttons.clear.color, + }), + }), + }), + }) + + if FFlagAssetManagerLuaCleanup1 then + Contents = Roact.createElement("Frame", { + Size = size, + BackgroundColor3 = searchBarTheme.backgroundColor, + BorderColor3 = borderColor, + BorderSizePixel = 1, + LayoutOrder = layoutOrder, + + [Roact.Event.MouseEnter] = self.onContainerHovered, + [Roact.Event.MouseLeave] = self.onContainerHoverEnded, + }, { + SearchBarLayout = Roact.createElement("UIListLayout", { + SortOrder = Enum.SortOrder.LayoutOrder, + Padding = UDim.new(0, 0), + FillDirection = Enum.FillDirection.Horizontal, + }), + + SearchImageFrame = Roact.createElement("Frame", { + BackgroundTransparency = 1, + Size = UDim2.new(0, buttonSize, 0, buttonSize), + LayoutOrder = layoutIndex:getNextOrder(), + } , { + SearchImage = Roact.createElement("ImageLabel", { + AnchorPoint = Vector2.new(0.5, 0.5), + Position = UDim2.new(0.5, 0, 0.5, 0), + Size = UDim2.new(0, searchBarTheme.buttons.iconSize, + 0, searchBarTheme.buttons.iconSize), + BackgroundTransparency = 1, + Image = searchBarTheme.images.search.image, + ImageColor3 = searchBarTheme.buttons.search.color, + }), + }), + + TextBox = Roact.createElement("TextBox", { + Size = UDim2.new(1, textBoxOffset, 0, buttonSize), + LayoutOrder = layoutIndex:getNextOrder(), + + BackgroundTransparency = 1, + ClipsDescendants = true, + + ClearTextOnFocus = false, + Font = searchBarTheme.font, + TextSize = searchBarTheme.textSize, + TextXAlignment = Enum.TextXAlignment.Left, + TextColor3 = searchBarTheme.text.color, + Text = text, + TextEditable = enabled, + + PlaceholderText = defaultText, + PlaceholderColor3 = searchBarTheme.text.placeholder.color, + + -- Get a reference to the text box so that clicking on the container can call :CaptureFocus() + [Roact.Ref] = self.textBoxRef, + + [Roact.Change.Text] = self.onTextChanged, + [Roact.Event.Focused] = self.onTextBoxFocused, + [Roact.Event.FocusLost] = self.onTextBoxFocusLost, + }), + + ClearButton = showClearButton and Roact.createElement("ImageButton", { + Size = UDim2.new(0, buttonSize, 0, buttonSize), + LayoutOrder = layoutIndex:getNextOrder(), + + BackgroundTransparency = 1, + + [Roact.Event.MouseEnter] = self.onClearButtonHovered, + [Roact.Event.MouseLeave] = self.onClearButtonHoverEnded, + [Roact.Event.MouseButton1Down] = self.onClearButtonClicked, + }, { + ClearImage = Roact.createElement("ImageLabel", { + AnchorPoint = Vector2.new(0.5, 0.5), + Position = UDim2.new(0.5, 0, 0.5, 0), + Size = UDim2.new(0, searchBarTheme.buttons.iconSize, + 0, searchBarTheme.buttons.iconSize), + BackgroundTransparency = 1, + Image = clearButtonImage, + ImageColor3 = searchBarTheme.buttons.clear.color, + }), + }), + }) + end + + if FFlagUXImprovementAddSearchBar and FFlagAssetManagerLuaCleanup1 then + Contents = Roact.createElement(rounded and RoundFrame or "Frame", { + Size = size, + BackgroundColor3 = searchBarTheme.backgroundColor, + BorderColor3 = borderColor, + BorderSizePixel = 1, + LayoutOrder = layoutOrder, + + [Roact.Event.MouseEnter] = self.onContainerHovered, + [Roact.Event.MouseLeave] = self.onContainerHoverEnded, + }, { + SearchBarLayout = Roact.createElement("UIListLayout", { + SortOrder = Enum.SortOrder.LayoutOrder, + Padding = UDim.new(0, 0), + FillDirection = Enum.FillDirection.Horizontal, + }), + + SearchImageFrame = Roact.createElement("Frame", { + BackgroundTransparency = 1, + Size = UDim2.new(0, buttonSize, 1, 0), + LayoutOrder = layoutIndex:getNextOrder(), + } , { + SearchImage = Roact.createElement("ImageLabel", { + AnchorPoint = Vector2.new(0.5, 0.5), + Position = UDim2.new(0.5, 0, 0.5, 0), + Size = UDim2.new(0, searchBarTheme.buttons.iconSize, + 0, searchBarTheme.buttons.iconSize), + BackgroundTransparency = 1, + Image = searchBarTheme.images.search.image, + ImageColor3 = searchBarTheme.buttons.search.color, + }), + }), + + TextBox = Roact.createElement("TextBox", { + Size = UDim2.new(1, textBoxOffset, 1, 0), + LayoutOrder = layoutIndex:getNextOrder(), + + BackgroundTransparency = 1, + ClipsDescendants = true, + + ClearTextOnFocus = false, + Font = searchBarTheme.font, + TextSize = searchBarTheme.textSize, + TextXAlignment = Enum.TextXAlignment.Left, + TextColor3 = searchBarTheme.text.color, + Text = text, + TextEditable = enabled, + + PlaceholderText = defaultText, + PlaceholderColor3 = searchBarTheme.text.placeholder.color, + + -- Get a reference to the text box so that clicking on the container can call :CaptureFocus() + [Roact.Ref] = self.textBoxRef, + + [Roact.Change.Text] = self.onTextChanged, + [Roact.Event.Focused] = self.onTextBoxFocused, + [Roact.Event.FocusLost] = self.onTextBoxFocusLost, + }), + + ClearButton = showClearButton and Roact.createElement("ImageButton", { + Size = UDim2.new(0, buttonSize, 1, 0), + LayoutOrder = layoutIndex:getNextOrder(), + + BackgroundTransparency = 1, + + [Roact.Event.MouseEnter] = self.onClearButtonHovered, + [Roact.Event.MouseLeave] = self.onClearButtonHoverEnded, + [Roact.Event.MouseButton1Down] = self.onClearButtonClicked, + }, { + ClearImage = Roact.createElement("ImageLabel", { + AnchorPoint = Vector2.new(0.5, 0.5), + Position = UDim2.new(0.5, 0, 0.5, 0), + Size = UDim2.new(0, searchBarTheme.buttons.iconSize, + 0, searchBarTheme.buttons.iconSize), + BackgroundTransparency = 1, + Image = clearButtonImage, + ImageColor3 = searchBarTheme.buttons.clear.color, + }), + }), + }) + end + + return Contents + end) +end + +return SearchBar diff --git a/BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Components/SearchBar.spec.lua b/BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Components/SearchBar.spec.lua new file mode 100644 index 0000000000..eb00ad0348 --- /dev/null +++ b/BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Components/SearchBar.spec.lua @@ -0,0 +1,59 @@ +local FFlagAssetManagerLuaCleanup1 = settings():GetFFlag("AssetManagerLuaCleanup1") + +return function() + local Library = script.Parent.Parent + local Roact = require(Library.Parent.Parent.Roact) + + local MockWrapper = require(Library.MockWrapper) + + local SearchBar = require(script.Parent.SearchBar) + + it("should create and destroy without errors", function() + local element = Roact.createElement(MockWrapper, {}, { + SearchBar = Roact.createElement(SearchBar, { + Size = UDim2.new(0, 100, 0, 20), + OnSearchRequested = function() end, + }), + }) + local instance = Roact.mount(element) + Roact.unmount(instance) + end) + + describe("the textbox", function() + it("should move as text is typed", function() + local width = 200 + local element = Roact.createElement(MockWrapper, {}, { + SearchBar = Roact.createElement(SearchBar, { + Size = UDim2.new(0, width, 0, 20), + Enabled = true, + OnSearchRequested = function() end, + }), + }) + local container = Instance.new("Folder") + local instance = Roact.mount(element, container, "SearchBar") + local searchBar = container.SearchBar + local textBox + if FFlagAssetManagerLuaCleanup1 then + textBox =searchBar.TextBox + else + textBox = searchBar.Background.TextBox + end + + local str = ("abcdefghijklmnopqrstuvwxyz"):rep(2) + + textBox.Text = str:sub(1, 1) + local previousWidth = textBox.AbsoluteSize.x + + for i = 1, #str, 1 do + local text = str:sub(1, i) + textBox.Text = text + + local width = textBox.AbsoluteSize.x + expect(width >= previousWidth).to.equal(true) + previousWidth = width + end + + Roact.unmount(instance) + end) + end) +end diff --git a/BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Components/Separator.lua b/BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Components/Separator.lua new file mode 100644 index 0000000000..f0ecae653d --- /dev/null +++ b/BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Components/Separator.lua @@ -0,0 +1,51 @@ +--[[ + A simple border to separate elements. + + Props: + Enum DominantAxis = Specifies whether the separator fills the + space horizontally or vertically. Width will make the separator + fill the horizontal space, and Height will make the separator + fill the vertical space. + Weight = The thickness of the separator line. + Padding = The padding in pixels to subtract from either side of + the separator's dominant axis. + + Position = The position of the center of the separator. + LayoutOrder = The order in which the separator appears in a UILayout. + ZIndex = The render order of the separator. +]] + +local Library = script.Parent.Parent +local Roact = require(Library.Parent.Parent.Roact) +local Theming = require(Library.Theming) +local withTheme = Theming.withTheme + +local function Separator(props) + local dominantAxis = props.DominantAxis or Enum.DominantAxis.Width + local position = props.Position + local weight = props.Weight or 1 + local padding = props.Padding or 0 + local zIndex = props.ZIndex + local layoutOrder = props.LayoutOrder + + local size + if dominantAxis == Enum.DominantAxis.Width then + size = UDim2.new(1, -padding * 2, 0, weight) + else + size = UDim2.new(0, weight, 1, -padding * 2) + end + + return withTheme(function(theme) + return Roact.createElement("Frame", { + Size = size, + Position = position, + AnchorPoint = Vector2.new(0.5, 0.5), + BackgroundColor3 = theme.separator.lineColor, + BorderSizePixel = 0, + ZIndex = zIndex, + LayoutOrder = layoutOrder, + }) + end) +end + +return Separator \ No newline at end of file diff --git a/BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Components/Separator.spec.lua b/BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Components/Separator.spec.lua new file mode 100644 index 0000000000..300dda2c74 --- /dev/null +++ b/BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Components/Separator.spec.lua @@ -0,0 +1,30 @@ +return function() + local Library = script.Parent.Parent + local Roact = require(Library.Parent.Parent.Roact) + + local MockWrapper = require(Library.MockWrapper) + + local Separator = require(script.Parent.Separator) + + local function createTestSeparator(props) + return Roact.createElement(MockWrapper, {}, { + Separator = Roact.createElement(Separator, props) + }) + end + + it("should create and destroy without errors", function() + local element = createTestSeparator() + local instance = Roact.mount(element) + Roact.unmount(instance) + end) + + it("should render correctly", function () + local container = Instance.new("Folder") + local instance = Roact.mount(createTestSeparator(), container) + local separator = container:FindFirstChildOfClass("Frame") + + expect(separator).to.be.ok() + + Roact.unmount(instance) + end) +end \ No newline at end of file diff --git a/BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Components/StyledDialog.lua b/BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Components/StyledDialog.lua new file mode 100644 index 0000000000..200abb3ce9 --- /dev/null +++ b/BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Components/StyledDialog.lua @@ -0,0 +1,105 @@ +--[[ + An implementation of BaseDialog that adds UILibrary Buttons to the bottom. + To use the component, the consumer supplies an array of buttons, optionally + defining a Style for each button if it should display differently. + + Props: + array Buttons = An array of items used to render the buttons for this dialog. + { + {Key = "Cancel", Text = "SomeLocalizedTextForCancel"}, + {Key = "Save", Text = "SomeLocalizedTextForSave", Style = "Primary"}, + } + function OnButtonClicked(key) = A callback for when the user clicked + a button in the dialog. Accepts the Key of the button that was clicked. + function OnClose = A callback for when the user closed the dialog by + clicking the X in the corner of the window. + + Vector2 Size = The starting size of the dialog. + Vector2 MinSize = The minimum size of the dialog, if it is resizable. + bool Resizable = Whether the dialog can be resized. + int BorderPadding = The padding to add around the edges of the dialog. + int ButtonPadding = The padding to add between buttons. + int ButtonHeight = The height of the buttons in the dialog, in pixels. + int ButtonWidth = The width of each button in the dialog, in pixels. + string Title = The title to display at the top of the window. +]] + +local Library = script.Parent.Parent +local Roact = require(Library.Parent.Parent.Roact) + +local Theming = require(Library.Theming) +local withTheme = Theming.withTheme + +local BaseDialog = require(Library.Components.BaseDialog) +local Button = require(Library.Components.Button) + +local StyledDialog = Roact.PureComponent:extend("StyledDialog") + +function StyledDialog:init() + self.enabledChanged = function(enabled) + if not enabled and self.props.OnClose then + self.props.OnClose() + end + end + + self.buttonClicked = function(button) + if self.props.OnButtonClicked then + self.props.OnButtonClicked(button.Key) + end + end +end + +function StyledDialog:render() + return withTheme(function(theme) + local props = self.props + local title = props.Title + local size = props.Size + local minSize = props.MinSize + local resizable = props.Resizable + local borderPadding = props.BorderPadding + local textSize = props.TextSize + + local buttons = props.Buttons + local buttonPadding = props.ButtonPadding + local buttonHeight = props.ButtonHeight + local buttonWidth = props.ButtonWidth + + return Roact.createElement(BaseDialog, { + Title = title, + Size = size, + MinSize = minSize, + Resizable = resizable, + Buttons = buttons, + ButtonHeight = buttonHeight, + BorderPadding = borderPadding, + ButtonPadding = buttonPadding, + + RenderButton = function(button, index, activated) + return Roact.createElement(Button, { + Size = UDim2.new(0, buttonWidth, 0, buttonHeight), + LayoutOrder = index, + Style = button.Style, + + OnClick = activated, + RenderContents = function(buttonTheme) + return { + Text = Roact.createElement("TextLabel", { + Size = UDim2.new(1, 0, 1, 0), + BackgroundTransparency = 1, + Font = buttonTheme.font, + Text = button.Text, + TextSize = textSize, + TextColor3 = buttonTheme.textColor, + }) + } + end, + }) + end, + + OnButtonClicked = self.buttonClicked, + OnClose = props.OnClose, + }, self.props[Roact.Children]) + end) +end + +return StyledDialog diff --git a/BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Components/StyledDialog.spec.lua b/BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Components/StyledDialog.spec.lua new file mode 100644 index 0000000000..4a4289e0ac --- /dev/null +++ b/BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Components/StyledDialog.spec.lua @@ -0,0 +1,94 @@ +return function() + local Library = script.Parent.Parent + local Roact = require(Library.Parent.Parent.Roact) + + local MockWrapper = require(Library.MockWrapper) + + local StyledDialog = require(script.Parent.StyledDialog) + + local function createTestStyledDialog(props, children, container) + return Roact.createElement(MockWrapper, { + Container = container, + }, { + StyledDialog = Roact.createElement(StyledDialog, props, children) + }) + end + + it("should create and destroy without errors", function() + local element = createTestStyledDialog({ + Buttons = {}, + }) + local instance = Roact.mount(element) + Roact.unmount(instance) + end) + + it("should render correctly", function() + local container = Instance.new("Folder") + + local element = createTestStyledDialog({ + Buttons = {}, + }, {}, container) + + local instance = Roact.mount(element, container) + + local gui = container:FindFirstChildOfClass("BillboardGui") + expect(gui).to.be.ok() + expect(gui.FocusProvider).to.be.ok() + expect(gui.FocusProvider.Padding).to.be.ok() + expect(gui.FocusProvider.Content).to.be.ok() + expect(gui.FocusProvider.Buttons).to.be.ok() + + Roact.unmount(instance) + end) + + it("should require a Buttons table", function() + local element = createTestStyledDialog() + expect(function() + Roact.mount(element) + end).to.throw() + + element = createTestStyledDialog({ + Buttons = true, + }) + expect(function() + Roact.mount(element) + end).to.throw() + end) + + it("should create a Button for each button", function() + local container = Instance.new("Folder") + + local element = createTestStyledDialog({ + Buttons = { + {Key = "Test", Text = "TestText"}, + {Key = "Test2", Text = "TestText2"}, + }, + }, {}, container) + + local instance = Roact.mount(element, container) + + local gui = container:FindFirstChildOfClass("BillboardGui") + expect(gui.FocusProvider.Buttons).to.be.ok() + expect(gui.FocusProvider.Buttons[1]).to.be.ok() + expect(gui.FocusProvider.Buttons[2]).to.be.ok() + + Roact.unmount(instance) + end) + + it("should render its children", function() + local container = Instance.new("Folder") + + local element = createTestStyledDialog({ + Buttons = {}, + }, { + Frame = Roact.createElement("Frame"), + }, container) + + local instance = Roact.mount(element, container) + + local gui = container:FindFirstChildOfClass("BillboardGui") + expect(gui.FocusProvider.Content.Frame).to.be.ok() + + Roact.unmount(instance) + end) +end \ No newline at end of file diff --git a/BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Components/StyledDropdown.lua b/BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Components/StyledDropdown.lua new file mode 100644 index 0000000000..56830dda86 --- /dev/null +++ b/BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Components/StyledDropdown.lua @@ -0,0 +1,281 @@ +--[[ + A dropdown menu styled to match the Roblox Studio start page. + Consists of a button used to open the dropdown as well as the menu itself. + Note that the logic for opening and closing the menu is contained within this component, + but the consumer is responsible for showing the current value in the button. + + Required Props: + UDim2 Size = The size of the button that opens the dropdown. + UDim2 Position = The position of the button that opens the dropdown. + int TextSize = The size of the text in the dropdown and button. + int ItemHeight = The height of each entry in the dropdown, in pixels. + string ButtonText = The text to display in the button that opens the dropdown. + Usually should be set to the currently selected dropdown entry. + array Items = An ordered array of each item that should appear in the dropdown. + The array is formatted like this: + { + {Key = "Item1", Text = "SomeLocalizedTextForItem1"}, + {Key = "Item2", Text = "SomeLocalizedTextForItem2"}, + {Key = "Item3", Text = "SomeLocalizedTextForItem3"}, + } + Key is how the item will be referenced in code. Text is what will appear to the user. + function OnItemClicked(item) = A callback when the user selects an item in the dropdown. + Returns the item as it was defined in the Items array. + + Optional Props: + int MaxItems = The maximum number of entries that can display at a time. + If this is less than the number of entries in the dropdown, a scrollbar will appear. + bool ShowRibbon = Whether to show a colored ribbon next to the currently + hovered dropdown entry. Usually should be enabled for Light theme only. + int TextPadding = The amount of padding, in pixels, around the text elements. + int IconSize = The size of the arrow icon in the button. + int IconPadding = The distance from the right side of the arrow icon to the button edge. + int ScrollBarPadding = The padding which appears on either side of the scrollbar. + int ScrollBarThickness = The horizontal width of the scrollbar. +]] +local FFlagStudioFixUILibDropdownStyle = game:GetFastFlag("StudioFixUILibDropdownStyle") +local FFlagStudioFixUILibDropdownText = game:GetFastFlag("StudioFixUILibDropdownText") + +-- Defaults +local TEXT_PADDING = 8 +local ICON_SIZE = 12 +local ICON_PADDING = 4 + +local RIBBON_WIDTH = 5 +local VERTICAL_OFFSET = 2 + +local TextService = game:GetService("TextService") + +local Library = script.Parent.Parent +local Roact = require(Library.Parent.Parent.Roact) + +local Theming = require(Library.Theming) +local withTheme = Theming.withTheme + +local DropdownMenu = require(Library.Components.DropdownMenu) +local RoundFrame = require(Library.Components.RoundFrame) + +local StyledDropdown = Roact.PureComponent:extend("StyledDropdown") + +function StyledDropdown:init() + self.state = { + showDropdown = false, + isButtonHovered = false, + hoveredKey = nil, + } + self.buttonRef = Roact.createRef() + + self.onItemClicked = function(key) + self.props.OnItemClicked(key) + self.hideDropdown() + end + + self.showDropdown = function() + self:setState({ + showDropdown = true, + }) + end + + self.hideDropdown = function() + self:setState({ + showDropdown = false, + }) + end + + self.onKeyMouseEnter = function(key) + self:setState({ + hoveredKey = key, + }) + end + + self.onKeyMouseLeave = function(key) + if self.state.hoveredKey == key then + self:setState({ + hoveredKey = Roact.None, + }) + end + end + + self.onMouseEnter = function() + self:setState({ + isButtonHovered = true, + }) + end + + self.onMouseLeave = function() + self:setState({ + isButtonHovered = false, + }) + end +end + +function StyledDropdown:createLabel(key, displayText, textSize, textPadding, font, textColor) + return Roact.createElement("TextLabel", { + Size = UDim2.new(1, 0, 1, 0), + Font = font, + TextSize = textSize, + Text = displayText, + TextXAlignment = Enum.TextXAlignment.Left, + TextColor3 = textColor, + BackgroundTransparency = 1, + + [Roact.Event.MouseEnter] = function() + self.onKeyMouseEnter(key) + end, + [Roact.Event.MouseLeave] = function() + self.onKeyMouseLeave(key) + end, + }, { + Padding = Roact.createElement("UIPadding", { + PaddingLeft = UDim.new(0, textPadding), + }), + }) +end + +function StyledDropdown:render() + return withTheme(function(theme) + local props = self.props + local state = self.state + local dropdownTheme = theme.styledDropdown + local listTheme = dropdownTheme.listTheme or dropdownTheme + local showDropdown = state.showDropdown + local buttonRef = self.buttonRef and self.buttonRef.current + local buttonExtents + if buttonRef then + local buttonMin = buttonRef.AbsolutePosition + local buttonSize = buttonRef.AbsoluteSize + local buttonMax = buttonMin + buttonSize + buttonExtents = Rect.new(buttonMin.X, buttonMin.Y, buttonMax.X, buttonMax.Y) + end + local listWidth = props.ListWidth or 0 + local items = props.Items or {} + local size = props.Size + local position = props.Position + local textSize = props.TextSize + local itemHeight = props.ItemHeight + local maxItems = props.MaxItems + local showRibbon = props.ShowRibbon + + local textPadding = props.TextPadding or TEXT_PADDING + local iconSize = props.IconSize or ICON_SIZE + local iconPadding = props.IconPadding or ICON_PADDING + local scrollBarPadding = props.ScrollBarPadding + local scrollBarThickness = props.ScrollBarThickness + + local hoveredKey = state.hoveredKey + local selectedItem = props.SelectedItem + local isButtonHovered = state.isButtonHovered + local buttonText = props.ButtonText + + local maxWidth = 0 + local maxHeight = maxItems and (maxItems * itemHeight) or nil + local LayoutOrder = props.LayoutOrder or 0 + + for _, data in ipairs(items) do + local textBound = TextService:GetTextSize(data.Text, + textSize, dropdownTheme.font, Vector2.new(9000, 100)) + + local itemWidth = textBound.X + textPadding * 2 + maxWidth = math.max(maxWidth, itemWidth) + end + + if FFlagStudioFixUILibDropdownStyle then + maxWidth = math.max(maxWidth, listWidth) + end + + local buttonTheme = (showDropdown or isButtonHovered) and dropdownTheme.selected + or dropdownTheme + + return Roact.createElement("ImageButton", { + Size = size, + Position = position, + BackgroundTransparency = 1, + ImageTransparency = 1, + + [Roact.Ref] = self.buttonRef, + + [Roact.Event.Activated] = self.showDropdown, + [Roact.Event.MouseEnter] = self.onMouseEnter, + [Roact.Event.MouseLeave] = self.onMouseLeave, + + LayoutOrder = LayoutOrder, + }, { + RoundFrame = Roact.createElement(RoundFrame, { + Size = UDim2.new(1, 0, 1, 0), + BackgroundColor3 = buttonTheme.backgroundColor, + BorderColor3 = buttonTheme.borderColor, + }), + + ArrowIcon = Roact.createElement("ImageLabel", { + Size = UDim2.new(0, iconSize, 0, iconSize), + Position = UDim2.new(1, -iconPadding, 0.5, 0), + AnchorPoint = Vector2.new(1, 0.5), + BackgroundTransparency = 1, + ImageColor3 = buttonTheme.textColor, + Image = dropdownTheme.arrowImage, + }), + + TextLabel = Roact.createElement("TextLabel", { + Size = UDim2.new(1, FFlagStudioFixUILibDropdownText and -iconSize or 0, 1, 0), + BackgroundTransparency = 1, + Font = dropdownTheme.font, + TextColor3 = buttonTheme.textColor, + TextSize = textSize, + Text = buttonText, + TextXAlignment = Enum.TextXAlignment.Left, + TextTruncate = FFlagStudioFixUILibDropdownText and Enum.TextTruncate.AtEnd or nil, + }, { + Padding = Roact.createElement("UIPadding", { + PaddingLeft = UDim.new(0, textPadding), + }), + }), + + Dropdown = showDropdown and buttonRef and Roact.createElement(DropdownMenu, { + OnItemClicked = self.onItemClicked, + OnFocusLost = self.hideDropdown, + SourceExtents = buttonExtents, + Offset = Vector2.new(0, VERTICAL_OFFSET), + MaxHeight = maxHeight, + ShowBorder = true, + ScrollBarPadding = scrollBarPadding, + ScrollBarThickness = scrollBarThickness, + ListWidth = FFlagStudioFixUILibDropdownStyle and maxWidth or listWidth, + Items = items, + RenderItem = function(item, index, activated) + local key = item.Key + local selected = key == selectedItem + local displayText = item.Text + local isHovered = hoveredKey == key + local textColor = (selected or isHovered) and dropdownTheme.hovered.textColor + or dropdownTheme.textColor + local itemColor = listTheme.backgroundColor + if selected then + itemColor = listTheme.selected.backgroundColor + elseif isHovered then + itemColor = listTheme.hovered.backgroundColor + end + + return Roact.createElement("ImageButton", { + Size = UDim2.new(0, FFlagStudioFixUILibDropdownStyle and maxWidth or math.max(listWidth, maxWidth), 0, itemHeight), + BackgroundColor3 = itemColor, + BorderSizePixel = 0, + LayoutOrder = index, + AutoButtonColor = false, + [Roact.Event.Activated] = activated, + }, { + Ribbon = isHovered and showRibbon and Roact.createElement("Frame", { + Size = UDim2.new(0, RIBBON_WIDTH, 1, 0), + BackgroundColor3 = listTheme.selected.backgroundColor, + BorderSizePixel = 0, + }), + + Label = self:createLabel(key, displayText, textSize, + textPadding, dropdownTheme.font, textColor), + }) + end, + }) + }) + end) +end + +return StyledDropdown diff --git a/BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Components/StyledDropdown.spec.lua b/BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Components/StyledDropdown.spec.lua new file mode 100644 index 0000000000..2e01d21f4d --- /dev/null +++ b/BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Components/StyledDropdown.spec.lua @@ -0,0 +1,34 @@ +return function() + local Library = script.Parent.Parent + local Roact = require(Library.Parent.Parent.Roact) + + local MockWrapper = require(Library.MockWrapper) + + local StyledDropdown = require(script.Parent.StyledDropdown) + + local function createTestStyledDropdown(props, children) + return Roact.createElement(MockWrapper, {}, { + StyledDropdown = Roact.createElement(StyledDropdown, props, children) + }) + end + + it("should create and destroy without errors", function() + local element = createTestStyledDropdown() + local instance = Roact.mount(element) + Roact.unmount(instance) + end) + + it("should render correctly", function() + local container = Instance.new("Folder") + local instance = Roact.mount(createTestStyledDropdown(), container) + local button = container:FindFirstChildOfClass("ImageButton") + + expect(button).to.be.ok() + expect(button.RoundFrame).to.be.ok() + expect(button.ArrowIcon).to.be.ok() + expect(button.TextLabel).to.be.ok() + expect(button.TextLabel.Padding).to.be.ok() + + Roact.unmount(instance) + end) +end \ No newline at end of file diff --git a/BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Components/StyledScrollingFrame.lua b/BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Components/StyledScrollingFrame.lua new file mode 100644 index 0000000000..7e54ef796e --- /dev/null +++ b/BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Components/StyledScrollingFrame.lua @@ -0,0 +1,98 @@ +--[[ + A scrolling frame with a colored background, providing a consistent look + with the Studio native start page. + + Props: + UDim2 Position = The position of the scrolling frame. + UDim2 Size = The size of the scrolling frame. + UDim2 CanvasSize = The size of the scrolling frame's canvas. + + int LayoutOrder = The order this component will display in a UILayout. + int ZIndex = The draw index of the frame. + + bool ScrollingEnabled = Whether scrolling in this frame will change the CanvasPosition. + int ScrollBarPadding = The padding which appears on either side of the scrollbar. + int ScrollBarThickness = The horizontal width of the scrollbar. + + function OnScroll(Vector2 CanvasPosition) = A callback for when the CanvasPosition changes. +]] + +local DEFAULT_SCROLLBAR_THICKNESS = 8 +local DEFAULT_SCROLLBAR_PADDING = 2 + +local Library = script.Parent.Parent +local Roact = require(Library.Parent.Parent.Roact) + +local Theming = require(Library.Theming) +local withTheme = Theming.withTheme + +local StyledScrollingFrame = Roact.PureComponent:extend("StyledScrollingFrame") + +function StyledScrollingFrame:init() + self.onScroll = function(rbx) + if self.props.OnScroll then + self.props.OnScroll(rbx.CanvasPosition) + end + end +end + +function StyledScrollingFrame:render() + return withTheme(function(theme) + local props = self.props + local scrollTheme = theme.scrollingFrame + + local position = props.Position + local size = props.Size + local canvasSize = props.CanvasSize + local layoutOrder = props.LayoutOrder + local zindex = props.ZIndex + local scrollingEnabled = props.ScrollingEnabled + local padding = props.ScrollBarPadding or DEFAULT_SCROLLBAR_PADDING + local scrollBarThickness = props.ScrollBarThickness or DEFAULT_SCROLLBAR_THICKNESS + + local backgroundThickness = scrollBarThickness + (padding * 2) + + local ref = props[Roact.Ref] + local children = props[Roact.Children] + + return Roact.createElement("Frame", { + Position = position, + Size = size, + LayoutOrder = layoutOrder, + ZIndex = zindex, + BackgroundTransparency = 1, + }, { + ScrollBarBackground = Roact.createElement("Frame", { + Position = UDim2.new(1, 0, 0, 0), + Size = UDim2.new(0, backgroundThickness, 1, 0), + AnchorPoint = Vector2.new(1, 0), + BorderSizePixel = 0, + BackgroundColor3 = scrollTheme.backgroundColor, + ZIndex = 2, + }), + + ScrollingFrame = Roact.createElement("ScrollingFrame", { + Size = UDim2.new(1, -padding, 1, 0), + CanvasSize = canvasSize, + BackgroundTransparency = 1, + BorderSizePixel = 0, + ScrollBarThickness = scrollBarThickness, + ZIndex = 2, + + TopImage = scrollTheme.topImage, + MidImage = scrollTheme.midImage, + BottomImage = scrollTheme.bottomImage, + + ScrollBarImageColor3 = scrollTheme.scrollbarColor, + + ScrollingEnabled = scrollingEnabled, + ScrollingDirection = Enum.ScrollingDirection.Y, + + [Roact.Change.CanvasPosition] = self.onScroll, + [Roact.Ref] = ref, + }, children), + }) + end) +end + +return StyledScrollingFrame diff --git a/BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Components/StyledScrollingFrame.spec.lua b/BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Components/StyledScrollingFrame.spec.lua new file mode 100644 index 0000000000..6ecb742b68 --- /dev/null +++ b/BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Components/StyledScrollingFrame.spec.lua @@ -0,0 +1,62 @@ +return function() + local Library = script.Parent.Parent + local Roact = require(Library.Parent.Parent.Roact) + + local MockWrapper = require(Library.MockWrapper) + + local StyledScrollingFrame = require(script.Parent.StyledScrollingFrame) + + local function createTestScrollingFrame(props, children) + return Roact.createElement(MockWrapper, {}, { + ScrollingFrame = Roact.createElement(StyledScrollingFrame, props, children) + }) + end + + it("should create and destroy without errors", function() + local element = createTestScrollingFrame() + local instance = Roact.mount(element) + Roact.unmount(instance) + end) + + it("should render correctly", function() + local container = Instance.new("Folder") + local instance = Roact.mount(createTestScrollingFrame(), container) + local frame = container:FindFirstChildOfClass("Frame") + + expect(frame).to.be.ok() + expect(frame.ScrollBarBackground).to.be.ok() + expect(frame.ScrollingFrame).to.be.ok() + + Roact.unmount(instance) + end) + + it("should render its children in the ScrollingFrame", function() + local container = Instance.new("Folder") + local instance = Roact.mount(createTestScrollingFrame({}, { + ChildFrame = Roact.createElement("Frame"), + }), container) + local frame = container:FindFirstChildOfClass("Frame") + + expect(frame).to.be.ok() + expect(frame.ScrollBarBackground).to.be.ok() + expect(frame.ScrollingFrame).to.be.ok() + expect(frame.ScrollingFrame.ChildFrame).to.be.ok() + + Roact.unmount(instance) + end) + + it("should add padding to both sides of the ScrollBar", function() + local container = Instance.new("Folder") + local instance = Roact.mount(createTestScrollingFrame({ + ScrollBarPadding = 2, + ScrollBarThickness = 8, + }), container) + local frame = container:FindFirstChildOfClass("Frame") + + expect(frame).to.be.ok() + expect(frame.ScrollBarBackground).to.be.ok() + expect(frame.ScrollBarBackground.Size.X.Offset).to.equal(12) + + Roact.unmount(instance) + end) +end \ No newline at end of file diff --git a/BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Components/StyledTooltip.lua b/BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Components/StyledTooltip.lua new file mode 100644 index 0000000000..91bffdcb3e --- /dev/null +++ b/BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Components/StyledTooltip.lua @@ -0,0 +1,196 @@ +--[[ + An element which can be added to a component to give that component a tooltip. + When the user hovers the mouse over the component, the tooltip will appear + after a short delay. + + Required Props: + table Elements = The table containing roact elements to display in the tooltip. + Vector2 TooltipExtents = vector containing tooltip size + bool Enabled = Whether the tooltip will display on hover. + + Optional Props: + float ShowDelay = The time in seconds before the tooltip appears + after the user stops moving the mouse over the element. Defaults to 0.5. + int Priority = The display order of this element, compared to other focused + elements or elements that show on top. +]] + +local SHOW_DELAY_DEFAULT = 0.5 + +local RunService = game:GetService("RunService") + +local Library = script.Parent.Parent +local Roact = require(Library.Parent.Parent.Roact) + +local Theming = require(Library.Theming) +local withTheme = Theming.withTheme + +local Focus = require(Library.Focus) +local ShowOnTop = Focus.ShowOnTop +local withFocus = Focus.withFocus + +local DropShadow = require(Library.Components.DropShadow) + +local Tooltip = Roact.PureComponent:extend("Tooltip") + +function Tooltip:init(props) + self.state = { + showToolTip = false, + } + + self.isElementHovered = false + self.isTooltipHovered = false + self.mousePos = nil + + self.connectHover = function() + self.hoverConnection = RunService.Heartbeat:Connect(function() + if self.isElementHovered or self.isTooltipHovered then + if tick() >= self.targetTime then + self.disconnectHover() + self:setState({ + showToolTip = true, + }) + end + end + end) + end + + self.disconnectHover = function() + if self.hoverConnection then + self.hoverConnection:Disconnect() + end + end + + self.elementMouseEnter = function(rbx, xpos, ypos) + local showDelay = props.ShowDelay or SHOW_DELAY_DEFAULT + self.isElementHovered = true + self.targetTime = tick() + showDelay + if not self.mousePos then + self.mousePos = Vector2.new(xpos, ypos) + end + self.connectHover() + end + + self.elementMouseMoved = function(rbx, xpos, ypos) + local showDelay = props.ShowDelay or SHOW_DELAY_DEFAULT + self.targetTime = tick() + showDelay + end + + self.elementMouseLeave = function() + self.isElementHovered = false + local hovered = self.isElementHovered or self.isTooltipHovered + self:setState({ + showToolTip = hovered, + }) + if not hovered then + self.targetTime = 0 + self.mousePos = nil + self.disconnectHover() + end + end + + self.tooltipMouseEnter = function(rbx, xpos, ypos) + self.isTooltipHovered = true + end + + self.tooltipMouseMoved = function(rbx, xpos, ypos) + local showDelay = props.ShowDelay or SHOW_DELAY_DEFAULT + self.targetTime = tick() + showDelay + end + + self.tooltipMouseLeave = function() + self.isTooltipHovered = false + local hovered = self.isElementHovered or self.isTooltipHovered + self:setState({ + showToolTip = hovered, + }) + if not hovered then + self.targetTime = 0 + self.mousePos = nil + self.disconnectHover() + end + end +end + +function Tooltip:willUnmount() + self.disconnectHover() +end + +function Tooltip:render() + return withTheme(function(theme) + return withFocus(function(pluginGui) + local props = self.props + local state = self.state + + local styledTooltipTheme = theme.styledTooltip + + local enabled = props.Enabled + local priority = props.Priority or 0 + + local mousePos = self.mousePos + + local elements = props.Elements + local tooltipWidth = props.TooltipExtents and props.TooltipExtents.X + local tooltipHeight = props.TooltipExtents and props.TooltipExtents.Y + + local content = {} + + if state.showToolTip and mousePos and enabled and pluginGui then + local targetX = mousePos.X + local targetY = mousePos.Y + + local targetWidth = pluginGui.AbsoluteSize.X + local targetHeight = pluginGui.AbsoluteSize.Y + + + if targetX + tooltipWidth >= targetWidth then + targetX = targetWidth - tooltipWidth + end + + if targetY + tooltipHeight >= targetHeight then + targetY = targetHeight - tooltipHeight + end + + content.TooltipContainer = Roact.createElement(ShowOnTop, { + Priority = priority, + }, { + Tooltip = Roact.createElement("Frame", { + Position = UDim2.new(0, targetX, 0, targetY), + Size = UDim2.new(0, tooltipWidth, 0, tooltipHeight), + BackgroundTransparency = 1, + BorderSizePixel = 0, + + [Roact.Event.MouseEnter] = self.tooltipMouseEnter, + [Roact.Event.MouseMoved] = self.tooltipMouseMoved, + [Roact.Event.MouseLeave] = self.tooltipMouseLeave, + }, { + DropShadow = Roact.createElement(DropShadow, { + Transparency = styledTooltipTheme.shadowTransparency, + Color = styledTooltipTheme.shadowColor, + Offset = styledTooltipTheme.shadowOffset, + ZIndex = 1, + }), + + ContentFrame = Roact.createElement("Frame", { + Size = UDim2.new(1, 0, 1, 0), + BackgroundColor3 = styledTooltipTheme.backgroundColor, + BorderSizePixel = 0, + ZIndex = 2, + }, elements), + }) + }) + end + + return Roact.createElement("Frame",{ + Size = UDim2.new(1, 0, 1, 0), + BackgroundTransparency = 1, + + [Roact.Event.MouseEnter] = self.elementMouseEnter, + [Roact.Event.MouseMoved] = self.elementMouseMoved, + [Roact.Event.MouseLeave] = self.elementMouseLeave, + }, content) + end) + end) +end + +return Tooltip diff --git a/BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Components/StyledTooltip.spec.lua b/BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Components/StyledTooltip.spec.lua new file mode 100644 index 0000000000..c2291b26f9 --- /dev/null +++ b/BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Components/StyledTooltip.spec.lua @@ -0,0 +1,30 @@ +return function() + local Library = script.Parent.Parent + local Roact = require(Library.Parent.Parent.Roact) + + local MockWrapper = require(Library.MockWrapper) + + local StyledTooltip = require(script.Parent.StyledTooltip) + + local function createTestTooltip(props) + return Roact.createElement(MockWrapper, {}, { + Tooltip = Roact.createElement(StyledTooltip, props) + }) + end + + it("should create and destroy without errors", function() + local element = createTestTooltip() + local instance = Roact.mount(element) + Roact.unmount(instance) + end) + + it("should render correctly", function () + local container = Instance.new("Folder") + local instance = Roact.mount(createTestTooltip(), container) + local frame = container:FindFirstChildOfClass("Frame") + + expect(frame).to.be.ok() + + Roact.unmount(instance) + end) +end \ No newline at end of file diff --git a/BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Components/TextEntry.lua b/BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Components/TextEntry.lua new file mode 100644 index 0000000000..04040995a1 --- /dev/null +++ b/BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Components/TextEntry.lua @@ -0,0 +1,137 @@ +--[[ + A text entry that is only one line. + Used in a RoundTextBox when Multiline is false. + + Props: + string Text = The text to display + string PlaceholderText = text to display when box is empty/in default state + bool Visible = Whether to display this component + function SetText(text) = Callback to tell parent that text has changed + function FocusChanged(focus) = Callback to tell parent that this component has focus +]] + +local Library = script.Parent.Parent +local Roact = require(Library.Parent.Parent.Roact) + +local Theming = require(Library.Theming) +local withTheme = Theming.withTheme + +local TextEntry = Roact.PureComponent:extend("TextEntry") +local FFlagAllowTextEntryToTakeSizeAndPositionProp = game:DefineFastFlag("AllowTextEntryToTakeSizeAndPositionProp", false) +local FFlagGameSettingsFixNameWhitespace = game:DefineFastFlag("GameSettingsFixNameWhitespace", false) +local FFlagFixTextChangedFromEmptyForTextEntry = game:DefineFastFlag("FixTextChangedFromEmptyForTextEntry", false) + +function TextEntry:init() + self.textBoxRef = Roact.createRef() + self.onTextChanged = function(rbx) + if rbx.TextFits then + rbx.TextXAlignment = Enum.TextXAlignment.Left + else + rbx.TextXAlignment = Enum.TextXAlignment.Right + end + if FFlagFixTextChangedFromEmptyForTextEntry then + if FFlagGameSettingsFixNameWhitespace then + local processed = string.gsub(rbx.Text, "[\n\r]", " ") + self.props.SetText(processed) + else + self.props.SetText(rbx.Text) + end + else + if rbx.Text ~= self.props.Text then + if FFlagGameSettingsFixNameWhitespace then + local processed = string.gsub(rbx.Text, "[\n\r]", " ") + self.props.SetText(processed) + else + self.props.SetText(rbx.Text) + end + end + end + end + + self.mouseEnter = function() + self.props.HoverChanged(true) + end + self.mouseLeave = function() + self.props.HoverChanged(false) + end +end + +function TextEntry:render() + return withTheme(function(theme) + local textSize = self.props.TextSize + local font = self.props.Font + + local textEntryTheme = theme.textEntry + + local size + local position + local textTransparency + local enabled + if FFlagAllowTextEntryToTakeSizeAndPositionProp then + size = self.props.Size and self.props.Size or UDim2.new(1, 0, 1, 0) + position = self.props.Position and self.props.Position or nil + enabled = (self.props.Enabled == nil) and true or self.props.Enabled + textTransparency = enabled and textEntryTheme.textTransparency.enabled or textEntryTheme.textTransparency.disabled + else + size = UDim2.new(1, 0, 1, 0) + position = nil + enabled = nil + textTransparency = nil + end + + return Roact.createElement("Frame", { + Size = size, + BackgroundTransparency = 1, + ClipsDescendants = true, + }, { + Text = Roact.createElement("TextBox", { + Visible = self.props.Visible, + + Size = UDim2.new(1, 0, 1, 0), + Position = position, + BackgroundTransparency = 1, + BorderSizePixel = 0, + + PlaceholderText = self.props.PlaceholderText, + PlaceholderColor3 = self.props.TextColor3, + ClearTextOnFocus = false, + Font = font, + TextSize = textSize, + TextColor3 = self.props.TextColor3, + Text = self.props.Text, + TextXAlignment = Enum.TextXAlignment.Left, + TextTransparency = textTransparency, + TextEditable = enabled, + + [Roact.Ref] = self.textBoxRef, + + [Roact.Event.MouseEnter] = self.mouseEnter, + [Roact.Event.MouseLeave] = self.mouseLeave, + + [Roact.Event.Focused] = function() + self.props.FocusChanged(true) + end, + + [Roact.Event.FocusLost] = function() + -- workaround because we do not disconnect events before we start unmounting host components. + -- see https://github.com/Roblox/roact/issues/235 for more info + if not self.textBoxRef.current then return end + + local textBox = self.textBoxRef.current + textBox.TextXAlignment = Enum.TextXAlignment.Left + self.props.FocusChanged(false) + end, + + [Roact.Change.Text] = function(rbx) + -- workaround because we do not disconnect events before we start unmounting host components. + -- see https://github.com/Roblox/roact/issues/235 for more info + if not self.textBoxRef.current then return end + + self.onTextChanged(rbx) + end + }), + }) + end) +end + +return TextEntry diff --git a/BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Components/TextEntry.spec.lua b/BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Components/TextEntry.spec.lua new file mode 100644 index 0000000000..254be99b19 --- /dev/null +++ b/BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Components/TextEntry.spec.lua @@ -0,0 +1,44 @@ +return function() + local Library = script.Parent.Parent + local Roact = require(Library.Parent.Parent.Roact) + + local MockWrapper = require(Library.MockWrapper) + + local TextEntry = require(script.Parent.TextEntry) + + local function createTestTextEntry(text, visible) + return Roact.createElement(MockWrapper, {}, { + TextEntry = Roact.createElement(TextEntry, { + Text = text, + Visible = visible, + TextSize = 22, + }), + }) + end + + it("should create and destroy without errors", function() + local element = createTestTextEntry() + local instance = Roact.mount(element) + Roact.unmount(instance) + end) + + it("should render correctly", function () + local container = Instance.new("Folder") + local instance = Roact.mount(createTestTextEntry("", true), container) + local frame = container:FindFirstChildOfClass("Frame") + + expect(frame.Text).to.be.ok() + + Roact.unmount(instance) + end) + + it("should hide its text when not visible", function () + local container = Instance.new("Folder") + local instance = Roact.mount(createTestTextEntry("", false), container) + local frame = container:FindFirstChildOfClass("Frame") + + expect(frame.Text.Visible).to.equal(false) + + Roact.unmount(instance) + end) +end \ No newline at end of file diff --git a/BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Components/Timeline/Keyframe.lua b/BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Components/Timeline/Keyframe.lua new file mode 100644 index 0000000000..fe02fb2ad6 --- /dev/null +++ b/BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Components/Timeline/Keyframe.lua @@ -0,0 +1,74 @@ +-- TODO: Delete file when FFlagRemoveUILibraryTimeline is retired +--[[ + A single keyframe which can be displayed on a media timeline. + + Props: + int Width = The size in pixels of the keyframe item. Determines both width and height. + UDim2 Position = The position of the keyframe. + int ZIndex = The display order of the keyframe. + int BorderSizePixel = The size of the keyframe's border highlight. + string Style = A style key for coloring this keyframe. Indexed into the keyframe theme. + + bool Selected = Whether this keyframe is currently selected. Changes the appearance. + + function OnActivated = A callback for when the user clicks on this keyframe. + function OnRightClick = A callback for when the user right-clicks on this keyframe. + function OnInputBegan = A callback for when the user starts interacting with the keyframe. + function OnInputEnded = A callback for when the user stops interacting with the keyframe. +]] + +local Library = script.Parent.Parent.Parent +local Roact = require(Library.Parent.Parent.Roact) +local Theming = require(Library.Theming) +local withTheme = Theming.withTheme + +local DEFAULT_WIDTH = 10 +local DEFAULT_BORDER_SIZE = 2 + +local Keyframe = Roact.PureComponent:extend("Keyframe") + +function Keyframe:render() + return withTheme(function(theme) + local props = self.props + + local style = props.Style + local selected = props.Selected + + local themeBase = style and theme.keyframe[style] or theme.keyframe.Default + local keyframeTheme = selected and themeBase.selected or themeBase + + local position = props.Position + local borderSize = props.BorderSizePixel or DEFAULT_BORDER_SIZE + local width = props.Width or DEFAULT_WIDTH + local zindex = props.ZIndex + + local onActivated = props.OnActivated + local onRightClick = props.OnRightClick + local onInputBegan = props.OnInputBegan + local onInputEnded = props.OnInputEnded + + return Roact.createElement("ImageButton", { + Size = UDim2.new(0, width, 0, width), + AnchorPoint = Vector2.new(0.5, 0.5), + Position = position, + Rotation = 45, + ZIndex = zindex, + + ImageTransparency = 1, + BackgroundTransparency = 0, + AutoButtonColor = false, + + BorderSizePixel = borderSize, + BorderColor3 = keyframeTheme.borderColor, + BackgroundColor3 = keyframeTheme.backgroundColor, + + [Roact.Event.Activated] = onActivated, + [Roact.Event.MouseButton2Click] = onRightClick, + + [Roact.Event.InputBegan] = onInputBegan, + [Roact.Event.InputEnded] = onInputEnded, + }, props[Roact.Children]) + end) +end + +return Keyframe diff --git a/BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Components/Timeline/Keyframe.spec.lua b/BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Components/Timeline/Keyframe.spec.lua new file mode 100644 index 0000000000..57826636bf --- /dev/null +++ b/BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Components/Timeline/Keyframe.spec.lua @@ -0,0 +1,31 @@ +-- TODO: Delete file when FFlagRemoveUILibraryTimeline is retired +return function() + local Library = script.Parent.Parent.Parent + local Roact = require(Library.Parent.Parent.Roact) + + local MockWrapper = require(Library.MockWrapper) + + local Keyframe = require(script.Parent.Keyframe) + + local function createTestKeyframe(enabled, selected) + return Roact.createElement(MockWrapper, {}, { + keyframe = Roact.createElement(Keyframe), + }) + end + + it("should create and destroy without errors", function() + local element = createTestKeyframe() + local instance = Roact.mount(element) + Roact.unmount(instance) + end) + + it("should render correctly", function () + local container = Instance.new("Folder") + local instance = Roact.mount(createTestKeyframe(), container) + local frame = container:FindFirstChildOfClass("ImageButton") + + expect(frame).to.be.ok() + + Roact.unmount(instance) + end) +end \ No newline at end of file diff --git a/BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Components/Timeline/Scrubber.lua b/BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Components/Timeline/Scrubber.lua new file mode 100644 index 0000000000..8ef1706dad --- /dev/null +++ b/BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Components/Timeline/Scrubber.lua @@ -0,0 +1,66 @@ +-- TODO: Delete file when FFlagRemoveUILibraryTimeline is retired +--[[ + Generic implementation of a Scrubber for a Timeline + + Properties: + UDim2 Position = position of the scrubber head + UDim2 HeadSize = size of the scrubber head + float Height = length of the scrubber line + bool ShowHead = whether or not the scrubber head is visible + Vector2 AnchorPoint = anchor point for the Scrubber component + int ZIndex = display order of the scrubber component + int thickness = pixel width of the scrubber line +]] + +local Library = script.Parent.Parent.Parent +local Roact = require(Library.Parent.Parent.Roact) +local Theming = require(Library.Theming) +local withTheme = Theming.withTheme + +local Scrubber = Roact.PureComponent:extend("Scrubber") + +function Scrubber:render() + return withTheme(function(theme) + local props = self.props + + local position = props.Position + local headSize = props.HeadSize + local height = props.Height + local showHead = props.ShowHead + local anchorPoint = props.AnchorPoint + local zIndex = props.ZIndex + local thickness = props.Thickness + + local children = props[Roact.Children] + if not children then + children = {} + end + if showHead then + table.insert(children, Roact.createElement("ImageLabel", { + Image = theme.scrubber.image, + ImageColor3 = theme.scrubber.backgroundColor, + BackgroundTransparency = 1, + Size = UDim2.new(1, 0, 1, 0), + })) + end + + table.insert(children, Roact.createElement("Frame", { + Position = UDim2.new(0.5, 0, 0, 0), + Size = UDim2.new(0, thickness, 0, height), + BackgroundColor3 = theme.scrubber.backgroundColor, + AnchorPoint = Vector2.new(0.5, 0), + BorderSizePixel = 0, + })) + + return Roact.createElement("Frame", { + BackgroundTransparency = 1, + Position = position, + Size = headSize, + ZIndex = zIndex, + AnchorPoint = anchorPoint, + [Roact.Event.InputBegan] = self.onDragBegan, + }, children) + end) +end + +return Scrubber \ No newline at end of file diff --git a/BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Components/Timeline/Scrubber.spec.lua b/BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Components/Timeline/Scrubber.spec.lua new file mode 100644 index 0000000000..4efff8bc32 --- /dev/null +++ b/BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Components/Timeline/Scrubber.spec.lua @@ -0,0 +1,54 @@ +-- TODO: Delete file when FFlagRemoveUILibraryTimeline is retired +return function() + local Library = script.Parent.Parent.Parent + local Roact = require(Library.Parent.Parent.Roact) + + local MockWrapper = require(Library.MockWrapper) + + local Scrubber = require(script.Parent.Scrubber) + + local function createTestScrubber(showHead) + return Roact.createElement(MockWrapper, {}, { + Scrubber = Roact.createElement(Scrubber, { + Height = 1000, + HeadSize = UDim2.new(0, 48, 0, 48), + ShowHead = showHead, + AnchorPoint = Vector2.new(0.5, 0), + Thickness = 1, + }) + }) + end + + it("should create and destroy without errors", function() + local element = createTestScrubber(true) + local instance = Roact.mount(element) + Roact.unmount(instance) + end) + + + describe("should render correctly", function() + it("should render with head correctly", function() + local container = Instance.new("Folder") + local instance = Roact.mount(createTestScrubber(true), container) + local frame = container:FindFirstChildOfClass("Frame") + + expect(frame).to.be.ok() + expect(frame["1"]).to.be.ok() + expect(frame["2"]).to.be.ok() + + Roact.unmount(instance) + end) + + it("should render without head correctly", function() + local container = Instance.new("Folder") + local instance = Roact.mount(createTestScrubber(false), container) + local frame = container:FindFirstChildOfClass("Frame") + + expect(frame).to.be.ok() + expect(#frame:GetChildren()).to.be.equal(1) + expect(frame["1"]).to.be.ok() + + Roact.unmount(instance) + end) + end) +end \ No newline at end of file diff --git a/BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Components/TitledFrame.lua b/BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Components/TitledFrame.lua new file mode 100644 index 0000000000..02a8431675 --- /dev/null +++ b/BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Components/TitledFrame.lua @@ -0,0 +1,57 @@ +--[[ + A frame with a title offset to the left side. + Used as a distinct vertical entry on a SettingsPage. + + Props: + string Title = The text to display in this TitledFrame's left-hand title. + int MaxHeight = The maximum height of this TitledFrame in pixels. Defaults to 100. + int LayoutOrder = The order which this TitledFrame will sort to in a UIListLayout. + int TextSize = The size of text +]] + +local Library = script.Parent.Parent +local Roact = require(Library.Parent.Parent.Roact) +local Theming = require(Library.Theming) +local withTheme = Theming.withTheme + +local CENTER_GUTTER = 180 + +local function TitledFrame(props) + return withTheme(function(theme) + local textSize = props.TextSize + local centerGutter = props.CenterGutter or CENTER_GUTTER + + return Roact.createElement("Frame", { + BackgroundTransparency = 1, + BorderSizePixel = 0, + ZIndex = props.ZIndex or 1, + Size = UDim2.new(1, 0, 0, props.MaxHeight or 100), + LayoutOrder = props.LayoutOrder or 1, + }, { + Title = Roact.createElement("TextLabel", { + BackgroundTransparency = 1, + BorderSizePixel = 0, + Size = UDim2.new(0, centerGutter, 1, 0), + + TextColor3 = theme.titledFrame.text, + Font = theme.titledFrame.font, + TextSize = textSize, + TextXAlignment = Enum.TextXAlignment.Left, + TextYAlignment = Enum.TextYAlignment.Top, + Text = props.Title, + TextWrapped = true, + }), + + Content = Roact.createElement("Frame", { + BackgroundTransparency = 1, + BorderSizePixel = 0, + + Size = UDim2.new(1, -centerGutter, 1, 0), + AnchorPoint = Vector2.new(1, 0), + Position = UDim2.new(1, 0, 0, 0), + }, props[Roact.Children]), + }) + end) +end + +return TitledFrame \ No newline at end of file diff --git a/BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Components/TitledFrame.spec.lua b/BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Components/TitledFrame.spec.lua new file mode 100644 index 0000000000..048a470fbc --- /dev/null +++ b/BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Components/TitledFrame.spec.lua @@ -0,0 +1,34 @@ +return function() + local Library = script.Parent.Parent + local Roact = require(Library.Parent.Parent.Roact) + + local MockWrapper = require(Library.MockWrapper) + + local TitledFrame = require(script.Parent.TitledFrame) + + + local function createTestTitledFrame() + return Roact.createElement(MockWrapper, {}, { + TitledFrame = Roact.createElement(TitledFrame, { + Title = "Title", + }), + }) + end + + it("should create and destroy without errors", function() + local element = createTestTitledFrame() + local instance = Roact.mount(element) + Roact.unmount(instance) + end) + + it("should render correctly", function () + local container = Instance.new("Folder") + local instance = Roact.mount(createTestTitledFrame(), container) + local titledFrame = container:FindFirstChildOfClass("Frame") + + expect(titledFrame.Title).to.be.ok() + expect(titledFrame.Content).to.be.ok() + + Roact.unmount(instance) + end) +end \ No newline at end of file diff --git a/BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Components/ToggleButton.lua b/BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Components/ToggleButton.lua new file mode 100644 index 0000000000..69ac76ab8a --- /dev/null +++ b/BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Components/ToggleButton.lua @@ -0,0 +1,61 @@ +--[[ + A toggle button with on and off state. + + Necessary props: + Position = explicit position, if not placed in UIListLayout + + bool Enabled = Whether or not this button can be clicked. + bool IsOn = whether the button should be on or off + + function onToggle = The function that will be called when this button is clicked to turn on and off + + Optional pros: + int LayoutOrder = The order this ToggleButton will sort to when placed in a UIListLayout +]] + +local Library = script.Parent.Parent +local Roact = require(Library.Parent.Parent.Roact) + +local Theming = require(Library.Theming) +local withTheme = Theming.withTheme + +local ToggleButton = Roact.PureComponent:extend("ToggleButton") + +function ToggleButton:init(props) + self.onToggle = function() + self.props.onToggle(not self.props.IsOn) + end +end + +function ToggleButton:render() + return withTheme(function(theme) + local props = self.props + + local toogleButtonTheme = theme.toggleButton + + local backgroundImage + if props.Enabled then + if props.IsOn then + backgroundImage = toogleButtonTheme.onImage + else + backgroundImage = toogleButtonTheme.offImage + end + else + backgroundImage = toogleButtonTheme.disabledImage + end + + return Roact.createElement("ImageButton", { + BackgroundTransparency = 1, -- Necessary to make the rounded background + Image = backgroundImage, + + Position = props.Position, + Size = props.Size or UDim2.new(0, toogleButtonTheme.defaultWidth, 0, toogleButtonTheme.defaultHeight), + + LayoutOrder = props.LayoutOrder or 1, + + [Roact.Event.Activated] = self.onToggle, + }) + end) +end + +return ToggleButton diff --git a/BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Components/ToggleButton.spec.lua b/BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Components/ToggleButton.spec.lua new file mode 100644 index 0000000000..56fd52742a --- /dev/null +++ b/BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Components/ToggleButton.spec.lua @@ -0,0 +1,30 @@ +return function() + local Library = script.Parent.Parent + local Roact = require(Library.Parent.Parent.Roact) + + local MockWrapper = require(Library.MockWrapper) + + local ToggleButton = require(script.Parent.ToggleButton) + + local function createTestToggleButton() + return Roact.createElement(MockWrapper, {}, { + ToggleButton = Roact.createElement(ToggleButton, { + Size = UDim2.new(0, 20, 0, 20), + Enabled = true, + IsOn = true, + + OnClickedOn = function() + end, + + OnClickedOff = function() + end, + }), + }) + end + + it("should create and destroy without errors", function() + local element = createTestToggleButton() + local instance = Roact.mount(element) + Roact.unmount(instance) + end) +end \ No newline at end of file diff --git a/BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Components/Tooltip.lua b/BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Components/Tooltip.lua new file mode 100644 index 0000000000..dd7f6e7fa3 --- /dev/null +++ b/BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Components/Tooltip.lua @@ -0,0 +1,193 @@ +--[[ + An element which can be added to a component to give that component a tooltip. + When the user hovers the mouse over the component, the tooltip will appear + after a short delay. + + Props: + string Text = The text to display in the tooltip. + float ShowDelay = The time in seconds before the tooltip appears + after the user stops moving the mouse over the element. Defaults to 0.5. + bool Enabled = Whether the tooltip will display on hover. + int Priority = The display order of this element, compared to other focused + elements or elements that show on top. +]] + +local PADDING = 3 +local SHADOW_OFFSET = Vector2.new(3, 3) +local OFFSET = Vector2.new(10, 5) +local SHOW_DELAY_DEFAULT = 0.5 + +local RunService = game:GetService("RunService") +local TextService = game:GetService("TextService") + +local Library = script.Parent.Parent +local Roact = require(Library.Parent.Parent.Roact) + +local Theming = require(Library.Theming) +local withTheme = Theming.withTheme + +local Focus = require(Library.Focus) +local ShowOnTop = Focus.ShowOnTop +local withFocus = Focus.withFocus + +local DropShadow = require(Library.Components.DropShadow) + +local Tooltip = Roact.PureComponent:extend("Tooltip") + +function Tooltip:init(props) + local showDelay = props.ShowDelay or SHOW_DELAY_DEFAULT + + self.state = { + showToolTip = false, + } + + self.isHovered = false + self.mousePos = nil + + self.connectHover = function() + self.hoverConnection = RunService.Heartbeat:Connect(function() + if self.isHovered then + if tick() >= self.targetTime then + self.disconnectHover() + self:setState({ + showToolTip = true, + }) + end + end + end) + end + + self.disconnectHover = function() + if self.hoverConnection then + self.hoverConnection:Disconnect() + end + end + + self.mouseEnter = function(rbx, xpos, ypos) + self.isHovered = true + self.targetTime = tick() + showDelay + self.mousePos = Vector2.new(xpos, ypos) + self.connectHover() + end + + self.mouseMoved = function(rbx, xpos, ypos) + self.mousePos = Vector2.new(xpos, ypos) + self.targetTime = tick() + showDelay + end + + self.mouseLeave = function() + self.isHovered = false + self.targetTime = 0 + self.mousePos = nil + self.disconnectHover() + self:setState({ + showToolTip = false, + }) + end +end + +function Tooltip:willUnmount() + self.disconnectHover() +end + +function Tooltip:render() + return withTheme(function(theme) + return withFocus(function(pluginGui) + local props = self.props + local state = self.state + + local tooltipTheme = theme.tooltip + local textSize = tooltipTheme.textSize + + local text = props.Text + local enabled = props.Enabled + local priority = props.Priority or 0 + + local mousePos = self.mousePos + + local content = {} + + if state.showToolTip and mousePos and enabled and pluginGui then + local targetX = mousePos.X + OFFSET.X + local targetY = mousePos.Y + OFFSET.Y + + local targetWidth = pluginGui.AbsoluteSize.X + local targetHeight = pluginGui.AbsoluteSize.Y + + local textBound = TextService:GetTextSize(text, + textSize, tooltipTheme.font, Vector2.new(100, 9000)) + + local tooltipTargetWidth = textBound.X + 2 * PADDING + local tooltipTargetHeight = textBound.Y + 2 * PADDING + + if targetX + tooltipTargetWidth >= targetWidth then + targetX = targetWidth - tooltipTargetWidth + end + + if targetY + tooltipTargetHeight >= targetHeight then + targetY = targetHeight - tooltipTargetHeight + end + + content.TooltipContainer = Roact.createElement(ShowOnTop, { + Priority = priority, + }, { + Tooltip = Roact.createElement("Frame", { + Position = UDim2.new(0, targetX, 0, targetY), + Size = UDim2.new(0, tooltipTargetWidth, 0, tooltipTargetHeight), + BackgroundTransparency = 1, + ZIndex = 10, + }, { + DropShadow = Roact.createElement(DropShadow, { + Transparency = tooltipTheme.shadowTransparency, + Color = tooltipTheme.shadowColor, + Offset = SHADOW_OFFSET, + ZIndex = 1, + }), + + ContentFrame = Roact.createElement("Frame", { + Size = UDim2.new(1, 0, 1, 0), + ZIndex = 2, + + BackgroundColor3 = tooltipTheme.backgroundColor, + BorderColor3 = tooltipTheme.borderColor, + }, { + UIPadding = Roact.createElement("UIPadding", { + PaddingBottom = UDim.new(0, PADDING), + PaddingLeft = UDim.new(0, PADDING), + PaddingRight = UDim.new(0, PADDING), + PaddingTop = UDim.new(0, PADDING), + }), + + Label = Roact.createElement("TextLabel", { + Size = UDim2.new(1, 0, 1, 0), + BackgroundTransparency = 1, + BorderSizePixel = 0, + Text = text, + + TextColor3 = tooltipTheme.textColor, + + Font = tooltipTheme.font, + TextSize = textSize, + TextXAlignment = Enum.TextXAlignment.Left, + TextYAlignment = Enum.TextYAlignment.Top, + TextWrapped = true, + ZIndex = 3, + }), + }) + }) + }) + end + + return Roact.createElement("Frame",{ + Size = UDim2.new(1, 0, 1, 0), + BackgroundTransparency = 1, + + [Roact.Event.MouseEnter] = self.mouseEnter, + [Roact.Event.MouseMoved] = self.mouseMoved, + [Roact.Event.MouseLeave] = self.mouseLeave, + }, content) + end) + end) +end + +return Tooltip diff --git a/BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Components/Tooltip.spec.lua b/BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Components/Tooltip.spec.lua new file mode 100644 index 0000000000..28c7d6b015 --- /dev/null +++ b/BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Components/Tooltip.spec.lua @@ -0,0 +1,30 @@ +return function() + local Library = script.Parent.Parent + local Roact = require(Library.Parent.Parent.Roact) + + local MockWrapper = require(Library.MockWrapper) + + local Tooltip = require(script.Parent.Tooltip) + + local function createTestTooltip(props) + return Roact.createElement(MockWrapper, {}, { + Tooltip = Roact.createElement(Tooltip, props) + }) + end + + it("should create and destroy without errors", function() + local element = createTestTooltip() + local instance = Roact.mount(element) + Roact.unmount(instance) + end) + + it("should render correctly", function () + local container = Instance.new("Folder") + local instance = Roact.mount(createTestTooltip(), container) + local frame = container:FindFirstChildOfClass("Frame") + + expect(frame).to.be.ok() + + Roact.unmount(instance) + end) +end \ No newline at end of file diff --git a/BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Components/TreeView.lua b/BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Components/TreeView.lua new file mode 100644 index 0000000000..14751f8704 --- /dev/null +++ b/BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Components/TreeView.lua @@ -0,0 +1,524 @@ +--[[ + Displays a heirarchical data set. + + For these notes, assume that : + - DataNode = the type of the data you have passed into the TreeView + + Props: + (required) + dataTree : (DataNode) the first element in some heirarchical data set + getChildren : (function>(DataNode)) gets a list of the node's children + renderElement : (function(table)) renders the current element given a table of props + + (optional) + sortChildren : (function(DataNode, DataNode)) a comparator function passed to table.sort() + onSelectionChanged : (function(list)) a callback for observing when items are selected + expandAll: (bool) a check to determine if the entire tree should be initially expanded. + expandRoot: (bool) a check to determine if the root of the tree should be initially expanded. + createFlatList: (bool) a check to determine if a flat list or node/child structure should be used. + + Elements rendered by the TreeView are given the following props : + -- data information + element : (DataNode) + parent : (DataNode) + + -- styling information + rowIndex : (int) the current row of the element + indent : (int) the current depth of the element + canExpand : (bool) true if the element contains children + isExpanded : (bool) true if the element is currently showing its children + isSelected : (bool) true if the element has been selected, + + -- function callbacks + toggleExpanded : (function()) a function that tells the treeview to expand or collapse this row + toggleSelected : (function(bool)) a function that tells the treeview to select this row +]] +local FFlagStudioFixTreeViewForSquish = settings():GetFFlag("StudioFixTreeViewForSquish") +-- Related Ticket https://jira.rbx.com/browse/CLISTUDIO-21831 +local FFlagStudioFixTreeViewForFlatList = settings():GetFFlag("StudioFixTreeViewForFlatList") +local FFlagFixTreeViewFlatListDefault = game:DefineFastFlag("FixTreeViewFlatListDefault", false) + +local Library = script.Parent.Parent + +local Roact = require(Library.Parent.Parent.Roact) +local Theming = require(Library.Theming) +local withTheme = Theming.withTheme + +local TreeView = Roact.PureComponent:extend("TreeView") + +function TreeView:init() + assert(self.props.dataTree ~= nil, "TreeView expected a dataTree, but none was provided") + assert(type(self.props.getChildren) == "function", "TreeView expected getChildren() to be defined") + if self.props.sortChildren then + assert(type(self.props.sortChildren) == "function", "TreeView expected sortChildren()") + end + + local dataTreeRoot = self.props.dataTree + local getChildrenFunc = self.props.getChildren + local sortChildrenFunc = self.props.sortChildren + local expandAll = self.props.expandAll + local expandRoot = self.props.expandRoot + + self.layoutRef = Roact.createRef() + self.contentRef = Roact.createRef() + self.scrollbarResizeSignalToken = nil + + self.previousNodesArray = {} + self.nodesArray = {} + + self.state = { + expandedItems = {}, + selectedItems = {}, + } + + --[[ + resizeScrollContent : a callback for when the content in the treeview resizes. Ensures that all content can be seen + ]] + self.resizeScrollContent = function() + if not FFlagStudioFixTreeViewForSquish then + return + end + + -- keep the canvas size equal to the size of the content in it + local absoluteContentSize = self.layoutRef.current.AbsoluteContentSize + self.contentRef.current.CanvasSize = UDim2.new(0, absoluteContentSize.X, 0, absoluteContentSize.Y) + end + + --[[ + onTreeUpdated: fires a callback (if provided) for whenever the tree is created/updated. Provides the nodes of the tree + in a list format. + ]] + self.onTreeUpdated = function() + local function treeChanged() + if #self.previousNodesArray ~= #self.nodesArray then + return true + end + + for index, node in ipairs(self.previousNodesArray) do + if self.nodesArray[index] ~= node then + return true + end + end + + return false + end + if self.props.onTreeUpdated and treeChanged() then + self.props.onTreeUpdated(self.nodesArray) + end + end + + --[[ + toggleStateValue : a helper function for changing state values. + + Since Roact only does partial merges of keys, this function is to help ensure that keys get removed + when they are assigned nil. + + Args : + tableName : (string) a table key defined in self.state + element : (DataNode) + copyOldState : (boolean) when true, preserves the old state. When false, removes old state entirely + ]] + self.toggleStateValue = function(tableName, element, copyOldState) + assert(tableName ~= nil, "Expected a table name, but found none") + assert(self.state[tableName] ~= nil, string.format("%s does not exist in the state table", tostring(tableName))) + assert(element ~= nil, string.format("Expected an element to add to %s but found none", tableName)) + assert(type(copyOldState) == "boolean", "Expected copyOldState to be a boolean") + + local newValue = true + if self.state[tableName][element] then + newValue = nil + end + + -- copy the old table and update with the new value + local newState = { + [element] = newValue + } + if copyOldState then + for k, v in pairs(self.state[tableName]) do + if k ~= element then + newState[k] = v + end + end + end + + self:setState({ + [tableName] = newState + }) + + return newState + end + + self.toggleExpandedElement = function(element) + return function() + if element == nil then + return + end + + self.toggleStateValue("expandedItems", element, true) + end + end + + self.toggleSelectedElement = function(element) + -- shouldSelectAlso : (bool) if true, does not clear the previous selection + return function(shouldSelectAlso) + if shouldSelectAlso == nil then + shouldSelectAlso = false + end + assert(type(shouldSelectAlso) == "boolean", "Expected shouldSelectAlso to be a boolean") + + local onSelectionChanged = self.props.onSelectionChanged + local newState = self.toggleStateValue("selectedItems", element, shouldSelectAlso) + + if onSelectionChanged then + -- return a list of selected elements + local selectedData = {} + for k, isSelected in pairs(newState) do + if isSelected then + table.insert(selectedData, k) + end + end + onSelectionChanged(selectedData) + end + end + end + + --[[ + createNode : provides a bunch of props to the consumer's renderElement callback + + Args : + element : (DataNode) + parent : (DataNode) + rowIndex : (int) the current row of the element + indent : (int) the current depth of the element + ]] + -- Remove with FFlagStudioFixTreeViewForSquish + self.DEPRECATED_createNode = function(element, parent, rowIndex, indent) + local expandedItems = self.state.expandedItems + local selectedItems = self.state.selectedItems + local renderElement = self.props.renderElement + local getChildren = self.props.getChildren + + local canExpand = next(getChildren(element)) ~= nil + + local props = { + -- data information + element = element, + parent = parent, + + -- styling information + rowIndex = rowIndex, + indent = indent, + canExpand = canExpand, + isExpanded = expandedItems[element] == true, + isSelected = selectedItems[element] == true, + + -- function callbacks + toggleExpanded = self.toggleExpandedElement(canExpand and element or nil), + toggleSelected = self.toggleSelectedElement(element), + } + + return renderElement(props) + end + + self.createNode = function(element, rowIndex, indent, children) + local expandedItems = self.state.expandedItems + local selectedItems = self.state.selectedItems + local renderElement = self.props.renderElement + local getChildren = self.props.getChildren + + local canExpand = next(getChildren(element)) ~= nil + + local props = { + -- data information + element = element, + + -- styling information + rowIndex = rowIndex, + indent = indent, + canExpand = canExpand, + isExpanded = expandedItems[element] == true, + isSelected = selectedItems[element] == true, + + -- function callbacks + toggleExpanded = self.toggleExpandedElement(canExpand and element or nil), + toggleSelected = self.toggleSelectedElement(element), + + children = children, + } + + return renderElement(props) + end + + --[[ + traverseDepthFirst : visits all of the children in a tree structure, assuming they have been expanded + + Args: + parent : (DataNode) the element to search inside for children + depth : (int) a counter for indenting purposes + handlers : (table) all of the callbacks to properly traverse the tree + - onNodeVisited : (function(DataNode, int, DataNode)) + - decideShouldContinue : (function(DataNode, int, DataNode)) decides if it should continue + - getChildrenOfElement : (function>(DataNode)) gets a list of children from a parent + - sortChildren : (optional, function(DataNode, DataNode) a comparator function to sort the children + ]] + + -- Remove with FFlagStudioFixTreeViewForSquish + self.DEPRECATED_traverseDepthFirst = function(parent, depth, handlers) + local children = handlers.getChildrenOfElement(parent) + + if handlers.sortChildren then + table.sort(children, handlers.sortChildren) + end + + for _, child in pairs(children) do + -- alert any listeners that we've visited this node + handlers.onNodeVisited(child, depth + 1, parent) + + -- check if there are any children of this node we should traverse + local shouldContinue = handlers.decideShouldContinue(child, depth + 1, parent) + if shouldContinue then + self.DEPRECATED_traverseDepthFirst(child, depth + 1, handlers) + end + end + end + + self.traverseDepthFirst = function(current, depth, handlers) + if not handlers.decideShouldContinue(current) then + return handlers.onNodeVisited(current, depth, {}) + end + + local children = handlers.getChildrenOfElement(current) + + local childComponents = {} + + local createFlatList + if FFlagFixTreeViewFlatListDefault then + if self.props.createFlatList == nil then + createFlatList = true + else + createFlatList = self.props.createFlatList + end + else + createFlatList = FFlagStudioFixTreeViewForFlatList and self.props.createFlatList + end + + if handlers.sortChildren then + table.sort(children, handlers.sortChildren) + end + + if createFlatList then + handlers.onNodeVisited(current, depth, {}) + for _, child in pairs(children) do + self.traverseDepthFirst(child, depth + 1, handlers) + end + else + for _, child in pairs(children) do + local childComponent = self.traverseDepthFirst(child, depth + 1, handlers) + table.insert(childComponents, childComponent) + end + + return handlers.onNodeVisited(current, depth, childComponents) + end + end + --[[ + getVisibleNodes : returns a map of the elements to render into the tree, including the root + ]] + self.getVisibleNodes = function() + self.previousNodesArray = self.nodesArray + self.nodesArray = {} + + local expandedItems = self.state.expandedItems + + local root = self.props.dataTree + local getChildren = self.props.getChildren + local sortChildren = self.props.sortChildren + local createFlatList + if FFlagFixTreeViewFlatListDefault then + if self.props.createFlatList == nil then + createFlatList = true + else + createFlatList = self.props.createFlatList + end + else + createFlatList = FFlagStudioFixTreeViewForFlatList and self.props.createFlatList + end + + local numNodes = 1 + local treeNodes + if not FFlagStudioFixTreeViewForSquish then + treeNodes = { + Root = self.DEPRECATED_createNode(root, nil, 0, 0), + } + else + treeNodes = {} + end + + if expandedItems[root] then + if FFlagStudioFixTreeViewForSquish then + treeNodes.Root = self.traverseDepthFirst(root, 0, { + -- upon visiting a node, add it to the map of elements to display + onNodeVisited = function(child, depth, children) + local node = self.createNode(child, numNodes, depth, children) + + if createFlatList then + if node then + local nodeName = string.format("Node-%d", numNodes) + treeNodes[nodeName] = node + numNodes = numNodes + 1 + end + + if FFlagFixTreeViewFlatListDefault then + table.insert(self.nodesArray, child) + end + else + numNodes = numNodes + 1 + return node + end + end, + + -- when deciding whether to continue traversing the child elements, check if it is expanded + decideShouldContinue = function(child) + return expandedItems[child] == true + end, + + -- allow the consumer to figure out how to get the children of each element + getChildrenOfElement = getChildren, + sortChildren = sortChildren }) + else + self.DEPRECATED_traverseDepthFirst(root, 0, { + -- upon visiting a node, add it to the map of elements to display + onNodeVisited = function(child, depth, parent) + local nodeName = string.format("Node-%d", numNodes) + treeNodes[nodeName] = self.DEPRECATED_createNode(child, parent, numNodes, depth) + + numNodes = numNodes + 1 + table.insert(self.nodesArray, child) + end, + + -- when deciding whether to continue traversing the child elements, check if it is expanded + decideShouldContinue = function(child, depth, parent) + return expandedItems[child] == true + end, + + -- allow the consumer to figure out how to get the children of each element + getChildrenOfElement = getChildren, + sortChildren = sortChildren }) + end + elseif FFlagStudioFixTreeViewForSquish then + treeNodes.Root = self.createNode(root, 0 , 0, {}) + end + + return treeNodes + end + + -- if the tree is marked as expandAll, then show all the nodes by default + if expandAll then + local expandedItems = { + [dataTreeRoot] = true, + } + if FFlagStudioFixTreeViewForSquish then + self.traverseDepthFirst(dataTreeRoot, 0, { + onNodeVisited = function(child) + expandedItems[child] = true + end, + decideShouldContinue = function() + return true + end, + getChildrenOfElement = getChildrenFunc, + sortChildren = sortChildrenFunc, + }) + else + self.DEPRECATED_traverseDepthFirst(dataTreeRoot, 0, { + onNodeVisited = function(child) + expandedItems[child] = true + end, + decideShouldContinue = function() + return true + end, + getChildrenOfElement = getChildrenFunc, + sortChildren = sortChildrenFunc, + }) + end + self.state.expandedItems = expandedItems + end + + if expandRoot then + self.state.expandedItems = { + [dataTreeRoot] = true, + } + end +end + +function TreeView:render() + return withTheme(function(theme) + local props = self.props + + local padding = theme.treeView.scrollbar.scrollbarPadding + + local size = FFlagStudioFixTreeViewForSquish and props.Size or UDim2.new(1, -2*padding, 1, -2*padding) + + local layoutOrder = props.LayoutOrder + + local childrenPadding = not FFlagStudioFixTreeViewForSquish and UDim.new(0, theme.treeView.elementPadding) or nil + + local treeViewChildren = { + Layout = Roact.createElement("UIListLayout", { + SortOrder = Enum.SortOrder.LayoutOrder, + FillDirection = Enum.FillDirection.Vertical, + Padding = childrenPadding, + [Roact.Ref] = self.layoutRef, + [Roact.Change.AbsoluteContentSize] = self.resizeScrollContent, + }) + } + + for name, node in pairs(self.getVisibleNodes()) do + -- each of these children will be rendered by the consumer + treeViewChildren[name] = node + end + + return Roact.createElement("ScrollingFrame", { + Position = UDim2.new(0, padding, 0, padding), + Size = size, + BorderSizePixel = 0, + BackgroundTransparency = 1, + ScrollBarThickness = theme.treeView.scrollbar.scrollbarThickness, + ClipsDescendants = true, + LayoutOrder = layoutOrder, + + TopImage = theme.treeView.scrollbar.topImage, + MidImage = theme.treeView.scrollbar.midImage, + BottomImage = theme.treeView.scrollbar.bottomImage, + + ScrollBarImageColor3 = theme.treeView.scrollbar.scrollbarImageColor, + + ElasticBehavior = Enum.ElasticBehavior.Always, + ScrollingDirection = Enum.ScrollingDirection.XY, + + [Roact.Ref] = self.contentRef, + }, treeViewChildren) + end) +end + +function TreeView:didUpdate() + self.onTreeUpdated() +end + +function TreeView:didMount() + if not FFlagStudioFixTreeViewForSquish then + local resizeSignal = self.layoutRef.current:GetPropertyChangedSignal("AbsoluteContentSize") + self.scrollbarResizeSignalToken = resizeSignal:Connect(self.resizeScrollContent) + end + + self.onTreeUpdated() +end + +function TreeView:didUnmount() + self.previousNodesArray = nil + self.nodesArray = nil + if self.scrollbarResizeSignalToken then + self.scrollbarResizeSignalToken:Disconnect() + self.scrollbarResizeSignalToken = nil + end +end + +return TreeView \ No newline at end of file diff --git a/BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Components/TreeView.spec.lua b/BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Components/TreeView.spec.lua new file mode 100644 index 0000000000..482c173dc5 --- /dev/null +++ b/BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Components/TreeView.spec.lua @@ -0,0 +1,355 @@ +local TreeView = require(script.Parent.TreeView) + +local Library = script.Parent.Parent +local Roact = require(Library.Parent.Parent.Roact) +local MockWrapper = require(Library.MockWrapper) + +local FFlagStudioFixTreeViewForSquish = settings():GetFFlag("StudioFixTreeViewForSquish") + +local function mockDataNode(value, parent) + local Node = { + Value = value, + Children = {}, + } + + if parent then + table.insert(parent.Children, Node) + end + + return Node +end + +local function mockDataTree() + local root = mockDataNode("Players") + local node1 = mockDataNode("John Doe", root) + local node2 = mockDataNode("Jane Doe", root) + local node3 = mockDataNode("Builderman", root) + mockDataNode("Sword", node1) + mockDataNode("Shield", node1) + mockDataNode("Gun", node2) + mockDataNode("Hammer", node3) + + return root +end + +local function mockGetChildren(node) + return node.Children +end + +local function mockRenderElement(props) + return Roact.createElement("Frame",{}) +end + +local function mockSortChildren(nodeA, nodeB) + return nodeA.Value > nodeB.Value +end + +return function() + describe("TreeView", function() + it("should create and destroy without errors", function() + local element = Roact.createElement(MockWrapper, {}, { + TreeView = Roact.createElement(TreeView, { + dataTree = mockDataTree(), + getChildren = mockGetChildren, + renderElement = mockRenderElement, + + -- optional props + sortChildren = mockSortChildren, + }), + }) + local container = Instance.new("Frame") + local instance = Roact.mount(element, container) + Roact.unmount(instance) + end) + + it("should error when it is missing important props", function() + expect(function() + local element = Roact.createElement(MockWrapper, {}, { + TreeView = Roact.createElement(TreeView, { + dataTree = mockDataTree(), + getChildren = mockGetChildren, + }), + }) + local instance = Roact.mount(element) + Roact.unmount(instance) + end).to.throw() + + expect(function() + local element = Roact.createElement(MockWrapper, {}, { + TreeView = Roact.createElement(TreeView, { + dataTree = mockDataTree, + renderElement = mockRenderElement, + }), + }) + local instance = Roact.mount(element) + Roact.unmount(instance) + end).to.throw() + + expect(function() + local element = Roact.createElement(MockWrapper, {}, { + TreeView = Roact.createElement(TreeView, { + getChildren = mockGetChildren, + renderElement = mockRenderElement, + }), + }) + local instance = Roact.mount(element) + Roact.unmount(instance) + end).to.throw() + end) + + it("should render correctly", function() + local element = Roact.createElement(MockWrapper, {}, { + TreeView = Roact.createElement(TreeView, { + dataTree = mockDataTree(), + getChildren = mockGetChildren, + renderElement = mockRenderElement, + }), + }) + local container = Instance.new("Folder") + local instance = Roact.mount(element, container) + + local treeView = container:FindFirstChildOfClass("ScrollingFrame") + expect(treeView).to.be.ok() + expect(treeView.Root).to.be.ok() + expect(treeView.Layout).to.be.ok() + + Roact.unmount(instance) + end) + + it("should render the children when you expand the node", function() + local nodesRenderedCount = 0 + local expandChildFunc + local function renderElement(props) + -- if rendering the root node, grab the callback to expand it + if props.element.Value == "Players" then + expandChildFunc = props.toggleExpanded + end + + nodesRenderedCount = nodesRenderedCount + 1 + + -- create an element + return Roact.createElement("TextLabel", { + Text = props.element.Value + }, props.children) + end + + local count = 0 + local function dfCount(root) + local children = root:GetChildren() + count = count + 1 + + if #children == 0 then + return + end + + for _, child in ipairs(children) do + dfCount(child) + end + end + + -- render the tree + local element = Roact.createElement(MockWrapper, {}, { + TreeView = Roact.createElement(TreeView, { + dataTree = mockDataTree(), + getChildren = mockGetChildren, + renderElement = renderElement, + }), + }) + local container = Instance.new("Folder") + local instance = Roact.mount(element, container) + local treeView = container:FindFirstChildOfClass("ScrollingFrame") + + -- Remove with FFlagStudioFixTreeViewForSquish + local renderedChildren + renderedChildren = treeView:GetChildren() + + if FFlagStudioFixTreeViewForSquish then + dfCount(treeView) + expect(count).to.equal(nodesRenderedCount + 2)-- should equal number of nodes + 1 UIListLayout + 1 for RoactTree + else + expect(#renderedChildren).to.equal(nodesRenderedCount + 1)-- should equal number of nodes + 1 UIListLayout + end + expect(nodesRenderedCount).to.equal(1) + + -- expand the root node, it should re-render the root node and its three children + nodesRenderedCount = 0 + expandChildFunc() + + -- it should have rendered the children + treeView = container:FindFirstChildOfClass("ScrollingFrame") + renderedChildren = treeView:GetChildren() + if FFlagStudioFixTreeViewForSquish then + count = 0 + dfCount(treeView) + expect(count).to.equal(nodesRenderedCount + 2)-- should equal number of nodes + 1 UIListLayout + 1 for RoactTree + else + expect(#renderedChildren).to.equal(nodesRenderedCount + 1)-- should equal number of nodes + 1 UIListLayout + end + + local foundChildNodes = 0 + local foundRoot = false + local foundChild1 = false + local foundChild2 = false + local foundChild3 = false + if FFlagStudioFixTreeViewForSquish then + local function dfs(node) + local children = node:GetChildren() + + if node:IsA("TextLabel") then + foundChildNodes = foundChildNodes + 1 + if node.Text == "Players" then + foundRoot = true + elseif node.Text == "John Doe" then + foundChild1 = true + elseif node.Text == "Jane Doe" then + foundChild2 = true + elseif node.Text == "Builderman" then + foundChild3 = true + end + end + + if #children == 0 then + return + end + + for _, child in ipairs(children) do + dfs(child) + end + end + + dfs(treeView) + else + for _, childNode in ipairs(renderedChildren) do + if childNode:IsA("TextLabel") then + foundChildNodes = foundChildNodes + 1 + if childNode.Text == "Players" then + foundRoot = true + elseif childNode.Text == "John Doe" then + foundChild1 = true + elseif childNode.Text == "Jane Doe" then + foundChild2 = true + elseif childNode.Text == "Builderman" then + foundChild3 = true + end + end + end + end + expect(foundChildNodes).to.equal(nodesRenderedCount) + expect(foundRoot).to.equal(true) + expect(foundChild1).to.equal(true) + expect(foundChild2).to.equal(true) + expect(foundChild3).to.equal(true) + + -- clean up + Roact.unmount(instance) + end) + + it("should allow you to select one or multiple elements in the tree", function() + local isRootSelected = false + local selectNodeFunc + + local function renderElement(props) + isRootSelected = props.isSelected + selectNodeFunc = props.toggleSelected + + return Roact.createElement("Frame") + end + + local element = Roact.createElement(MockWrapper, {}, { + TreeView = Roact.createElement(TreeView, { + dataTree = mockDataTree(), + getChildren = mockGetChildren, + renderElement = renderElement, + }), + }) + local container = Instance.new("Folder") + local instance = Roact.mount(element, container) + + -- select the root node + expect(isRootSelected).to.equal(false) + selectNodeFunc() + expect(isRootSelected).to.equal(true) + + Roact.unmount(instance) + end) + + it("should render all of the children immediately if expandAll is set", function() + local nodeCount = 0 + local renderElement = function(props) + nodeCount = nodeCount + 1 + return Roact.createElement("Frame", {}, props.children) + end + + local element = Roact.createElement(MockWrapper, {}, { + TreeView = Roact.createElement(TreeView, { + dataTree = mockDataTree(), + getChildren = mockGetChildren, + renderElement = renderElement, + expandAll = true, + }), + }) + local container = Instance.new("Folder") + local instance = Roact.mount(element, container) + + local treeView = container:FindFirstChildOfClass("ScrollingFrame") + local treeViewChildren = treeView:GetChildren() + + local count = 0 + local function dfCount(root) + local children = root:GetChildren() + count = count + 1 + + if #children == 0 then + return + end + + for _, child in ipairs(children) do + dfCount(child) + end + end + + -- mockDataTree has 8 nodes + expect(nodeCount).to.equal(8) + if FFlagStudioFixTreeViewForSquish then + dfCount(treeView) + expect(count).to.equal(nodeCount + 2) + else + -- there should be 8 nodes + 1 UIListLayout + expect(#treeViewChildren).to.equal(nodeCount + 1) + end + + Roact.unmount(instance) + end) + + itSKIP("should fire update callback", function() + local nodeCount = 0 + local renderElement = function(props) + nodeCount = nodeCount + 1 + return Roact.createElement("Frame", {}) + end + + local numInvoked = 0 + local treeList = nil + local element = Roact.createElement(MockWrapper, {}, { + TreeView = Roact.createElement(TreeView, { + dataTree = mockDataTree(), + getChildren = mockGetChildren, + renderElement = renderElement, + expandAll = true, + onTreeUpdated = function(tree) + treeList = tree + numInvoked = numInvoked + 1 + end, + }), + }) + local container = Instance.new("Folder") + local instance = Roact.mount(element, container) + + expect(treeList).to.be.ok() + expect(#treeList).to.equal(nodeCount) + expect(numInvoked).to.equal(1) + + Roact.unmount(instance) + end) + end) +end diff --git a/BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Components/createFitToContent.lua b/BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Components/createFitToContent.lua new file mode 100644 index 0000000000..014946770f --- /dev/null +++ b/BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Components/createFitToContent.lua @@ -0,0 +1,70 @@ +--[[ + This component generates a list of elements and resizes the container so that it fits the size of the list. + + Call the function createFitToContent and pass in the container, the layout of the elements, and any properties +]] +local FFlagFixFitToContentOnCloseError = game:DefineFastFlag("FixFitToContentOnCloseError", false) + +local Library = script.Parent.Parent +local Roact = require(Library.Parent.Parent.Roact) + +local join = require(Library.join) + +local function createFitToContent(containerComponent, layoutComponent, layoutProps) + local name = ("FitComponent(%s, %s)"):format(containerComponent, layoutComponent) + local FitComponent = Roact.Component:extend(name) + + function FitComponent:init() + self.layoutRef = Roact.createRef() + self.containerRef = Roact.createRef() + + self.layoutProps = join(layoutProps, { + [Roact.Ref] = self.layoutRef, + [Roact.Change.AbsoluteContentSize] = function() + if self.layoutRef.current ~= nil and self.containerRef.current ~= nil then + self:resizeContainer() + end + end, + }) + end + + function FitComponent:render() + assert(self.props.Size == nil, "Size must not be specified!") + + local children = join({ + ["Layout"] = Roact.createElement(layoutComponent, self.layoutProps), + }, self.props[Roact.Children]) + + local props = join(self.props, { + [Roact.Children] = children, + [Roact.Ref] = self.containerRef, + }) + + return Roact.createElement(containerComponent, props) + end + + function FitComponent:didMount() + self:resizeContainer() + end + + function FitComponent:didUpdate() + self:resizeContainer() + end + + function FitComponent:resizeContainer() + if FFlagFixFitToContentOnCloseError then + local layout = self.layoutRef.current + if layout then + local layoutSize = layout.AbsoluteContentSize + self.containerRef.current.Size = UDim2.new(1, 0, 0, layoutSize.Y) + end + else + local layoutSize = self.layoutRef.current.AbsoluteContentSize + self.containerRef.current.Size = UDim2.new(1, 0, 0, layoutSize.Y) + end + end + + return FitComponent +end + +return createFitToContent \ No newline at end of file diff --git a/BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Components/createFitToContent.spec.lua b/BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Components/createFitToContent.spec.lua new file mode 100644 index 0000000000..ed5fa5f239 --- /dev/null +++ b/BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Components/createFitToContent.spec.lua @@ -0,0 +1,44 @@ +return function() + local Library = script.Parent.Parent +local Roact = require(Library.Parent.Parent.Roact) + + local createFitToContent = require(script.Parent.createFitToContent) + + it("should create and destroy without errors", function() + local fitToContent = createFitToContent("Frame", "UIListLayout", {}) + local component = Roact.createElement(fitToContent, {}, {}) + local instance = Roact.mount(component) + + Roact.unmount(instance) + end) + + it("should throw an error if size is specified", function() + local fitToContent = createFitToContent("Frame", "UIListLayout", {}) + local component = Roact.createElement(fitToContent, { + Size = UDim2.new() + }, {}) + + expect(function() + Roact.mount(component) + end).to.throw() + end) + + it("should add a Layout to its children", function() + local container = Instance.new("Folder") + + local fitToContent = createFitToContent("Frame", "UIListLayout", {}) + local component = Roact.createElement(fitToContent, {}, { + Frame1 = Roact.createElement("Frame"), + Frame2 = Roact.createElement("Frame"), + }) + + local instance = Roact.mount(component, container) + local frame = container:FindFirstChildOfClass("Frame") + + expect(frame.Frame1).to.be.ok() + expect(frame.Frame2).to.be.ok() + expect(frame.Layout).to.be.ok() + + Roact.unmount(instance) + end) +end \ No newline at end of file diff --git a/BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Focus.lua b/BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Focus.lua new file mode 100644 index 0000000000..2aa90263cf --- /dev/null +++ b/BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Focus.lua @@ -0,0 +1,181 @@ +--[[ + A utility for elements that need to display on top of all other elements, + and elements that need to capture focus and block input to all other elements. + + Uses Portals to place elements in the main PluginGui. You will need to + pass in the main PluginGui when creating a FocusProvider. + + withFocus(pluginGui) + Gets the top-level PluginGui. Useful for querying its size or state. + For example, a Tooltip queries the size of the pluginGui to avoid + clipping the tooltip off of the right or bottom side of the screen. + + ShowOnTop + A Roact component that wraps its children such that they will be + rendered on top of all other components. + Props: + int Priority = The ZIndex of this component relative to other + focused elements. + + CaptureFocus + A Roact component that wraps its children such that they will be + rendered on top of all other components, and will block input to all + other components. + + Props: + int Priority = The ZIndex of this component relative to other + focused elements. + callback OnFocusLost = A callback for when the user clicks + outside of the focused element. + + KeyboardListener + A Roact component that listens to keyboard events within the PluginGui. + + Props: + callback OnKeyPressed(input, keysHeld) + A callback for when the user presses a key inside the plugin. + The input param is the InputObject for the InputBegan event. The + keysHeld param is a map containing every key that is currently held. + callback OnKeyReleased(input) + A callback for when the user releases a key. +]] + +local FOCUSED_ZINDEX = 100000 +local Library = script.Parent +local Roact = require(Library.Parent.Parent.Roact) +local Symbol = require(Library.Utils.Symbol) +local focusKey = Symbol.named("UILibraryFocus") + +local FocusProvider = Roact.PureComponent:extend("UILibraryFocusProvider") +function FocusProvider:init() + local pluginGui = self.props.pluginGui + assert(pluginGui ~= nil, "No pluginGui was given to this FocusProvider.") + + self._context[focusKey] = pluginGui +end +function FocusProvider:render() + return Roact.oneChild(self.props[Roact.Children]) +end + +-- the consumer should complain if it doesn't have a focus +local FocusConsumer = Roact.PureComponent:extend("UILibraryFocusConsumer") +function FocusConsumer:init() + assert(self._context[focusKey] ~= nil, "No FocusProvider found.") + assert(self.props.focusedRender ~= nil, "Use withFocus, not FocusConsumer.") + self.pluginGui = self._context[focusKey] +end +function FocusConsumer:render() + return self.props.focusedRender(self.pluginGui) +end + +-- withFocus should provide a simple way to make components that use focus +-- callback : function(FocusConsumer) +local function withFocus(callback) + return Roact.createElement(FocusConsumer, { + focusedRender = callback + }) +end + +local CaptureFocus = Roact.PureComponent:extend("UILibraryCaptureFocus") +function CaptureFocus:render() + return withFocus(function(pluginGui) + local priority = self.props.Priority or 0 + return Roact.createElement(Roact.Portal, { + target = pluginGui, + }, { + -- Consume all clicks outside the element to close it when it loses focus + TopLevelDetector = Roact.createElement("ImageButton", { + ZIndex = priority + FOCUSED_ZINDEX, + Size = UDim2.new(1, 0, 1, 0), + BackgroundTransparency = 1, + [Roact.Event.Activated] = self.props.OnFocusLost, + }, { + -- Also block all scrolling events going through + ScrollBlocker = Roact.createElement("ScrollingFrame", { + Size = UDim2.new(1, 0, 1, 0), + -- We need to have ScrollingEnabled = true for this frame for it to block + -- But we don't want it to actually scroll, so its canvas must be same size as the frame + ScrollingEnabled = true, + CanvasSize = UDim2.new(1, 0, 1, 0), + BackgroundTransparency = 1, + ScrollBarThickness = 0, + }, self.props[Roact.Children]), + }), + }) + end) +end + +local ShowOnTop = Roact.PureComponent:extend("UILibraryShowOnTop") +function ShowOnTop:render() + return withFocus(function(pluginGui) + local priority = self.props.Priority or 0 + return Roact.createElement(Roact.Portal, { + target = pluginGui, + }, { + TopLevelFrame = Roact.createElement("Frame", { + ZIndex = priority + FOCUSED_ZINDEX, + Size = UDim2.new(1, 0, 1, 0), + BackgroundTransparency = 1, + }, self.props[Roact.Children]), + }) + end) +end + +local KeyboardListener = Roact.PureComponent:extend("KeyboardListener") +function KeyboardListener:init() + self.keysHeld = {} + assert(self._context[focusKey] ~= nil, "No FocusProvider found.") + self.pluginGui = self._context[focusKey] + + self.onInputBegan = function(input) + if input.UserInputType == Enum.UserInputType.Keyboard then + self.keysHeld[input.KeyCode] = true + self.props.OnKeyPressed(input, self.keysHeld) + end + end + self.onInputEnded = function(input) + if input.UserInputType == Enum.UserInputType.Keyboard then + self.keysHeld[input.KeyCode] = nil + self.props.OnKeyReleased(input) + end + end + if self.pluginGui:IsA("DockWidgetPluginGui") then + self.focusConnection = self.pluginGui.WindowFocusReleased:Connect(function() + for key, _ in pairs(self.keysHeld) do + self.props.OnKeyReleased({ + KeyCode = key, + UserInputType = Enum.UserInputType.Keyboard, + }) + end + self.keysHeld = {} + end) + end +end +function KeyboardListener:render() + return Roact.createElement(ShowOnTop, {}, { + Listener = Roact.createElement("Frame", { + Size = UDim2.new(1, 0, 1, 0), + BackgroundTransparency = 1, + [Roact.Event.InputBegan] = function(_, input) + self.onInputBegan(input) + end, + [Roact.Event.InputEnded] = function(_, input) + self.onInputEnded(input) + end, + }), + }) +end +function KeyboardListener:willUnmount() + if self.focusConnection then + self.focusConnection:Disconnect() + end +end + +return { + Provider = FocusProvider, + Consumer = FocusConsumer, + CaptureFocus = CaptureFocus, + ShowOnTop = ShowOnTop, + KeyboardListener = KeyboardListener, + withFocus = withFocus, +} \ No newline at end of file diff --git a/BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Focus.spec.lua b/BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Focus.spec.lua new file mode 100644 index 0000000000..4be889f2e9 --- /dev/null +++ b/BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Focus.spec.lua @@ -0,0 +1,118 @@ +return function() + local Library = script.Parent + local Roact = require(Library.Parent.Parent.Roact) + + local MockWrapper = require(Library.MockWrapper) + + local Focus = require(script.Parent.Focus) + local ShowOnTop = Focus.ShowOnTop + local CaptureFocus = Focus.CaptureFocus + local KeyboardListener = Focus.KeyboardListener + + describe("ShowOnTop", function() + local function createTestShowOnTop(children, container) + return Roact.createElement(MockWrapper, { + Container = container, + }, { + ShowOnTop = Roact.createElement(ShowOnTop, {}, children) + }) + end + + it("should create and destroy without errors", function() + local element = createTestShowOnTop() + local instance = Roact.mount(element) + Roact.unmount(instance) + end) + + it("should render correctly", function() + local container = Instance.new("Folder") + local element = createTestShowOnTop({}, container) + local instance = Roact.mount(element) + + local gui = container:FindFirstChild("MockGui") + expect(gui).to.be.ok() + expect(gui.TopLevelFrame).to.be.ok() + Roact.unmount(instance) + end) + + it("should render its children", function() + local container = Instance.new("Folder") + local element = createTestShowOnTop({ + ChildFrame = Roact.createElement("Frame"), + }, container) + local instance = Roact.mount(element) + + local gui = container:FindFirstChild("MockGui") + expect(gui.TopLevelFrame.ChildFrame).to.be.ok() + Roact.unmount(instance) + end) + end) + + describe("CaptureFocus", function() + local function createTestCaptureFocus(children, container) + return Roact.createElement(MockWrapper, { + Container = container, + }, { + CaptureFocus = Roact.createElement(CaptureFocus, {}, children) + }) + end + + it("should create and destroy without errors", function() + local element = createTestCaptureFocus() + local instance = Roact.mount(element) + Roact.unmount(instance) + end) + + it("should render correctly", function() + local container = Instance.new("Folder") + local element = createTestCaptureFocus({}, container) + local instance = Roact.mount(element) + + local gui = container:FindFirstChild("MockGui") + expect(gui).to.be.ok() + expect(gui.TopLevelDetector).to.be.ok() + expect(gui.TopLevelDetector.ScrollBlocker).to.be.ok() + Roact.unmount(instance) + end) + + it("should render its children", function() + local container = Instance.new("Folder") + local element = createTestCaptureFocus({ + ChildFrame = Roact.createElement("Frame"), + }, container) + local instance = Roact.mount(element) + + local gui = container:FindFirstChild("MockGui") + expect(gui.TopLevelDetector.ScrollBlocker.ChildFrame).to.be.ok() + Roact.unmount(instance) + end) + end) + + describe("KeyboardListener", function() + local function createTestKeyboardListener(container) + return Roact.createElement(MockWrapper, { + Container = container, + }, { + KeyboardListener = Roact.createElement(KeyboardListener) + }) + end + + it("should create and destroy without errors", function() + local element = createTestKeyboardListener() + local instance = Roact.mount(element) + Roact.unmount(instance) + end) + + it("should render correctly", function() + local container = Instance.new("Folder") + local element = createTestKeyboardListener(container) + local instance = Roact.mount(element) + + local gui = container:FindFirstChild("MockGui") + expect(gui).to.be.ok() + expect(gui.TopLevelFrame).to.be.ok() + expect(gui.TopLevelFrame.Listener).to.be.ok() + Roact.unmount(instance) + end) + end) +end \ No newline at end of file diff --git a/BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Localizing.lua b/BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Localizing.lua new file mode 100644 index 0000000000..1c8f65af37 --- /dev/null +++ b/BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Localizing.lua @@ -0,0 +1,98 @@ +--[[ + A utility for elements that require text to be localized and display translated + strings. + + When initializing LocalizationProvider, it expects a Localization object, an example being + src/Studio/Localization.lua where there is two tables for development strings and translated + strings. withLocalization is mainly used to render elements with the localized strings using + the localization object passed into LocalizationProvider +]] + +local Library = script.Parent + +local Roact = require(Library.Parent.Parent.Roact) +local Symbol = require(Library.Utils.Symbol) + + +local localizationKey = Symbol.named("Localization") + +--[[ + Inserted near the top of the Roact tree, this stores the localization object into _context + + Props: + localization : (Localization) an object that can provide localized strings, preferrably a Localization object +]] +local LocalizationProvider = Roact.PureComponent:extend("LocalizationProvider") + +function LocalizationProvider:init() + assert(self.props.localization ~= nil, "LocalizationProvider expects a Localization object") + local localization = self.props.localization + + self._context[localizationKey] = localization +end + +function LocalizationProvider:render() + return Roact.oneChild(self.props[Roact.Children]) +end + + +--[[ + Never explicitly created by the user, it exposes the localization object from context. + This should only ever be created by a call to withLocalization(). + + Props: + localizedRender : (function(Localization localization)) + a callback used to render children while exposing the Localization object stored in _context +]] +local LocalizationConsumer = Roact.PureComponent:extend("LocalizationConsumer") + +function LocalizationConsumer:init() + assert(type(self.props.localizedRender) == "function", "LocalizationConsumer expected to be created with withLocale()") + assert(self._context[localizationKey] ~= nil, "LocalizationConsumer expects a LocalizationProvider in the Roact tree") + + self.localization = self._context[localizationKey] + self.state = { + -- keep a simple string of the table reference so we have something to call setstate on later + localization = tostring(self.localization.translator) + } + + self.lcToken = self.localization.localeChanged:connect(function(newLocale) + -- force trigger a re-render of children + self:setState({ + localization = tostring(newLocale) + }) + end) +end + +function LocalizationConsumer:render() + return self.props.localizedRender(self.localization) +end + +function LocalizationConsumer:willUnmount() + if self.lcToken then + self.lcToken:disconnect() + self.lcToken = nil + end +end + +--[[ + callback : function(Localization localization) + a callback used to render children while exposing the localization stored in _context +]] +local function withLocalization(callback) + assert(type(callback) == "function", "withLocalization expects a function") + return Roact.createElement(LocalizationConsumer, { + localizedRender = callback + }) +end + +local function getLocalization(component) + return component._context[localizationKey] +end + +return { + Provider = LocalizationProvider, + Consumer = LocalizationConsumer, + withLocalization = withLocalization, + getLocalization = getLocalization, +} diff --git a/BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Localizing.spec.lua b/BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Localizing.spec.lua new file mode 100644 index 0000000000..3840e0ef41 --- /dev/null +++ b/BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Localizing.spec.lua @@ -0,0 +1,157 @@ +local Localizing = require(script.Parent.Localizing) + +local Library = script.Parent +local Roact = require(Library.Parent.Parent.Roact) +local Signal = require(Library.Utils.Signal) +local Localization = require(Library.Studio.Localization) + +local LocalizationProvider = Localizing.Provider +local LocalizationConsumer = Localizing.Consumer +local withLocalization = Localizing.withLocalization + +return function() + describe("LocalizationProvider", function() + it("should construct/deconstruct without a problem", function() + local localization = Localization.mock() + + local root = Roact.createElement(LocalizationProvider, { + localization = localization + }) + local handle = Roact.mount(root) + Roact.unmount(handle) + + localization:destroy() + end) + + it("should error if a localization object isn't provided", function() + expect(function() + local root = Roact.createElement(LocalizationProvider, {}) + local instance = Roact.mount(root) + Roact.unmount(instance) + end).to.throw() + end) + end) + + + describe("LocalizationConsumer", function() + it("should construct/deconstruct without a problem when used appropriately", function() + local mockLocalization = Localization.mock() + + local function createTestElement() + return withLocalization(function(localization) + return Roact.createElement("TextLabel", { + Text = localization:getText("Anything", "test") + }) + end) + end + + local root = Roact.createElement(LocalizationProvider, { + localization = mockLocalization + },{ + Roact.createElement(createTestElement, {}) + }) + + local handle = Roact.mount(root) + Roact.unmount(handle) + + mockLocalization:destroy() + end) + + it("should error if constructed without a LocalizationProvider in the Roact tree", function() + local function createTestElement() + return withLocalization(function(localization) + return Roact.createElement("TextLabel", { + Text = localization:getText("Anything", "test") + }) + end) + end + + expect(function() + local root = Roact.createElement(createTestElement) + local instance = Roact.mount(root) + Roact.unmount(instance) + end).to.throw() + end) + + it("should error if you construct it on its own", function() + expect(function() + local root = Roact.createElement(LocalizationConsumer, {}) + local instance = Roact.mount(root) + Roact.unmount(instance) + end).to.throw() + end) + + it("should re-render its contents if the localization changes", function() + local changeSignal = Signal.new() + local localization = Localization.mock(changeSignal) + + -- create a test element and keep track of how many times it renders + local renderCount = 0 + + local TestElement = Roact.PureComponent:extend("TestElement") + function TestElement:render() + renderCount = renderCount + 1 + local text = self.props.text + + return Roact.createElement("TextLabel", { + Text = text + }) + end + + -- create the roact tree + local root = Roact.createElement(LocalizationProvider, { + localization = localization + },{ + Roact.createElement(function() + return withLocalization(function(localizationObject) + return Roact.createElement(TestElement, { + text = localizationObject:getText("Test", "hello_world") + }) + end) + end) + }) + + local instance = Roact.mount(root) + expect(renderCount).to.equal(1) + + -- trigger a locale change + changeSignal:fire() + expect(renderCount).to.equal(2) + + -- clean up + Roact.unmount(instance) + localization:destroy() + end) + end) + + + describe("withLocalization()", function() + it("should error if a render callback isn't provided", function() + expect(function() + withLocalization() + end).to.throw() + end) + + it("should expose the stored localization object", function() + local mockLocalization = Localization.mock() + local foundLocalization = nil + + local function localizedRender(localization) + foundLocalization = localization + return Roact.createElement("TextLabel") + end + + local root = Roact.createElement(LocalizationProvider, { + localization = mockLocalization + },{ + Roact.createElement(function() + return withLocalization(localizedRender) + end) + }) + local instance = Roact.mount(root) + Roact.unmount(instance) + + expect(foundLocalization).to.equal(mockLocalization) + end) + end) +end \ No newline at end of file diff --git a/BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/MockWrapper.lua b/BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/MockWrapper.lua new file mode 100644 index 0000000000..1dc9b74743 --- /dev/null +++ b/BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/MockWrapper.lua @@ -0,0 +1,44 @@ +--[[ + USE IN TESTS ONLY + Provides mocks of all necessary context items for testing. +]] + +local Library = script.Parent +local Roact = require(Library.Parent.Parent.Roact) + +local UILibraryWrapper = require(Library.UILibraryWrapper) + +local createTheme = require(Library.createTheme) +local dummyTheme = createTheme() + +local MockWrapper = Roact.PureComponent:extend("MockWrapper") + +local function createMockPlugin(container) + return { + CreateQWidgetPluginGui = function() + return Instance.new("BillboardGui", container) + end + } +end + +function MockWrapper:init(props) + local container = props.Container + + self.mockGui = Instance.new("ScreenGui", container) + self.mockGui.Name = "MockGui" + self.mockPlugin = createMockPlugin(container) +end + +function MockWrapper:render() + return Roact.createElement(UILibraryWrapper, { + theme = dummyTheme, + focusGui = self.mockGui, + plugin = self.mockPlugin, + }, self.props[Roact.Children]) +end + +function MockWrapper:willUnmount() + self.mockGui:Destroy() +end + +return MockWrapper \ No newline at end of file diff --git a/BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Plugin.lua b/BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Plugin.lua new file mode 100644 index 0000000000..1eb3a1fec5 --- /dev/null +++ b/BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Plugin.lua @@ -0,0 +1,28 @@ +local Library = script.Parent + +local Roact = require(Library.Parent.Parent.Roact) +local Symbol = require(Library.Utils.Symbol) +local pluginKey = Symbol.named("Plugin") + +local PluginProvider = Roact.PureComponent:extend("PluginProvider") +function PluginProvider:init() + local plugin = self.props.plugin + assert(plugin ~= nil, "No plugin was given to this PluginProvider.") + + self._context[pluginKey] = plugin +end +function PluginProvider:render() + return Roact.oneChild(self.props[Roact.Children]) +end + +-- Gets the plugin at the passed in component's context. +local function getPlugin(component) + assert(component._context[pluginKey] ~= nil, "No PluginProvider found.") + local plugin = component._context[pluginKey] + return plugin +end + +return { + Provider = PluginProvider, + getPlugin = getPlugin, +} \ No newline at end of file diff --git a/BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Studio/Analytics.lua b/BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Studio/Analytics.lua new file mode 100644 index 0000000000..e08e32d7dc --- /dev/null +++ b/BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Studio/Analytics.lua @@ -0,0 +1,123 @@ +--[[ + Providable helper for sending analytics events and reporting counters. + Consumers can use this utility as a wrapper for AnalyticsService, + allowing mock Analytics objects to be made for testing without sending + actual analytics calls. + + function Analytics.new(props) + Creates a new Analytics object with the given props. + Props: + string Target = The target namespace to send EventStream events to. + For Studio plugins, this is usually "studio". If no Target is + provided, this defaults to "studio". + string Context = The context of the namespace to send EventStream + events to. This will usually match or reference the name of the plugin. + bool LogEvents = Whether to log Analytics events to the console. + + function Analytics.mock() + Creates a mock Analytics object which will do nothing, but provides a + proxy for every function so that it can be safely used for testing. + + function Analytics:sendEvent(string eventName, table additionalArgs) + Sends an EventStream event. The additionalArgs table can be used to pass + more info along with the event itself. + + function Analytics:reportCounter(string counterName, number num = 1) + Reports number num to the RCity bucket named counterName. Used for + iterating ephemeral counters. +]] + +local RbxAnalyticsService = game:GetService("RbxAnalyticsService") +local HttpService = game:GetService("HttpService") +local StudioService = game:GetService("StudioService") + +local Library = script.Parent.Parent +local join = require(Library.join) + +local Analytics = {} +Analytics.__index = Analytics + +function Analytics.new(props) + assert(type(props) == "table", "Analytics props is expected to be a table.") + assert(props.Context, "Analytics expected a context string.") + + local self = { + senders = props.Senders or RbxAnalyticsService, + logEvents = props.LogEvents, + + target = props.Target or "studio", + context = props.Context, + + placeId = game.PlaceId, + userId = StudioService:GetUserId(), + } + setmetatable(self, Analytics) + + self.sessionId = self.senders:GetSessionId() + self.clientId = self.senders:GetClientId() + + return self +end + +-- EventStream events handler +function Analytics:sendEventDeferred(eventName, additionalArgs) + self:logEvent(eventName, additionalArgs) + local args = join(additionalArgs, { + studioSid = self.sessionId, + clientId = self.clientId, + placeId = self.placeId, + userId = self.userId, + }) + self.senders:SendEventDeferred(self.target, self.context, eventName, args) +end + +-- RCity Ephemeral Counters handler +function Analytics:reportCounter(counterName, num) + self:logCounter(counterName, num) + self.senders:ReportCounter(counterName, num or 1) +end + +function Analytics:reportStats(statName, num) + self:logStats(statName, num) + self.senders:ReportStats(statName, num) +end + +function Analytics:logEvent(eventName, tab) + if self.logEvents then + local readableTable = HttpService:JSONEncode(tab) + print(string.format("Analytics: sendEventDeferred: \"%s\", %s", eventName, readableTable)) + end +end + +function Analytics:logCounter(counterName, value) + if self.logEvents then + print(string.format("Analytics: reportCounter: \"%s\", %s", counterName, value)) + end +end + +function Analytics:logStats(statName, value) + if self.logEvents then + print(string.format("Analytics: reportStats: \"%s\", %s", statName, value)) + end +end + +function Analytics.mock(props) + return Analytics.new(join(props, { + Senders = { + SendEventDeferred = function() + end, + ReportCounter = function() + end, + ReportStats = function() + end, + GetSessionId = function() + return 0 + end, + GetClientId = function() + return 0 + end, + } + })) +end + +return Analytics \ No newline at end of file diff --git a/BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Studio/ContextMenus.lua b/BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Studio/ContextMenus.lua new file mode 100644 index 0000000000..a49b560b3c --- /dev/null +++ b/BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Studio/ContextMenus.lua @@ -0,0 +1,42 @@ +--[[ + A Roact wrapper for the PluginMenu API. + + Props: + table Actions = The set of actions to send to MakePluginMenu. + function OnMenuOpened() = A callback for when the context menu has successfully opened. +]] + +local Library = script.Parent.Parent +local Roact = require(Library.Parent.Parent.Roact) + +local PluginContext = require(Library.Plugin) +local getPlugin = PluginContext.getPlugin +local PluginMenus = require(Library.Studio.PluginMenus) + +local ContextMenu = Roact.PureComponent:extend("ContextMenu") + +function ContextMenu:showMenu() + local props = self.props + local actions = props.Actions + local plugin = getPlugin(self) + + props.OnMenuOpened() + PluginMenus.makePluginMenu(plugin, actions) +end + +function ContextMenu:didMount() + self:showMenu() +end + +function ContextMenu:didUpdate() + self:showMenu() +end + +function ContextMenu:render() + return nil +end + +return { + ContextMenu = ContextMenu, + Separator = PluginMenus.Separator, +} \ No newline at end of file diff --git a/BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Studio/Hyperlink.lua b/BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Studio/Hyperlink.lua new file mode 100644 index 0000000000..f7a64aa58c --- /dev/null +++ b/BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Studio/Hyperlink.lua @@ -0,0 +1,60 @@ +--[[ + A widget that contains a hyperlink. + + Props: + bool Enabled = Whether this widget should be interactable. + string Text = The hyperlink text + int TextSize = The size of the text + int LayoutOrder = The order in which this element is displayed if in a UIListLayout. + function OnClick = what happens when the hyperlink is clicked + Mouse = plugin mouse for changing the mouse icon +]] + +local Library = script.Parent.Parent +local Roact = require(Library.Parent.Parent.Roact) + +local Mouse = require(script.Parent.Internal.Mouse) + +local Theming = require(Library.Theming) +local withTheme = Theming.withTheme + +local Hyperlink = Roact.PureComponent:extend("hyperlink") + +local function calculateTextSize(text, textSize, font) + local hugeFrameSizeNoTextWrapping = Vector2.new(5000, 5000) + local size = game:GetService("TextService"):GetTextSize(text, textSize, font, hugeFrameSizeNoTextWrapping) + return UDim2.new(0, size.X, 0, size.Y) +end + +function Hyperlink:render() + return withTheme(function(theme) + if self.props.Enabled == nil then + self.props.Enabled = true + end + + local textSize = self.props.TextSize or 22 + + return Roact.createElement("TextButton", { + BackgroundTransparency = 1, + Text = self.props.Text, + TextSize = textSize, + Font = Enum.Font.SourceSans, + TextColor3 = theme.hyperlink.textColor, + Size = self.props.Size or calculateTextSize(self.props.Text, textSize, Enum.Font.SourceSans), + Position = self.props.Position, + TextXAlignment = Enum.TextXAlignment.Left, + LayoutOrder = self.props.LayoutOrder, + + [Roact.Event.MouseEnter] = function() if self.props.Enabled then Mouse.onEnter(self.props.Mouse) end end, + [Roact.Event.MouseLeave] = function() if self.props.Enabled then Mouse.onLeave(self.props.Mouse) end end, + + [Roact.Event.Activated] = function() + if self.props.Enabled and nil ~= self.props.OnClick then + self.props.OnClick() + end + end, + }) + end) +end + +return Hyperlink \ No newline at end of file diff --git a/BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Studio/Internal/Mouse.lua b/BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Studio/Internal/Mouse.lua new file mode 100644 index 0000000000..8db89bd316 --- /dev/null +++ b/BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Studio/Internal/Mouse.lua @@ -0,0 +1,19 @@ +--[[ + Mouse helper functions +]] + +local Mouse = {} + +function Mouse.onEnter(pluginMouse, iconName) + if pluginMouse then + pluginMouse.Icon = iconName and "rbxasset://SystemCursors/" .. iconName or "rbxasset://SystemCursors/PointingHand" + end +end + +function Mouse.onLeave(pluginMouse) + if pluginMouse then + pluginMouse.Icon = "rbxasset://SystemCursors/Arrow" + end +end + +return Mouse \ No newline at end of file diff --git a/BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Studio/Localization.lua b/BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Studio/Localization.lua new file mode 100644 index 0000000000..bec44c12bb --- /dev/null +++ b/BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Studio/Localization.lua @@ -0,0 +1,284 @@ +--[[ + Reads data out of the localization table, provides a simple interface for fetching strings + + Props : + stringResourceTable : (CSV localization file) the file with the English strings, used for development + translationResourceTable : (CSV localization file) the file with all of the translated strings + pluginName : (string) the "plugin_name" field used in the localization file's keys + + Optional Props : + namespace : (string) the namespace of all keys in the localization, defaults to "Studio" + overrideGetLocale : (function(void)) a function that returns a localeId + overrideLocaleId : (string) a locale used to ignore the current locale set by Roblox + overrideLocaleChangedSignal : (Signal) a signal that the user has changed to a different language + overrideTranslator : (function<>()) + + - NOTE - + To make the localization resource files backend friendly, the keys should be structured like this: + ... + + For formatted strings, follow this guide online : https://developer.roblox.com/articles/localization-format-strings + + For example, your DevelopmentReferenceTable.csv should look something like this : + Key,Context,Example,Source,en + Studio.MoneyManager.Currency.Robux,the name displayed for Robux,,,R$ + Studio.MoneyManager.Currency.USD,the name displayed for US dollars,,,USD + Studio.MoneyManager.Sell.LimitedsTitle,the page title for selling limited items,,,Sell your limiteds + Studio.MoneyManager.Sell.LimitedValue,shows how much an item is worth,,,{1:translate} is worth {2:fixed} {3:translate} + + And your TranslationReferenceTable.csv should look something like this : (line breaks added for readability) + Key,Context,Example,Source,de,es,es-es,ja,ko + Studio.MoneyManager.Sell.LimitedValue,{1:translate} ist fünf {2:fixed} {3:translate}, + {1:translate} vale {2:fixed} {3:translate}, + {1:translate} vale {2:fixed} {3:translate}, + {1:translate}は{2:fixed}{3:translate}の価値がある, + {1:translate}은{2:fixed}{3:translate}가치가있다. + + (it is okay for keys to be missing in this file. This file can be empty and that's fine) + + + Localization Usage : + local rsTable = script.Parent.DevelopmentReferenceTable + local trsTable = script.Parent.TranslationReferenceTable + local pluginLocalization = Localization.new({ + stringResourceTable = rsTable, + translationResourceTable = trsTable, + pluginName = "MoneyManager" + }) + local example = pluginLocalization:getText("Sell", "LimitedsValue", "Valkyrie Helm", 71850, "R$") +]] + +game:DefineFastFlag("FixStudioLocalizationLocaleId", false) + +-- services +local LocalizationService = game:GetService("LocalizationService") +local StudioService = game:GetService("StudioService") + +-- libraries +local Library = script.Parent.Parent +local Signal = require(Library.Utils.Signal) + +-- constants +local FALLBACK_LOCALE = "en-us" + + +local Localization = {} +Localization.__index = Localization + +function Localization.new(props) + assert(type(props) == "table", "Localization props is expected to be a table.") + assert(props.stringResourceTable ~= nil, "Localization must have a .csv string resource table for English strings") + assert(props.translationResourceTable ~= nil, "Localization must have a .csv string resource table of translations") + assert(type(props.pluginName) == "string", "Please specify the plugin's name") + + local stringResourceTable = props.stringResourceTable + local translationResourceTable = props.translationResourceTable + local overrideGetLocale = props.getLocale + local overrideLocaleId = props.overrideLocaleId + local overrideLocaleChangedSignal = props.overrideLocaleChangedSignal + local keyNamespace = props.namespace + local keyPluginName = props.pluginName + + if keyNamespace == nil then + keyNamespace = "Studio" + end + + local externalLocaleChanged + if overrideLocaleChangedSignal then + externalLocaleChanged = overrideLocaleChangedSignal + elseif game:GetFastFlag("FixStudioLocalizationLocaleId") then + externalLocaleChanged = StudioService:GetPropertyChangedSignal("StudioLocaleId") + else + externalLocaleChanged = LocalizationService:GetPropertyChangedSignal("RobloxLocaleId") + end + + -- a function that gets called when the locale changes, returns the new locale + local function getLocale() + if overrideGetLocale then + return overrideGetLocale() + end + + if overrideLocaleId ~= nil then + return overrideLocaleId + elseif game:GetFastFlag("FixStudioLocalizationLocaleId") then + return StudioService["StudioLocaleId"] + else + return LocalizationService["RobloxLocaleId"] + end + end + + local self = { + -- localeChanged : (Signal) + -- a public facing signal for Localization consumers to observe updates + localeChanged = Signal.new(), + + -- externalLocaleChanged : (Signal) + -- the system signal fired when a user changes their language settings + externalLocaleChanged = externalLocaleChanged, + + -- externalLocaleChangedConnection : (connection token) + -- a subscription token for cleaning up the connection + externalLocaleChangedConnection = nil, + + -- locale : (string) + -- an id for knowing which translation to read from. ex) "en-us" + locale = FALLBACK_LOCALE, + + -- keyNamespace : (string) + -- the first field used to construct a key + keyNamespace = keyNamespace, + + -- keyPluginName : (string) + -- the second field used to construct a key + keyPluginName = keyPluginName, + + -- getLocale : (function()) + -- gets the current locale string + getLocale = getLocale, + + -- stringResourceTable : a CSV file containing all of the English strings + -- this is converted into a proper resource by Rojo + stringResourceTable = stringResourceTable, + + -- translationResourceTable : a CSV file containing all of the translated strings + -- this is converted into a proper resource by Rojo + translationResourceTable = translationResourceTable, + + -- translator & fallbackTranslator : (Translator) + -- objects that handle the string formatting from the current stringResourceTable + translator = nil, + fallbackTranslator = nil + } + setmetatable(self, Localization) + + -- listen to changes to the locale to alert all listeners of the change + self.localeChangedConnection = self.externalLocaleChanged:connect(function() + self:updateLocaleAndTranslator() + self.localeChanged:fire(self.locale) + end) + + -- create the translators for the first time + self:updateLocaleAndTranslator() + + return self +end + +-- scope : (string) the general group of data that the key belongs to +-- key : (string) the id of the string in the resource table +-- ... : (optional, Variant) values used to format a string +function Localization:getText(scope, key, ...) + assert(type(scope) == "string", "Cannot fetch the string without a scope") + assert(type(key) == "string", "Cannot fetch a string without the key") + + local stringKey = string.format("%s.%s.%s.%s", self.keyNamespace, self.keyPluginName, scope, key) + local args = {...} + + local function getTranslation(translator) + if not translator then + return false, nil + end + + local success, result = pcall(function() + return translator:FormatByKey(stringKey, args) + end) + return success, result + end + + -- optimize for one lookup when the locale is English + local success + local translated + if self.locale == FALLBACK_LOCALE then + -- English strings are only written into the development string table, + -- so don't bother looking up the key in the localization table. + success, translated = getTranslation(self.fallbackTranslator) + if success then + return translated + end + + else + -- try to find a translation in our translation file + success, translated = getTranslation(self.translator) + if success then + return translated + end + + -- If no translation exists for this locale id, fall back to default (English) + success, translated = getTranslation(self.fallbackTranslator) + if success then + return translated + end + end + + -- Fall back to the given key if there is no translation for this value + -- Useful for finding misspelled or missing keys + return stringKey +end + +function Localization:destroy() + if self.localeChangedConnection then + self.localeChangedConnection:disconnect() + end +end + +function Localization:updateLocaleAndTranslator() + -- the locale has changed, update the translators + self.locale = self.getLocale() + self.translator = self.translationResourceTable:GetTranslator(self.locale) + self.fallbackTranslator = self.stringResourceTable:GetTranslator(FALLBACK_LOCALE) +end + +-- changeSignal : (Signal, optional) a signal to trigger localization changes +function Localization.mock(localizationChangedSignal) + local changeSignal + if localizationChangedSignal then + changeSignal = localizationChangedSignal + else + changeSignal = Signal.new() + end + + -- any time the localizationChangedSignal fires, this will get the next one + -- this should trigger re-renders for any elements + local currentLocale = 0 + local localeIDs = {"en-us", "es", "es-es", "ko", "ja"} + local function getLocale() + currentLocale = (currentLocale + 1) % 5 + local nextLocale = localeIDs[currentLocale] + return nextLocale + end + + local fakeResourceTable = { + GetTranslator = function(stringResourceTableSelf, localeId) + local translator = { + FormatByKey = function(translatorSelf, key, args) + if not args then + args = {} + elseif type(args) ~= "table" then + error("Args must be a table") + end + + -- return a string like en-us|TEST.MOCK_LOCALIZATION.A.hello_world:[a,b,c,10] + return string.format("%s|%s:[%s]", localeId,key, table.concat(args, ",")) + end, + } + + return translator + end + } + + -- create a fake localization object for tests + return Localization.new({ + -- create a fake resource file that mimics the real thing + stringResourceTable = fakeResourceTable, + translationResourceTable = fakeResourceTable, + + namespace = "TEST", + pluginName = "MOCK_LOCALIZATION", + + -- for tests, don't connect to any system signals to ensure stuff doesn't change mid test + overrideLocaleChangedSignal = changeSignal, + getLocale = getLocale, + }) +end + + +return Localization \ No newline at end of file diff --git a/BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Studio/Localization.spec.lua b/BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Studio/Localization.spec.lua new file mode 100644 index 0000000000..ede3c1b091 --- /dev/null +++ b/BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Studio/Localization.spec.lua @@ -0,0 +1,228 @@ +local Localization = require(script.Parent.Localization) + +local Library = script.Parent.Parent +local Signal = require(Library.Utils.Signal) + +local TestLocalizationChangedSignal = Signal.new() +local TestDevStrings = Library.Studio.TestDevStrings +local TestTranslationStrings = Library.Studio.TestTranslationStrings + + + +return function() + -- since Localization connects to system signals, it's important to clean up after the test + describe("Localization", function() + it("should construct with the correct inputs", function() + local localization = Localization.new({ + stringResourceTable = TestDevStrings, + translationResourceTable = TestTranslationStrings, + pluginName = "UILibrary", + }) + expect(localization).to.be.ok() + + localization:destroy() + end) + + it("should error if it is missing any props", function() + expect(function() + Localization.new() + end).to.throw() + + expect(function() + Localization.new({}) + end).to.throw() + + expect(function() + Localization.new({ stringResourceTable = TestDevStrings }) + end).to.throw() + + expect(function() + Localization.new({ translationResourceTable = TestTranslationStrings }) + end).to.throw() + + expect(function() + Localization.new({ pluginName = "UILibrary" }) + end).to.throw() + end) + + it("should return localized strings when given keys to look up", function() + local localization = Localization.new({ + stringResourceTable = TestDevStrings, + translationResourceTable = TestTranslationStrings, + pluginName = "UILibrary", + overrideLocaleId = "en-us", + overrideLocaleChangedSignal = TestLocalizationChangedSignal + }) + + local greeting = localization:getText("Spec", "greeting_formal") + expect(greeting).to.equal("Hello") + + localization:destroy() + end) + + it("should return a formatted string when args are provided", function() + local localization = Localization.new({ + stringResourceTable = TestDevStrings, + translationResourceTable = TestTranslationStrings, + pluginName = "UILibrary", + overrideLocaleId = "en-us", + overrideLocaleChangedSignal = TestLocalizationChangedSignal + }) + + local greeting = localization:getText("Spec", "greeting_informal", "Builderman", 2) + expect(greeting).to.equal("Sup Builderman, I haven't seen you in 2 days") + + localization:destroy() + end) + + it("should return the English text of a string if a translation is missing in the resourceTable", function() + local localization = Localization.new({ + stringResourceTable = TestDevStrings, + translationResourceTable = TestTranslationStrings, + pluginName = "UILibrary", + overrideLocaleId = "es-es", + overrideLocaleChangedSignal = TestLocalizationChangedSignal + }) + + local normal = localization:getText("Spec", "greeting_formal") + expect(normal).to.equal("Hola") + + local informal = localization:getText("Spec", "greeting_informal", "John Doe", 100) + expect(informal).to.equal("¿Qué pasa John Doe? No te he visto en 100 días") + + local surprise = localization:getText("Spec", "greeting_surprise") + expect(surprise).to.equal("No one expects the Spanish Inquisition!") + + localization:destroy() + end) + + it("should return the key if the string does not exist in the resourceTable at all", function() + local localization = Localization.new({ + stringResourceTable = TestDevStrings, + translationResourceTable = TestTranslationStrings, + pluginName = "UILibrary", + overrideLocaleId = "en-us", + overrideLocaleChangedSignal = TestLocalizationChangedSignal + }) + + local greeting = localization:getText("Spec", "greeting_serious") + expect(greeting).to.equal("Studio.UILibrary.Spec.greeting_serious") + + localization:destroy() + end) + + it("should update its strings if the localization changes", function() + local changeSignal = Signal.new() + local currentLocale = "en-us" + local function getLocale() + return currentLocale + end + + local localization = Localization.new({ + stringResourceTable = TestDevStrings, + translationResourceTable = TestTranslationStrings, + pluginName = "UILibrary", + getLocale = getLocale, + overrideLocaleChangedSignal = changeSignal + }) + + local greeting = localization:getText("Spec", "greeting_formal") + expect(greeting).to.equal("Hello") + + -- trigger a locale change + currentLocale = "es-es" + changeSignal:fire() + + greeting = localization:getText("Spec", "greeting_formal") + expect(greeting).to.equal("Hola") + + + localization:destroy() + end) + + it("should remove the observer to the localization changed signal when it is destroyed", function() + local changeSignal = Signal.new() + local currentLocale = "en-us" + local callCount = 0 + local function getLocale() + callCount = callCount + 1 + return currentLocale + end + + local localization = Localization.new({ + stringResourceTable = TestDevStrings, + translationResourceTable = TestTranslationStrings, + pluginName = "UILibrary", + getLocale = getLocale, + overrideLocaleChangedSignal = changeSignal + }) + + expect(callCount).to.equal(1) + + local greeting = localization:getText("Spec", "greeting_formal") + expect(greeting).to.equal("Hello") + + -- trigger a locale change + currentLocale = "es-es" + changeSignal:fire() + + greeting = localization:getText("Spec", "greeting_formal") + expect(greeting).to.equal("Hola") + expect(callCount).to.equal(2) + + -- destroy the connection and trigger another change + localization:destroy() + currentLocale = "en-us" + changeSignal:fire() + + greeting = localization:getText("Spec", "greeting_formal") + expect(greeting).to.equal("Hola") + expect(callCount).to.equal(2) + end) + + it("should fallback to the base language if it is available when a specific locale isn't supported", function() + local localization = Localization.new({ + stringResourceTable = TestDevStrings, + translationResourceTable = TestTranslationStrings, + pluginName = "UILibrary", + overrideLocaleId = "es-mx", + }) + + local greeting = localization:getText("Spec", "greeting_formal") + expect(greeting).to.equal("Hola") + end) + end) + + + describe("Localization.mock()", function() + it("should always return a string, even without the actual resourceTable", function() + local mock = Localization.mock() + + -- expect it to return someting, if not the real string + -- something like : |...:[list of args] + local strA = mock:getText("Anything", "greeting_formal") + expect(strA).to.equal("en-us|TEST.MOCK_LOCALIZATION.Anything.greeting_formal:[]") + + local strB = mock:getText("Anything", "greeting_informal", "Jane Doe", 1) + expect(strB).to.equal("en-us|TEST.MOCK_LOCALIZATION.Anything.greeting_informal:[Jane Doe,1]") + + mock:destroy() + end) + + it("should allow for an external signal to fake locale changes", function() + local testSignal = Signal.new() + local mockLocalization = Localization.mock(testSignal) + + local callCount = 0 + local mockToken = mockLocalization.localeChanged:connect(function() + callCount = callCount + 1 + end) + + testSignal:fire() + expect(callCount).to.equal(1) + + mockToken:disconnect() + mockLocalization:destroy() + end) + end) +end \ No newline at end of file diff --git a/BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Studio/PartialHyperLink.lua b/BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Studio/PartialHyperLink.lua new file mode 100644 index 0000000000..ef8cf9b89b --- /dev/null +++ b/BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Studio/PartialHyperLink.lua @@ -0,0 +1,39 @@ +local Library = script.Parent.Parent +local Roact = require(Library.Parent.Parent.Roact) + +local StudioWidgetHyperlink = require(script.Parent.Hyperlink) + +local PartialHyperlink = Roact.PureComponent:extend("PartialHyperlink") + +local function calculateTextSize(text, textSize, font) + local hugeFrameSizeNoTextWrapping = Vector2.new(5000, 5000) + return game:GetService('TextService'):GetTextSize(text, textSize, font, hugeFrameSizeNoTextWrapping) +end + + +function PartialHyperlink:render() + local hyperLinkTextSize = calculateTextSize(self.props.HyperLinkText, self.props.Theme.fontStyle.Normal.TextSize, self.props.Theme.fontStyle.Normal.Font) + return Roact.createElement("Frame", { + Size = UDim2.new(1, 0, 0, hyperLinkTextSize.Y), + BackgroundTransparency = 1, + }, { + HyperLink = Roact.createElement(StudioWidgetHyperlink, { + Text = self.props.HyperLinkText, + Size = UDim2.new(0, hyperLinkTextSize.X, 0, hyperLinkTextSize.Y), + Mouse = self.props.Mouse, + OnClick = self.props.OnClick, + }), + TextLabel = Roact.createElement("TextLabel", { + BackgroundTransparency = 1, + Position = UDim2.new(0, hyperLinkTextSize.X, 0, 0), + Size = UDim2.new(1, -hyperLinkTextSize.X, 1, 0), + TextColor3 = self.props.Theme.fontStyle.Normal.TextColor3, + Font = Enum.Font.SourceSans, + TextSize = 22, + TextXAlignment = Enum.TextXAlignment.Left, + Text = self.props.NonHyperLinkText, + }), + }) +end + +return PartialHyperlink \ No newline at end of file diff --git a/BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Studio/PluginMenus.lua b/BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Studio/PluginMenus.lua new file mode 100644 index 0000000000..ee9caf58b9 --- /dev/null +++ b/BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Studio/PluginMenus.lua @@ -0,0 +1,131 @@ +--[[ + Returns a Separator constant and makePluginMenu. makePluginMenu creates a PluginMenu made up of + PluginActions, or SubMenus of PluginActions. + + Parameters: + plugin = The Roblox plugin instance that this menu belongs to + entries = a list of actions to be displayed in the menu. Actions can be a PluginAction instance + created externally, the SEPARATOR constant, or a dictionary containing action information that + will be used to create a new action. Action dictionaries have the following fields: + + Text = the action text displayed in the menu + ItemSelected = a callback that is invoked whenever the action is clicked + [Key] = an optional value provided as an argument to the ItemSelected callback. Can be nil if you + aren't sharing ItemSelected callbacks between actions + [Icon] = an optional icon displayed next to the action in the menu + [Checked] optionally show a check mark next to the action in the menu. Ignored if the action is in + a selection submenu (see below) + [Enabled] optionally control whether an action is selectable or not. Defaults to true + + If you insert one or more actions into the dictionary, the dictionary it was inserted into will + become a submenu. Submenus have the same properties as actions since the submenu is an action, + but ItemSelected will never be invoked. + + There is also a special kind of selection submenu that will automatically display a checkmark next + to a selected action. If you include a CurrentKey field in the submenu, any action whose Key field + equals CurrentKey will have a checkmark displayed. It is the responsibility of the consumer to use + the actions' ItemSelected callback to update the CurrentKey field of the submenu. + + Examples: + + Explorer context menu: + { + -- Ordinary actions + { Text = "Cut", Icon = "rbxasset://textures/cutIcon.png", ItemSelected = function() cutSelection() end }, + { Text = "Copy", Icon = "rbxasset://textures/copyIcon.png", ItemSelected = function() copySelection() end }, + { Text = "Paste Into", Enabled = false, ItemSelected = function() pasteIntoSelection() end }, + ... + PluginMenus.Separator, + ... + -- A submenu action with two inner items (the inner items can also be submenus) + { + Text = "Insert Object", Icon = "insertObjects.png", Enabled = true, + { Text = "Part", Key = partKey, Icon = "part.png", ItemSelected = function(key) insertObject(key) end }, + { Text = "Wedge", Key = wedgeKey, Icon = "wedge.png", ItemSelected = function(key) insertObject(key) end }, + ... + }, + } + + Tools Context Menu + { + { Text = "Collisions Enabled", Checked = true, ItemSelected = function(text) toggleCollisions() end }, + { Text = "Constraints Enabled", Checked = false, ItemSelected = function(text) toggleConstraints() end }, + { + CurrentKey = alwaysKey, Text = "Join Mode", Icon = "rbxasset://textures/joinMode.png", + { Text = "Always", Key = alwaysKey, ItemSelected = function(key) joinModeSelected(key) end }, + { Text = "None", Key = noneKey, ItemSelected = function(key) joinModeSelected(key) end }, + } + } +]] + +-- Importing HttpService only for GenerateGUID +local HttpService = game:GetService("HttpService") + +local Library = script.Parent.Parent +local Symbol = require(Library.Utils.Symbol) + +local SEPARATOR = Symbol.named("(PluginMenuSeparator)") + +local function newId() + return HttpService:GenerateGUID() +end + +local function connectAction(connections, action, entry, item) + table.insert(connections, action.Triggered:Connect(function() + for _, connection in ipairs(connections) do + connection:Disconnect() + end + entry.ItemSelected(item) + end)) +end + +local function createPluginMenu(plugin, entries, subMenus, connections) + local menu = plugin:CreatePluginMenu(newId(), entries.Text, entries.Icon) + + for _, entry in ipairs(entries) do + if entry == SEPARATOR then + menu:AddSeparator() + elseif typeof(entry) == "Instance" and entry:IsA("PluginAction") then + menu:AddAction(entry) + elseif typeof(entry) == "table" then + if #entry > 0 then + local subMenu = createPluginMenu(plugin, entry, subMenus, connections) + table.insert(subMenus, subMenu) + menu:AddMenu(subMenu) + else + local action = menu:AddNewAction(newId(), entry.Text, entry.Icon) + action.Enabled = (entry.Enabled == nil) and true or entry.Enabled + + if entries.CurrentKey then + action.Checked = entries.CurrentKey == entry.Key + else + action.Checked = entry.Checked + end + + connectAction(connections, action, entry, entry.Key) + end + elseif entry then -- Ignore false/nil for when plugins do {xyz, fflag and abc, ...} + error("Unsupported action "..tostring(entry)) + end + end + + return menu +end + +local function makePluginMenu(plugin, entries) + local subMenus = {} + local connections = {} + + local menu = createPluginMenu(plugin, entries, subMenus, connections) + + menu:ShowAsync() + for _, subMenu in ipairs(subMenus) do + subMenu:Destroy() + end + menu:Destroy() +end + +return { + makePluginMenu = makePluginMenu, + Separator = SEPARATOR, +} \ No newline at end of file diff --git a/BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Studio/StudioStyle.lua b/BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Studio/StudioStyle.lua new file mode 100644 index 0000000000..b7af169df8 --- /dev/null +++ b/BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Studio/StudioStyle.lua @@ -0,0 +1,54 @@ +--[[ + Matches the table structure of UILibrary's Style table. Provides a default mapping for colors + +]] +local StudioStyle = {} +StudioStyle.__index = StudioStyle + +-- c : StudioStyleGuideColor +-- m : StudioStyleGuideModifier +function StudioStyle.new(getColor, c, m) + local FStringMainFont = game:GetFastString("StudioBuiltinPluginDefaultFont") + + return { + font = Enum.Font[FStringMainFont], + + backgroundColor = getColor(c.MainBackground), + liveBackgroundColor = Color3.new(), + textColor = getColor(c.MainText), + subTextColor = getColor(c.SubText), + dimmerTextColor = getColor(c.DimmedText), + + itemColor = getColor(c.Item), + borderColor = getColor(c.Border), + + hoveredItemColor = getColor(c.Item, m.Hover), + hoveredTextColor = getColor(c.MainText, m.Hover), + + primaryItemColor = getColor(c.DialogMainButton), + primaryBorderColor = getColor(c.DialogMainButton), + primaryTextColor = getColor(c.DialogMainButtonText), + + primaryHoveredItemColor = getColor(c.DialogMainButton, m.Hover), + primaryHoveredBorderColor = getColor(c.DialogMainButton, m.Hover), + primaryHoveredTextColor = getColor(c.DialogMainButtonText, m.Hover), + + selectionColor = getColor(c.Item, m.Selected), + selectionBorderColor = getColor(c.Border, m.Selected), + selectedTextColor = getColor(c.MainText, m.Selected), + + shadowColor = getColor(c.Shadow), + shadowTransparency = getColor(c.Shadow, m.Hover), + + separationLineColor = getColor(c.Separator), + + disabledColor = getColor(c.MainText, m.Disabled), + errorColor = getColor(c.ErrorText), + + hoverColor = getColor(c.MainBackground, m.Hover), + + hyperlinkTextColor = getColor(c.LinkText), + } +end + +return StudioStyle \ No newline at end of file diff --git a/BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Studio/StudioStyle.spec.lua b/BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Studio/StudioStyle.spec.lua new file mode 100644 index 0000000000..c79266d200 --- /dev/null +++ b/BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Studio/StudioStyle.spec.lua @@ -0,0 +1,35 @@ +local StudioStyle = require(script.Parent.StudioStyle) + +local Library = script.Parent.Parent +local Style = require(Library.StyleDefaults) +local StudioTheme = require(Library.Studio.StudioTheme) + +return function() + it("should define all of the keys in the Style.Defaults object", function() + local numKeysFound = 0 + local numKeysExpected = 0 + + local defaultStyle = Style.Defaults + local studioTheme = StudioTheme.newDummyTheme(function() return {} end) + local mockStudioTheme = studioTheme.getTheme() + local studioStyle = StudioStyle.new(mockStudioTheme.GetColor, + Enum.StudioStyleGuideColor, + Enum.StudioStyleGuideModifier) + + -- every key in the default style should be accounted for + for colorKey, _ in pairs(defaultStyle) do + numKeysExpected = numKeysExpected + 1 + expect(studioStyle[colorKey]).to.be.ok() + end + + -- there should not be extra keys defined + for _, _ in pairs(studioStyle) do + numKeysFound = numKeysFound + 1 + end + + expect(numKeysFound).to.equal(numKeysExpected) + + -- clean up + studioTheme:destroy() + end) +end \ No newline at end of file diff --git a/BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Studio/StudioTheme.lua b/BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Studio/StudioTheme.lua new file mode 100644 index 0000000000..159fc6ce35 --- /dev/null +++ b/BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Studio/StudioTheme.lua @@ -0,0 +1,133 @@ +--[[ + Wraps theme colors and update logic for Roblox Studio. + Plugins are responsible for making a wrapper around this StudioTheme that + defines a createValues function. This function maps Studio theme enums + to named table entries which can be used in the plugin's theme. + + Example usage (a Theme.lua module in your plugin): + + local StudioTheme = require(Plugin.UILibrary.Studio.StudioTheme) + local Theme = {} + + function Theme.createValues(getColor, c, m) + return { + backgroundColor = getColor(c.MainBackground), + } + end + + function Theme.new() + return StudioTheme.new(Theme.createValues) + end + + return Theme +]] + +game:DefineFastFlag("FixMockStudioTheme", false) + +local Library = script.Parent.Parent +local join = require(Library.join) +local Signal = require(Library.Utils.Signal) + +local StudioTheme = {} +StudioTheme.__index = StudioTheme + +function StudioTheme.new(createValues, overrideSignal) + local self = { + getTheme = function() + return settings().Studio.Theme + end, + + createValues = function(...) + return createValues(...) + end, + + valuesChanged = Signal.new(), + values = {}, + themeChangedConnection = nil, + } + + setmetatable(self, StudioTheme) + + if overrideSignal then + self.themeChangedConnection = overrideSignal:Connect(function() + self:recalculateTheme() + end) + else + self.themeChangedConnection = settings().Studio.ThemeChanged:Connect(function() + self:recalculateTheme() + end) + end + + self:recalculateTheme() + + return self +end + +function StudioTheme:connect(...) + return self.valuesChanged:connect(...) +end + +function StudioTheme:destroy() + if self.themeChangedConnection then + self.themeChangedConnection:Disconnect() + end +end + +function StudioTheme:update(changedValues) + self.values = join(self.values, changedValues) + + if self.valuesChanged then + self.valuesChanged:fire(self.values) + end +end + +function StudioTheme:recalculateTheme() + local theme = self.getTheme() + + -- Shorthands for getting a color + local c = Enum.StudioStyleGuideColor + local m = Enum.StudioStyleGuideModifier + + local function getColor(...) + return theme:GetColor(...) + end + + local newValues = self.createValues(getColor, c, m) + + self:update(newValues) +end + +function StudioTheme.newDummyTheme(createValues) + local self = { + getTheme = function() + return { + GetColor = function() + return Color3.new() + end, + } + end, + + createValues = function(...) + return createValues(...) + end, + + valuesChanged = Signal.new(), + values = {}, + } + + setmetatable(self, StudioTheme) + + if game:GetFastFlag("FixMockStudioTheme") then + local newValues = self.createValues(function() + return self.getTheme():GetColor() + end, {}, {}) + + self:update(newValues) + else + self:recalculateTheme() + end + + return self +end + +return StudioTheme diff --git a/BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Studio/StudioTheme.spec.lua b/BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Studio/StudioTheme.spec.lua new file mode 100644 index 0000000000..100b16a65f --- /dev/null +++ b/BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Studio/StudioTheme.spec.lua @@ -0,0 +1,102 @@ +return function() + local Library = script.Parent.Parent + local StudioTheme = require(Library.Studio.StudioTheme) + + local function createValues() + return {} + end + + describe("StudioTheme.new", function() + it("should return a new StudioTheme", function() + local theme = StudioTheme.new(createValues) + expect(theme).to.be.ok() + expect(theme.values).to.be.ok() + + theme:destroy() + end) + + it("should have a getTheme function that gets the Studio theme", function() + local theme = StudioTheme.new(createValues) + expect(theme.getTheme).to.be.ok() + expect(theme.getTheme()).to.equal(settings().Studio.Theme) + + theme:destroy() + end) + + it("should listen for Studio theme changes", function() + local event = Instance.new("BindableEvent") + local theme = StudioTheme.new(createValues, event.Event) + expect(theme.themeChangedConnection).to.be.ok() + + local called = false + theme:connect(function() + called = true + end) + event:Fire() + expect(called).to.equal(true) + + event:Destroy() + theme:destroy() + end) + end) + + describe("StudioTheme.newDummyTheme", function() + it("should return a new fake StudioTheme", function() + local theme = StudioTheme.newDummyTheme(createValues) + expect(theme).to.be.ok() + expect(theme.values).to.be.ok() + + theme:destroy() + end) + + it("should have a getTheme function that returns a constant color", function() + local theme = StudioTheme.newDummyTheme(createValues) + expect(theme.getTheme).to.be.ok() + expect(theme.getTheme()).never.to.equal(settings().Studio.Theme) + expect(theme.getTheme().GetColor).to.be.ok() + expect(theme.getTheme().GetColor()).to.equal(Color3.new()) + + theme:destroy() + end) + + it("should not listen for theme changes", function() + local theme = StudioTheme.newDummyTheme(createValues) + expect(theme.themeChangedConnection).never.to.be.ok() + + theme:destroy() + end) + end) + + describe("StudioTheme.recalculateTheme", function() + it("should call the createValues function", function() + local called = false + + local theme = StudioTheme.new(function() + called = true + return {} + end) + + expect(called).to.equal(true) + called = false + theme:recalculateTheme() + expect(called).to.equal(true) + + theme:destroy() + end) + + it("should update the theme values", function() + local called = false + + local theme = StudioTheme.new(function() + return called and {newColor = Color3.new()} or {} + end) + + expect(theme.values.newColor).never.to.be.ok() + called = true + theme:recalculateTheme() + expect(theme.values.newColor).to.be.ok() + + theme:destroy() + end) + end) +end \ No newline at end of file diff --git a/BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/StyleDefaults.lua b/BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/StyleDefaults.lua new file mode 100644 index 0000000000..537f6b3c69 --- /dev/null +++ b/BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/StyleDefaults.lua @@ -0,0 +1,71 @@ +--[[ + Provides style utility for the UILibrary. + Default values for style can be defined here. +]] + +local Style = {} + +--Defines default entries for the Style table in createTheme. +--Default entries are only to provide compatibility, not actual theme info. +Style.Defaults = { + font = Enum.Font.SourceSans, + + backgroundColor = Color3.new(), + liveBackgroundColor = Color3.new(), + textColor = Color3.new(), + subTextColor = Color3.new(), + dimmerTextColor = Color3.new(), + + itemColor = Color3.new(), + borderColor = Color3.new(), + + hoveredItemColor = Color3.new(), + hoveredTextColor = Color3.new(), + + primaryItemColor = Color3.new(), + primaryBorderColor = Color3.new(), + primaryTextColor = Color3.new(), + + primaryHoveredItemColor = Color3.new(), + primaryHoveredBorderColor = Color3.new(), + primaryHoveredTextColor = Color3.new(), + + selectionColor = Color3.new(), + selectionBorderColor = Color3.new(), + selectedTextColor = Color3.new(), + + shadowColor = Color3.new(), + shadowTransparency = Color3.new(), + + separationLineColor = Color3.new(), + + disabledColor = Color3.new(), + errorColor = Color3.new(), + + hoverColor = Color3.new(), + + hyperlinkTextColor = Color3.new(), +} + +-- A function that checks to see if there are any missing or +-- extraneous keys in the given style. If a value is available +-- for all necessary entries, then the UILibrary will be able to run. +Style.isValid = function(style) + local requiredStyle = Style.Defaults + + for key, _ in pairs(requiredStyle) do + if not style[key] then + return false + end + end + + for key, _ in pairs(style) do + if not requiredStyle[key] then + return false + end + end + + return true +end + +return Style \ No newline at end of file diff --git a/BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Theming.lua b/BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Theming.lua new file mode 100644 index 0000000000..4bda76d40d --- /dev/null +++ b/BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Theming.lua @@ -0,0 +1,63 @@ +local Library = script.Parent + +local Roact = require(Library.Parent.Parent.Roact) +local Signal = require(Library.Utils.Signal) +local Symbol = require(Library.Utils.Symbol) +local themeKey = Symbol.named("UILibraryTheme") + +local ThemeProvider = Roact.PureComponent:extend("UILibraryThemeProvider") +function ThemeProvider:init() + local theme = self.props.theme + assert(theme ~= nil, "No theme was given to this ThemeProvider.") + self.themeChanged = Signal.new() + + self._context[themeKey] = { + values = theme, + themeChanged = self.themeChanged, + } +end +function ThemeProvider:render() + self._context[themeKey].values = self.props.theme + self.themeChanged:fire() + return Roact.oneChild(self.props[Roact.Children]) +end + +-- the consumer should complain if it doesn't have a theme +local ThemeConsumer = Roact.PureComponent:extend("UILibraryThemeConsumer") +function ThemeConsumer:init() + assert(self._context[themeKey] ~= nil, "No ThemeProvider found.") + local theme = self._context[themeKey] + + self.state = { + themeValues = theme.values, + } + + self.themeConnection = theme.themeChanged:connect(function() + self:setState({ + themeValues = theme.values, + }) + end) +end +function ThemeConsumer:render() + local themeValues = self.state.themeValues + return self.props.themedRender(themeValues) +end +function ThemeConsumer:willUnmount() + if self.themeConnection then + self.themeConnection:disconnect() + end +end + +-- withTheme should provide a simple way to style elements +-- callback : function(theme) +local function withTheme(callback) + return Roact.createElement(ThemeConsumer, { + themedRender = callback + }) +end + +return { + Provider = ThemeProvider, + Consumer = ThemeConsumer, + withTheme = withTheme, +} \ No newline at end of file diff --git a/BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/UILibraryWrapper.lua b/BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/UILibraryWrapper.lua new file mode 100644 index 0000000000..51b7868561 --- /dev/null +++ b/BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/UILibraryWrapper.lua @@ -0,0 +1,69 @@ +--[[ + A component that wraps all external elements needed for the UILibrary. + Entries in the wrapper are optional, but if you do not provide an + element that is needed by the components you are using, you will get + an error upon trying to mount those components. + + Props: + Theme theme = A theme object to be used by a ThemeProvider. + PluginGui focusGui = The top-level gui to be used by a FocusProvider. + Plugin plugin = A Plugin object which can be used to construct guis. +]] + +local Library = script.Parent +local Roact = require(Library.Parent.Parent.Roact) + +local Theming = require(Library.Theming) +local ThemeProvider = Theming.Provider + +local Focus = require(Library.Focus) +local FocusProvider = Focus.Provider + +local Plugin = require(Library.Plugin) +local PluginProvider = Plugin.Provider + +local Camera = require(Library.Camera) +local CameraProvider = Camera.Provider + +local UILibraryWrapper = Roact.PureComponent:extend("UILibraryWrapper") + +function UILibraryWrapper:addProvider(root, provider, props) + return Roact.createElement(provider, props, {root}) +end + +function UILibraryWrapper:render() + local props = self.props + local children = props[Roact.Children] + local root = Roact.oneChild(children) + + -- ThemeProvider + local theme = props.theme + if theme then + root = self:addProvider(root, ThemeProvider, { + theme = theme, + }) + end + + -- FocusProvider + local focusGui = props.focusGui + if focusGui then + root = self:addProvider(root, FocusProvider, { + pluginGui = focusGui, + }) + end + + -- PluginProvider + local plugin = props.plugin + if plugin then + root = self:addProvider(root, PluginProvider, { + plugin = plugin, + }) + end + + -- CameraProvider + root = self:addProvider(root, CameraProvider, nil) + + return root +end + +return UILibraryWrapper \ No newline at end of file diff --git a/BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/UILibraryWrapper.spec.lua b/BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/UILibraryWrapper.spec.lua new file mode 100644 index 0000000000..089f6bf3ee --- /dev/null +++ b/BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/UILibraryWrapper.spec.lua @@ -0,0 +1,86 @@ +return function() + local Library = script.Parent + local Roact = require(Library.Parent.Parent.Roact) + + local workspace = game:GetService("Workspace") + + local UILibraryWrapper = require(script.Parent.UILibraryWrapper) + + local function createTestWrapper(props, children) + return Roact.createElement(UILibraryWrapper, props, children) + end + + it("should create and destroy without errors", function() + local element = createTestWrapper() + local instance = Roact.mount(element) + Roact.unmount(instance) + end) + + it("should render its children if nothing is provided", function () + local container = Instance.new("Folder") + local instance = Roact.mount(createTestWrapper({}, { + Frame = Roact.createElement("Frame") + }), container) + + local frame = container:FindFirstChildOfClass("Frame") + + expect(frame).to.be.ok() + + Roact.unmount(instance) + end) + + it("should render its children if items are provided", function () + local container = Instance.new("Folder") + local instance = Roact.mount(createTestWrapper({ + theme = {}, + focusGui = {}, + plugin = {}, + }, { + Frame = Roact.createElement("Frame") + }), container) + + local frame = container:FindFirstChildOfClass("Frame") + + expect(frame).to.be.ok() + + Roact.unmount(instance) + end) + + describe("addProvider", function() + it("should place the new provider above the root", function () + local container = Instance.new("Folder") + local root = Roact.createElement("Frame") + + local result = UILibraryWrapper:addProvider(root, "Frame") + local instance = Roact.mount(result, container) + local frame = container:FindFirstChildOfClass("Frame") + + expect(frame).to.be.ok() + expect(frame[1]).to.be.ok() + + Roact.unmount(instance) + end) + + it("should not modify the tree below the root", function () + local container = Instance.new("Folder") + local root = Roact.createElement("Frame", {}, { + ChildFrame = Roact.createElement("Frame", {}, { + DescendantFrame = Roact.createElement("Frame"), + }), + OtherChild = Roact.createElement("Frame"), + }) + + local result = UILibraryWrapper:addProvider(root, "Frame") + local instance = Roact.mount(result, container) + local frame = container:FindFirstChildOfClass("Frame") + + expect(frame).to.be.ok() + expect(frame[1]).to.be.ok() + expect(frame[1].ChildFrame).to.be.ok() + expect(frame[1].ChildFrame.DescendantFrame).to.be.ok() + expect(frame[1].OtherChild).to.be.ok() + + Roact.unmount(instance) + end) + end) +end \ No newline at end of file diff --git a/BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Utils/AssetType.lua b/BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Utils/AssetType.lua new file mode 100644 index 0000000000..ad59cfcd6a --- /dev/null +++ b/BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Utils/AssetType.lua @@ -0,0 +1,126 @@ +local FFlagFixGetAssetTypeErrorHandling = game:DefineFastFlag("FixGetAssetTypeErrorHandling", false) +local FFlagStudioUILibFixAssetTypeMap = game:DefineFastFlag("StudioUILibFixAssetTypeMap", false) +local FFlagStudioFixMeshPartPreview = game:DefineFastFlag("StudioFixMeshPartPreview", false) +local FFlagEnableToolboxVideos = game:GetFastFlag("EnableToolboxVideos") + +local AssetType = {} + +AssetType.TYPES = { + ModelType = 1, -- MeshPart, Mesh, Model + ImageType = 2, + SoundType = 3, -- Sound comes with the model or mesh. + ScriptType = 4, -- Server, local, module + PluginType = 5, + OtherType = 6, + LoadingType = 7, + VideoType = 8, +} + +-- For check if we show preview button or not. +AssetType.AssetTypesPreviewEnabled = { + [Enum.AssetType.Mesh.Value] = true, + [Enum.AssetType.MeshPart.Value] = true, + [Enum.AssetType.Model.Value] = true, + [Enum.AssetType.Decal.Value] = true, + [Enum.AssetType.Image.Value] = true, + [Enum.AssetType.Audio.Value] = true, + [Enum.AssetType.Lua.Value] = true, + [Enum.AssetType.Plugin.Value] = true, + [Enum.AssetType.Video.Value] = FFlagEnableToolboxVideos or nil, +} + +local classTypeMap = { + BasePart = AssetType.TYPES.ModelType, + Model = AssetType.TYPES.ModelType, + BackpackItem = AssetType.TYPES.ModelType, + Accoutrement = AssetType.TYPES.ModelType, + + Decal = AssetType.TYPES.ImageType, + ImageLabel = AssetType.TYPES.ImageType, + ImageButton = AssetType.TYPES.ImageType, + Texture =AssetType.TYPES.ImageType, + Sky = AssetType.TYPES.ImageType, + + Sound = AssetType.TYPES.SoundType, + VideoFrame = AssetType.TYPES.VideoType, + + BaseScript = AssetType.TYPES.ScriptType, +} + +if FFlagStudioUILibFixAssetTypeMap then + classTypeMap.Part = AssetType.TYPES.ModelType +end + +if FFlagStudioFixMeshPartPreview then + classTypeMap.MeshPart = AssetType.TYPES.ModelType +end + +-- For AssetPreview, we divide assets into four categories. +-- For any parts or meshes, we will need to do a model preview. +-- For images, we show only an image. +-- For sound, we will need to show something and provide play control. (Will +-- probably improve this in the future) +-- For BaseScript, show only names while for all other types show assetName and type +function AssetType:getAssetType(assetInstance) + local notInstance + if FFlagFixGetAssetTypeErrorHandling then + notInstance = not assetInstance or typeof(assetInstance) ~= "Instance" + else + notInstance = not assetInstance + end + + if notInstance then + return self.TYPES.LoadingType + end + local className = assetInstance.className + local type = classTypeMap[className] + + if not type then + return self.TYPES.OtherType + end + + return type +end + +function AssetType:isModel(currentType) + return currentType == self.TYPES.ModelType +end + +function AssetType:isImage(currentType) + return currentType == self.TYPES.ImageType +end + +function AssetType:isAudio(currentType) + return currentType == self.TYPES.SoundType +end + +function AssetType:isScript(currentType) + return currentType == self.TYPES.ScriptType +end + +function AssetType:isPlugin(currentType) + return currentType == self.TYPES.PluginType +end + +function AssetType:markAsPlugin() + return self.TYPES.PluginType +end + +function AssetType:isOtherType(currentType) + return currentType == self.TYPES.OtherType +end + +function AssetType:isLoading(currentType) + return currentType == self.TYPES.LoadingType +end + +function AssetType:isVideo(currentType) + return currentType == self.TYPES.VideoType +end + +function AssetType:isPreviewAvailable(typeId) + assert(typeId ~= nil, "AssetPreviewType can't be nil") + return AssetType.AssetTypesPreviewEnabled[typeId] ~= nil +end + +return AssetType \ No newline at end of file diff --git a/BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Utils/AssetType.spec.lua b/BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Utils/AssetType.spec.lua new file mode 100644 index 0000000000..f4bbeee4ce --- /dev/null +++ b/BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Utils/AssetType.spec.lua @@ -0,0 +1,24 @@ +return function() + local AssetType = require(script.Parent.AssetType) + + describe("isPreviewAvailable()", function() + it("should make sure the assetPreviewType is nil.", function() + expect(function() + AssetType.isPreviewAvailable(nil) + end).to.throw() + end) + + it("should show preview for sound.", function() + local typeId = Enum.AssetType.Audio.Value + local result = AssetType:isPreviewAvailable(typeId) + expect(result).to.equal(true) + end) + + it("should not show preview for LeftArm.", function() + local typeId = Enum.AssetType.LeftArm.Value + local result = AssetType:isPreviewAvailable(typeId) + expect(result).to.equal(false) + end) + end) + +end diff --git a/BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Utils/GetClassIcon.lua b/BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Utils/GetClassIcon.lua new file mode 100644 index 0000000000..4d3cf1a3e7 --- /dev/null +++ b/BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Utils/GetClassIcon.lua @@ -0,0 +1,31 @@ +local FFlagStudioFixGetClassIcon = settings():GetFFlag("StudioFixGetClassIcon") +local FFlagStudioMinorFixesForAssetPreview = settings():GetFFlag("StudioMinorFixesForAssetPreview") + +local StudioService = game:GetService("StudioService") + +local function GetClassIcon(instance) + if FFlagStudioFixGetClassIcon then + local className = instance.ClassName + if instance.IsA then + if instance:IsA("JointInstance") and className == "ManualWeld" or className == "ManualGlue" then + return StudioService:GetClassIcon("JointInstance") + end + end + return StudioService:GetClassIcon(className) + else + if FFlagStudioMinorFixesForAssetPreview then + if typeof(instance) ~= "Instance" then + return StudioService:GetClassIcon("Model") + end + end + + local className = instance.ClassName + if instance:IsA("JointInstance") and className == "ManualWeld" or className == "ManualGlue" then + return StudioService:GetClassIcon("JointInstance") + else + return StudioService:GetClassIcon(className) + end + end +end + +return GetClassIcon diff --git a/BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Utils/GetClassIcon.spec.lua b/BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Utils/GetClassIcon.spec.lua new file mode 100644 index 0000000000..6d1800ed04 --- /dev/null +++ b/BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Utils/GetClassIcon.spec.lua @@ -0,0 +1,43 @@ +-- Icon location is determined by an offset within a sequential list of icons in a single image. +local ICON_NOTFOUND = Vector2.new(0,0) +local ICON_JOINTINSTANCE = Vector2.new(544,0) +local ICON_SCRIPT = Vector2.new(96,0) + +return function() + local GetClassIcon = require(script.Parent.GetClassIcon) + + describe("getClassIcon", function() + it("should correctly return 'JointInstance' classIcon for ManualWelds", function() + local manualWeld = Instance.new("ManualWeld") + local classIconTable = GetClassIcon(manualWeld) + + expect(classIconTable.ImageRectOffset).to.equal(ICON_JOINTINSTANCE) + expect(classIconTable.ImageRectOffset).never.to.equal(ICON_NOTFOUND) + end) + + it("should correctly return 'JointInstance' classIcon for ManualGlues", function() + local manualGlue = Instance.new("ManualGlue") + local classIconTable = GetClassIcon(manualGlue) + + expect(classIconTable.ImageRectOffset).to.equal(ICON_JOINTINSTANCE) + expect(classIconTable.ImageRectOffset).never.to.equal(ICON_NOTFOUND) + end) + + it("should return the Script Class Icon for scripts", function() + local script = Instance.new("Script") + local classIconTable = GetClassIcon(script) + + expect(classIconTable.ImageRectOffset).to.equal(ICON_SCRIPT) + expect(classIconTable.ImageRectOffset).never.to.equal(ICON_NOTFOUND) + end) + + it("should support non-instance objects that have a ClassName member", function() + local notAnInstance = { + ClassName = "Folder" + } + + local classIconTable = GetClassIcon(notAnInstance) + expect(classIconTable).to.be.ok() + end) + end) +end \ No newline at end of file diff --git a/BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Utils/GetTextSize.lua b/BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Utils/GetTextSize.lua new file mode 100644 index 0000000000..37984ffb20 --- /dev/null +++ b/BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Utils/GetTextSize.lua @@ -0,0 +1,20 @@ +local TextService = game:GetService("TextService") + +local FStringMainFont = game:DefineFastString("StudioBuiltinPluginDefaultFont", "Gotham") + +local FONT_SIZE_MEDIUM = 16 +local FONT = Enum.Font.Gotham +pcall(function() + FONT = Enum.Font[FStringMainFont] +end) + +local function GetTextSize(text, fontSize, font, frameSize) + + fontSize = fontSize or FONT_SIZE_MEDIUM + font = font or FONT + frameSize = frameSize or Vector2.new(math.huge, math.huge) + + return TextService:GetTextSize(text, fontSize, font, frameSize) +end + +return GetTextSize \ No newline at end of file diff --git a/BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Utils/Immutable.lua b/BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Utils/Immutable.lua new file mode 100644 index 0000000000..a73d2035b1 --- /dev/null +++ b/BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Utils/Immutable.lua @@ -0,0 +1,141 @@ +--[[ + Provides functions for manipulating immutable data structures. +]] + +local Immutable = {} + +--[[ + Merges dictionary-like tables together. +]] +function Immutable.JoinDictionaries(...) + local result = {} + + for i = 1, select("#", ...) do + local dictionary = select(i, ...) + for key, value in pairs(dictionary) do + result[key] = value + end + end + + return result +end + +--[[ + Joins any number of lists together into a new list +]] +function Immutable.JoinLists(...) + local new = {} + + for listKey = 1, select("#", ...) do + local list = select(listKey, ...) + local len = #new + + for itemKey = 1, #list do + new[len + itemKey] = list[itemKey] + end + end + + return new +end + +--[[ + Creates a new copy of the dictionary and sets a value inside it. +]] +function Immutable.Set(dictionary, key, value) + local new = {} + + for key, value in pairs(dictionary) do + new[key] = value + end + + new[key] = value + + return new +end + +--[[ + Creates a new copy of the list with the given elements appended to it. +]] +function Immutable.Append(list, ...) + local new = {} + local len = #list + + for key = 1, len do + new[key] = list[key] + end + + for i = 1, select("#", ...) do + new[len + i] = select(i, ...) + end + + return new +end + +--[[ + Remove elements from a dictionary +]] +function Immutable.RemoveFromDictionary(dictionary, ...) + local result = {} + + for key, value in pairs(dictionary) do + local found = false + for listKey = 1, select("#", ...) do + if key == select(listKey, ...) then + found = true + break + end + end + if not found then + result[key] = value + end + end + + return result +end + +--[[ + Remove the given key from the list. +]] +function Immutable.RemoveFromList(list, removeIndex) + local new = {} + + for i = 1, #list do + if i ~= removeIndex then + table.insert(new, list[i]) + end + end + + return new +end + +--[[ + Remove the range from the list starting from the index. +]] +function Immutable.RemoveRangeFromList(list, index, count) + local new = {} + + for i = 1, #list do + if i < index or i >= index + count then + table.insert(new, list[i]) + end + end + + return new +end + +--[[ + Creates a new list that has no occurrences of the given value. +]] +function Immutable.RemoveValueFromList(list, removeValue) + local new = {} + + for i = 1, #list do + if list[i] ~= removeValue then + table.insert(new, list[i]) + end + end + + return new +end + +return Immutable diff --git a/BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Utils/Immutable.spec.lua b/BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Utils/Immutable.spec.lua new file mode 100644 index 0000000000..12ad39a52a --- /dev/null +++ b/BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Utils/Immutable.spec.lua @@ -0,0 +1,284 @@ +return function() + local Immutable = require(script.Parent.Immutable) + + describe("JoinDictionaries", function() + it("should preserve immutability", function() + local a = {} + local b = {} + + local c = Immutable.JoinDictionaries(a, b) + + expect(c).never.to.equal(a) + expect(c).never.to.equal(b) + end) + + it("should treat list-like values like dictionary values", function() + local a = { + [1] = 1, + [2] = 2, + [3] = 3 + } + + local b = { + [1] = 11, + [2] = 22 + } + + local c = Immutable.JoinDictionaries(a, b) + + expect(c[1]).to.equal(b[1]) + expect(c[2]).to.equal(b[2]) + expect(c[3]).to.equal(a[3]) + end) + + it("should merge dictionary values correctly", function() + local a = { + hello = "world", + foo = "bar" + } + + local b = { + foo = "baz", + tux = "penguin" + } + + local c = Immutable.JoinDictionaries(a, b) + + expect(c.hello).to.equal(a.hello) + expect(c.foo).to.equal(b.foo) + expect(c.tux).to.equal(b.tux) + end) + + it("should merge multiple dictionaries", function() + local a = { + foo = "yes" + } + + local b = { + bar = "yup" + } + + local c = { + baz = "sure" + } + + local d = Immutable.JoinDictionaries(a, b, c) + + expect(d.foo).to.equal(a.foo) + expect(d.bar).to.equal(b.bar) + expect(d.baz).to.equal(c.baz) + end) + end) + + describe("JoinLists", function() + it("should preserve immutability", function() + local a = {} + local b = {} + + local c = Immutable.JoinLists(a, b) + + expect(c).never.to.equal(a) + expect(c).never.to.equal(b) + end) + + it("should treat list-like values correctly", function() + local a = {1, 2, 3} + local b = {4, 5, 6} + + local c = Immutable.JoinLists(a, b) + + expect(#c).to.equal(6) + + for i = 1, #c do + expect(c[i]).to.equal(i) + end + end) + + it("should merge multiple lists", function() + local a = {1, 2} + local b = {3, 4} + local c = {5, 6} + + local d = Immutable.JoinLists(a, b, c) + + expect(#d).to.equal(6) + + for i = 1, #d do + expect(d[i]).to.equal(i) + end + end) + end) + + describe("Set", function() + it("should preserve immutability", function() + local a = {} + + local b = Immutable.Set(a, "foo", "bar") + + expect(b).never.to.equal(a) + end) + + it("should treat numeric keys normally", function() + local a = {1, 2, 3} + + local b = Immutable.Set(a, 2, 4) + + expect(b[1]).to.equal(1) + expect(b[2]).to.equal(4) + expect(b[3]).to.equal(3) + end) + + it("should overwrite dictionary-like keys", function() + local a = { + foo = "bar", + baz = "qux" + } + + local b = Immutable.Set(a, "foo", "hello there") + + expect(b.foo).to.equal("hello there") + expect(b.baz).to.equal(a.baz) + end) + end) + + describe("Append", function() + it("should preserve immutability", function() + local a = {} + + local b = Immutable.Append(a, "another happy landing") + + expect(b).never.to.equal(a) + end) + + it("should append values", function() + local a = {1, 2, 3} + local b = Immutable.Append(a, 4, 5) + + expect(#b).to.equal(5) + + for i = 1, #b do + expect(b[i]).to.equal(i) + end + end) + end) + + describe("RemoveFromDictionary", function() + it("should preserve immutability", function() + local a = { foo = "bar" } + + local b = Immutable.RemoveFromDictionary(a, "foo") + + expect(b).to.never.equal(a) + end) + + it("should remove fields from the dictionary", function() + local a = { + foo = "bar", + baz = "qux", + boof = "garply", + } + + local b = Immutable.RemoveFromDictionary(a, "foo", "boof") + + expect(b.foo).to.never.be.ok() + expect(b.baz).to.equal("qux") + expect(b.boof).to.never.be.ok() + end) + end) + + describe("RemoveFromList", function() + it("should preserve immutability", function() + local a = {1, 2, 3} + local b = Immutable.RemoveFromList(a, 2) + + expect(b).never.to.equal(a) + end) + + it("should remove elements from the list", function() + local a = {1, 2, 3} + local b = Immutable.RemoveFromList(a, 2) + + expect(b[1]).to.equal(1) + expect(b[2]).to.equal(3) + expect(b[3]).never.to.be.ok() + end) + end) + + describe("RemoveRangeFromList", function() + it("should preserve immutability", function() + local a = {1, 2, 3} + local b = Immutable.RemoveRangeFromList(a, 2, 1) + + expect(b).never.to.equal(a) + end) + + it("should remove single elements from the middle of the list", function() + local a = {1, 2, 3} + local b = Immutable.RemoveRangeFromList(a, 2, 1) + + expect(b[1]).to.equal(1) + expect(b[2]).to.equal(3) + expect(b[3]).never.to.be.ok() + end) + + it("should remove multiple elements from the front of the list", function() + local a = {1, 2, 3, 4, 5, 6} + local b = Immutable.RemoveRangeFromList(a, 1, 4) + + expect(b[1]).to.equal(5) + expect(b[2]).to.equal(6) + expect(b[3]).never.to.be.ok() + end) + + it("should remove multiple elements properly from middle of the list", function() + local a = {1, 2, 3, 4, 5, 6} + local b = Immutable.RemoveRangeFromList(a, 2, 4) + + expect(b[1]).to.equal(1) + expect(b[2]).to.equal(6) + expect(b[3]).never.to.be.ok() + end) + + it("should remove multiple elements properly from the end of the list", function() + local a = {1, 2, 3, 4, 5, 6, 7} + local b = Immutable.RemoveRangeFromList(a, 4, 4) + + expect(b[1]).to.equal(1) + expect(b[2]).to.equal(2) + expect(b[3]).to.equal(3) + expect(b[4]).never.to.be.ok() + end) + + it("should not remove any elements when count is 0 or less", function() + local a = {1, 2, 3} + local b = Immutable.RemoveRangeFromList(a, 2, 0) + + expect(b[1]).to.equal(1) + expect(b[2]).to.equal(2) + expect(b[3]).to.equal(3) + + local c = Immutable.RemoveRangeFromList(a, 2, -1) + expect(c[1]).to.equal(1) + expect(c[2]).to.equal(2) + expect(c[3]).to.equal(3) + end) + end) + + describe("RemoveValueFromList", function() + it("should preserve immutability", function() + local a = {1, 1, 1} + local b = Immutable.RemoveValueFromList(a, 1) + + expect(b).never.to.equal(a) + end) + + it("should remove all elements from the list", function() + local a = {1, 2, 2, 3} + local b = Immutable.RemoveValueFromList(a, 2) + + expect(b[1]).to.equal(1) + expect(b[2]).to.equal(3) + expect(b[3]).never.to.be.ok() + end) + end) +end \ No newline at end of file diff --git a/BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Utils/InsertToolEvent.lua b/BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Utils/InsertToolEvent.lua new file mode 100644 index 0000000000..3e399ab0d0 --- /dev/null +++ b/BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Utils/InsertToolEvent.lua @@ -0,0 +1,51 @@ +local InsertToolEvent = {} +InsertToolEvent.__index = InsertToolEvent + +InsertToolEvent.INSERT_TO_WORKSPACE = 0 +InsertToolEvent.INSERT_TO_STARTER_PACK = 1 +InsertToolEvent.INSERT_CANCELLED = 2 + +function InsertToolEvent.new(onPromptCallback) + local self = { + _onPromptCallback = onPromptCallback, + _bindable = Instance.new("BindableEvent"), + _waiting = false, + } + setmetatable(self, InsertToolEvent) + return self +end + +function InsertToolEvent:isWaiting() + return self._waiting +end + +function InsertToolEvent:destroy() + self:cancel() + self._bindable:Destroy() +end + +function InsertToolEvent:insertToWorkspace() + self._bindable:Fire(InsertToolEvent.INSERT_TO_WORKSPACE) +end + +function InsertToolEvent:insertToStarterPack() + self._bindable:Fire(InsertToolEvent.INSERT_TO_STARTER_PACK) +end + +function InsertToolEvent:cancel() + self._bindable:Fire(InsertToolEvent.INSERT_CANCELLED) +end + +function InsertToolEvent:promptAndWait() + if self._waiting then + return InsertToolEvent.INSERT_CANCELLED + end + + self._waiting = true + self._onPromptCallback() + local result = self._bindable.Event:Wait() + self._waiting = false + return result +end + +return InsertToolEvent diff --git a/BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Utils/LayoutOrderIterator.lua b/BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Utils/LayoutOrderIterator.lua new file mode 100644 index 0000000000..3f5186b038 --- /dev/null +++ b/BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Utils/LayoutOrderIterator.lua @@ -0,0 +1,37 @@ +--[[ + LayoutOrderIterator + Dynamically generates the "LayoutOrder = ..." so LayoutOrder for all elements + does not need to be adjusted when adding or removing elements. + + e.g. + local orderIterator = LayoutOrderIterator.new() + ... + Part1 = Roact.createElement(..., { + ... + LayoutOrder = orderIterator:getNextOrder(), + ... + }), + Part2 = Roact.createElement(..., { + ... + LayoutOrder = orderIterator:getNextOrder(), + ... + }), +]] + +local LayoutOrderIterator = {} +LayoutOrderIterator.__index = LayoutOrderIterator + +function LayoutOrderIterator.new() + local self = setmetatable({}, LayoutOrderIterator) + + self.order = 0 + + return self +end + +function LayoutOrderIterator:getNextOrder() + self.order = self.order + 1 + return self.order +end + +return LayoutOrderIterator \ No newline at end of file diff --git a/BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Utils/LayoutOrderIterator.spec.lua b/BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Utils/LayoutOrderIterator.spec.lua new file mode 100644 index 0000000000..1619ac28a9 --- /dev/null +++ b/BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Utils/LayoutOrderIterator.spec.lua @@ -0,0 +1,30 @@ +return function() + local LayoutOrderIterator = require(script.Parent.LayoutOrderIterator) + + describe("new", function() + it("should construct from nothing", function() + local orderIterator = LayoutOrderIterator.new() + + expect(orderIterator).to.be.ok() + end) + end) + + describe("getNextOrder", function() + it("should correctly generate the next order", function() + local orderIterator = LayoutOrderIterator.new() + + expect(orderIterator:getNextOrder()).to.be.equal(1) + expect(orderIterator:getNextOrder()).to.be.equal(2) + expect(orderIterator:getNextOrder()).to.be.equal(3) + + end) + + it("should correctly generate the next order when more than one iterators are created", function() + local orderIterator1 = LayoutOrderIterator.new() + local orderIterator2 = LayoutOrderIterator.new() + + expect(orderIterator1:getNextOrder()).to.be.equal(1) + expect(orderIterator2:getNextOrder()).to.be.equal(1) + end) + end) +end \ No newline at end of file diff --git a/BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Utils/MathUtils.lua b/BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Utils/MathUtils.lua new file mode 100644 index 0000000000..dbafba5b63 --- /dev/null +++ b/BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Utils/MathUtils.lua @@ -0,0 +1,15 @@ +local MathUtils = {} + +MathUtils.NEAR_ZERO = 0.0001 + +function MathUtils:fuzzyEq(numOne, numTwo, epsilon) + epsilon = epsilon or MathUtils.NEAR_ZERO + return math.abs(numOne - numTwo) < epsilon +end + +function MathUtils:round(num, numDecimalPlaces) + local mult = 10^(numDecimalPlaces or 0) + return math.floor(num * mult + 0.5) / mult +end + +return MathUtils \ No newline at end of file diff --git a/BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Utils/MathUtils.spec.lua b/BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Utils/MathUtils.spec.lua new file mode 100644 index 0000000000..4a2e6195f4 --- /dev/null +++ b/BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Utils/MathUtils.spec.lua @@ -0,0 +1,40 @@ +return function() + local Utils = script.Parent + local mathUtils = require(Utils.MathUtils) + + describe("Round", function() + it("should round to specified place", function() + local num = 101.39901 + expect(mathUtils:round(num, 0)).to.be.equal(101) + expect(mathUtils:round(num, 1)).to.be.equal(101.4) + expect(mathUtils:round(num, 2)).to.be.equal(101.40) + expect(mathUtils:round(num, 3)).to.be.equal(101.399) + expect(mathUtils:round(num, 4)).to.be.equal(101.3990) + end) + + it("round should work for negative numbers", function() + local num = -0.99095 + expect(mathUtils:round(num, 0)).to.be.equal(-1) + expect(mathUtils:round(num, 1)).to.be.equal(-1) + expect(mathUtils:round(num, 2)).to.be.equal(-0.99) + expect(mathUtils:round(num, 3)).to.be.equal(-0.991) + expect(mathUtils:round(num, 4)).to.be.equal(-0.9909) + end) + end) + + describe("Fuzzy Equals", function() + it("fuzzyEq should work with no epsilon provided", function() + expect(mathUtils:fuzzyEq(2.00009, 2)).to.be.equal(true) + expect(mathUtils:fuzzyEq(2, 2)).to.be.equal(true) + expect(mathUtils:fuzzyEq(2, 3)).to.be.equal(false) + expect(mathUtils:fuzzyEq(mathUtils.NEAR_ZERO, 0)).to.be.equal(false) + end) + + it("fuzzyEq should work with supplied epsilon value", function() + expect(mathUtils:fuzzyEq(2.0000009, 2, 0.000001)).to.be.equal(true) + expect(mathUtils:fuzzyEq(2, 2, 0.1)).to.be.equal(true) + expect(mathUtils:fuzzyEq(2, 3, 0.01)).to.be.equal(false) + expect(mathUtils:fuzzyEq(0.00000001, 0, 0.00000001)).to.be.equal(false) + end) + end) +end \ No newline at end of file diff --git a/BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Utils/Signal.lua b/BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Utils/Signal.lua new file mode 100644 index 0000000000..cde7956269 --- /dev/null +++ b/BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Utils/Signal.lua @@ -0,0 +1,63 @@ +--[[ + A limited, simple implementation of a Signal. + + Handlers are fired in order, and (dis)connections are properly handled when + executing an event. + + Signal uses Immutable to avoid invalidating the 'Fire' loop iteration. +]] + +local Immutable = require(script.Parent.Immutable) + +local Signal = {} + +Signal.__index = Signal + +function Signal.new() + local self = { + _listeners = {} + } + + setmetatable(self, Signal) + + return self +end + +function Signal:connect(callback) + local listener = { + callback = callback, + isConnected = true, + } + self._listeners = Immutable.Append(self._listeners, listener) + + local function disconnect() + listener.isConnected = false + self._listeners = Immutable.RemoveValueFromList(self._listeners, listener) + end + + return { + Disconnect = function() + disconnect() + end, + disconnect = disconnect, + } +end + +function Signal:fire(...) + for _, listener in ipairs(self._listeners) do + if listener.isConnected then + listener.callback(...) + end + end +end + +function Signal:Connect(...) + return self:connect(...) +end + +function Signal:Fire(...) + self:fire(...) +end + + +return Signal diff --git a/BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Utils/Signal.spec.lua b/BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Utils/Signal.spec.lua new file mode 100644 index 0000000000..f00f9477b0 --- /dev/null +++ b/BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Utils/Signal.spec.lua @@ -0,0 +1,114 @@ +return function() + local Signal = require(script.Parent.Signal) + + it("should construct from nothing", function() + local signal = Signal.new() + + expect(signal).to.be.ok() + end) + + it("should fire connected callbacks", function() + local callCount = 0 + local value1 = "Hello World" + local value2 = 7 + + local callback = function(arg1, arg2) + expect(arg1).to.equal(value1) + expect(arg2).to.equal(value2) + callCount = callCount + 1 + end + + local signal = Signal.new() + + local connection = signal:connect(callback) + signal:fire(value1, value2) + + expect(callCount).to.equal(1) + + connection:disconnect() + signal:fire(value1, value2) + + expect(callCount).to.equal(1) + end) + + it("should disconnect handlers", function() + local callback = function() + error("Callback was called after disconnect!") + end + + local signal = Signal.new() + + local connection = signal:connect(callback) + connection:disconnect() + + signal:fire() + end) + + it("should fire handlers in order", function() + local signal = Signal.new() + local x = 0 + local y = 0 + + local callback1 = function() + expect(x).to.equal(0) + expect(y).to.equal(0) + x = x + 1 + end + + local callback2 = function() + expect(x).to.equal(1) + expect(y).to.equal(0) + y = y + 1 + end + + signal:connect(callback1) + signal:connect(callback2) + signal:fire() + + expect(x).to.equal(1) + expect(y).to.equal(1) + end) + + it("should continue firing despite mid-event disconnection", function() + local signal = Signal.new() + local countA = 0 + local countB = 0 + + local connectionA + connectionA = signal:connect(function() + connectionA:disconnect() + countA = countA + 1 + end) + + signal:connect(function() + countB = countB + 1 + end) + + signal:fire() + + expect(countA).to.equal(1) + expect(countB).to.equal(1) + end) + + it("should skip listeners that were disconnected during event evaluation", function() + local signal = Signal.new() + local countA = 0 + local countB = 0 + + local connectionB + + signal:connect(function() + countA = countA + 1 + connectionB:disconnect() + end) + + connectionB = signal:connect(function() + countB = countB + 1 + end) + + signal:fire() + + expect(countA).to.equal(1) + expect(countB).to.equal(0) + end) +end \ No newline at end of file diff --git a/BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Utils/Spritesheet.lua b/BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Utils/Spritesheet.lua new file mode 100644 index 0000000000..0b9d15893d --- /dev/null +++ b/BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Utils/Spritesheet.lua @@ -0,0 +1,86 @@ +--[[ + Parses spritesheets into a list of sprites that you can then join with Roact ImageLabel/Button props + Sprites are returned in the order they appear in the spritesheet in the form of: + { + Image = asset, + ImageRectSize = SpriteSize, + ImageRectOffset = positionOfSpriteInSheet, + } + + Required arguments: + string asset - AssetId or path to the spritesheet + table props - Table of properties, similar to creating a Roact element + + Required props: + number/Vector2 SpriteSize - how large each sprite is (must be the same for all sprites) + a number SpriteSize is converted to a uniform Vector2 + number NumSprites - how many sprites there are in the spritesheet + + Optional props: + number SpritesheetWidth - how wide the entire spritesheet is. Defaults to 1024 (max image size) + You do not need to change this unless you break your sprites onto a new + line before X=1024 when there is still enough room for another sprite, or + your spritesheet is wider than 1024 (Don't do this! The engine will automatically + downscale images larger than the max size) + + Example usage: + + local expandButton = Spritesheet("rbxasset://textures/folder/expand.png", { + SpriteSize = 32, + NumSprites = 4, + } + + local onStateImageProps = expandButton[1] + local offStateImageProps = expandButton[2] + + component:render() + local expandImageProps = self.state.expanded and onStateImage or offStateImage + Roact.createElement("ImageLabel", Cryo.Dictionary.join(expandImageProps, { + ... + })) +]] + +-- TODO check if 1K limitation is necessary in /content or only web +local MAX_IMAGE_SIZE = 1024 + +local function Spritesheet(image, props) + local spriteSizeType = typeof(props.SpriteSize) + local spriteCountType = typeof(props.NumSprites) + local sheetWidthType = typeof(props.SpritesheetWidth) + + assert(spriteSizeType == "number" or spriteSizeType == "Vector2", + "SpriteSize must be number or Vector2. Got type '"..spriteSizeType.."'") + assert(spriteCountType == "number", + "NumSprites must be number. Got type'"..spriteCountType.."'") + assert(sheetWidthType == "number" or sheetWidthType == "nil", + "SpritesheetWidth must be a number or nil. Got '"..sheetWidthType.."'") + + local spriteSize = spriteSizeType == "number" and Vector2.new(1, 1) * props.SpriteSize or props.SpriteSize + local numSprites = props.NumSprites + local sheetWidth = props.SpritesheetWidth or MAX_IMAGE_SIZE + + assert(spriteSize.X > 0 and spriteSize.Y > 0, + "SpriteSize does not support <= 0 values. Got '"..tostring(spriteSize).."'") + assert(numSprites > 0, + "NumSprites must be > 0. Got '"..numSprites) + assert(sheetWidth > 0, + "SpritesheetWidth does not support <= 0 values. Got '"..sheetWidth.."'") + + local sprites = {} + + local numColumns = math.floor(sheetWidth / spriteSize.X) + for i = 0, props.NumSprites - 1 do + local row = math.floor(i / numColumns) + local column = i % numColumns + + table.insert(sprites, { + Image = image, + ImageRectSize = spriteSize, + ImageRectOffset = Vector2.new(column * spriteSize.X, row * spriteSize.Y), + }) + end + + return sprites +end + +return Spritesheet \ No newline at end of file diff --git a/BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Utils/Spritesheet.spec.lua b/BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Utils/Spritesheet.spec.lua new file mode 100644 index 0000000000..479459d975 --- /dev/null +++ b/BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Utils/Spritesheet.spec.lua @@ -0,0 +1,115 @@ +return function() + local Spritesheet = require(script.Parent.Spritesheet) + + it("should verify correct props", function() + local success,_ + + -- Missing required props + success,_ = pcall(function() + return Spritesheet("", {}) + end) + expect(success).to.equal(false) + + -- Missing required prop NumSprites + success,_ = pcall(function() + return Spritesheet("", {SpriteSize = 1, NumSprites = nil}) + end) + expect(success).to.equal(false) + + -- Missing required props SpriteSize + success,_ = pcall(function() + return Spritesheet("", {SpriteSize = nil, NumSprites = 1}) + end) + expect(success).to.equal(false) + + -- SpritesheetWidth is invalid type + success,_ = pcall(function() + return Spritesheet("", {SpriteSize = 1, NumSprites = 1, SpritesheetWidth = ""}) + end) + expect(success).to.equal(false) + + -- Has all required props of correct type + success,_ = pcall(function() + return Spritesheet("", {SpriteSize = 1, NumSprites = 1}) + end) + expect(success).to.equal(true) + + -- Has all required props of correct type + success,_ = pcall(function() + return Spritesheet("", {SpriteSize = Vector2.new(1,1), NumSprites = 1}) + end) + expect(success).to.equal(true) + + -- Has all props of correct type + success,_ = pcall(function() + return Spritesheet("", {SpriteSize = 1, NumSprites = 1, SpritesheetWidth = 1}) + end) + expect(success).to.equal(true) + + -- SpriteSize out of range + success,_ = pcall(function() + return Spritesheet("", {SpriteSize = 0, NumSprites = 1}) + end) + expect(success).to.equal(false) + + -- NumSprites out of range + success,_ = pcall(function() + return Spritesheet("", {SpriteSize = 1, NumSprites = 0}) + end) + expect(success).to.equal(false) + + -- SpritesheetWidth out of range + success,_ = pcall(function() + return Spritesheet("", {SpriteSize = 1, NumSprites = 1, SpritesheetWidth = 0}) + end) + expect(success).to.equal(false) + end) + + it("should return correct result for a single-row spritesheet", function() + local asset = "rbxasset://test" + local numSprites = 3 + local spriteSize = Vector2.new(32, 16) + local sprites = Spritesheet(asset, { + NumSprites = numSprites, + SpriteSize = spriteSize, + }) + + -- Correct number of sprites + expect(#sprites).to.equal(numSprites) + + -- Correct sprite properties + for i,sprite in pairs(sprites) do + expect(sprite.Image).to.equal(asset) + expect(sprite.ImageRectSize).to.equal(spriteSize) + expect(sprite.ImageRectOffset.X).to.equal((i - 1) * spriteSize.X) + expect(sprite.ImageRectOffset.Y).to.equal(0) + end + end) + + it("should return correct result for a multi-row spritesheet", function() + local asset = "rbxasset://test" + local numSprites = 5 + local spriteSize = Vector2.new(32, 16) + local sheetWidth = 66 + local sprites = Spritesheet(asset, { + NumSprites = numSprites, + SpriteSize = spriteSize, + SpritesheetWidth = sheetWidth, + }) + + -- Correct number of sprites + expect(#sprites).to.equal(numSprites) + + -- Correct sprite properties + local numColumns = math.floor(sheetWidth / spriteSize.X) + for i,sprite in pairs(sprites) do + local row = math.floor((i - 1) / numColumns) + local column = (i - 1) % numColumns + + expect(sprite.Image).to.equal(asset) + expect(sprite.ImageRectSize).to.equal(spriteSize) + expect(sprite.ImageRectOffset.X).to.equal(column * spriteSize.X) + expect(sprite.ImageRectOffset.Y).to.equal(row * spriteSize.Y) + end + end) +end \ No newline at end of file diff --git a/BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Utils/Symbol.lua b/BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Utils/Symbol.lua new file mode 100644 index 0000000000..d9e26d9c65 --- /dev/null +++ b/BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Utils/Symbol.lua @@ -0,0 +1,44 @@ +--[[ + A 'Symbol' is an opaque marker type. + + Symbols have the type 'userdata', but when printed to the console, the name + of the symbol is shown. +]] + +local Symbol = {} + +--[[ + Creates a Symbol with the given name. + + When printed or coerced to a string, the symbol will turn into the string + given as its name. +]] +function Symbol.named(name) + assert(type(name) == "string", "Symbols must be created using a string name!") + + local self = newproxy(true) + + local wrappedName = ("Symbol(%s)"):format(name) + + getmetatable(self).__tostring = function() + return wrappedName + end + + return self +end + +--[[ + Create an unnamed Symbol. Usually, you should create a named Symbol using + Symbol.named(name) +]] +function Symbol.unnamed() + local self = newproxy(true) + + getmetatable(self).__tostring = function() + return "Unnamed Symbol" + end + + return self +end + +return Symbol \ No newline at end of file diff --git a/BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Utils/Symbol.spec.lua b/BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Utils/Symbol.spec.lua new file mode 100644 index 0000000000..f3312055c9 --- /dev/null +++ b/BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Utils/Symbol.spec.lua @@ -0,0 +1,45 @@ +return function() + local Symbol = require(script.Parent.Symbol) + + describe("named", function() + it("should give an opaque object", function() + local symbol = Symbol.named("foo") + + expect(symbol).to.be.a("userdata") + end) + + it("should coerce to the given name", function() + local symbol = Symbol.named("foo") + + expect(tostring(symbol):match("foo")).to.be.ok() + end) + + it("should be unique when constructed", function() + local symbolA = Symbol.named("abc") + local symbolB = Symbol.named("abc") + + expect(symbolA).never.to.equal(symbolB) + end) + end) + + describe("unnamed", function() + it("should give an opaque object", function() + local symbol = Symbol.unnamed() + + expect(symbol).to.be.a("userdata") + end) + + it("should coerce to some string", function() + local symbol = Symbol.unnamed() + + expect(tostring(symbol)).to.be.a("string") + end) + + it("should be unique when constructed", function() + local symbolA = Symbol.unnamed() + local symbolB = Symbol.unnamed() + + expect(symbolA).never.to.equal(symbolB) + end) + end) +end diff --git a/BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Utils/Urls.lua b/BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Utils/Urls.lua new file mode 100644 index 0000000000..5327c279e5 --- /dev/null +++ b/BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Utils/Urls.lua @@ -0,0 +1,43 @@ +local ContentProvider = game:GetService("ContentProvider") + +-- helper functions +local function parseBaseUrlInformation() + -- get the current base url from the current configuration + local baseUrl = ContentProvider.BaseUrl + -- keep a copy of the base url + -- append a trailing slash if there isn't one + if baseUrl:sub(#baseUrl) ~= "/" then + baseUrl = baseUrl .. "/" + end + -- parse out scheme (http, https) + local _, schemeEnd = baseUrl:find("://") + -- parse out the prefix (www, kyle, ying, etc.) + local prefixIndex, prefixEnd = baseUrl:find("%.", schemeEnd + 1) + local basePrefix = baseUrl:sub(schemeEnd + 1, prefixIndex - 1) + -- parse out the domain + local baseDomain = baseUrl:sub(prefixEnd + 1) + return baseUrl, basePrefix, baseDomain +end + +-- url construction building blocks +local baseUrl, basePrefix, baseDomain = parseBaseUrlInformation() + +local BASE_GAMEASSET_URL = "https://assetgame.%sasset/?id=%d&#assetTypeId=%d&isPackage=%s" +local RBXTHUMB_BASE_URL = "rbxthumb://type=%s&id=%d&w=%d&h=%d" +local ASSET_ID_STRING = "rbxassetid://%d" + +local Urls = {} + +function Urls.constructAssetThumbnailUrl(assetId, width, height) + return RBXTHUMB_BASE_URL:format("Asset", tonumber(assetId) or 0, width, height) +end + +function Urls.constructAssetIdString(assetId) + return ASSET_ID_STRING:format(assetId) +end + +function Urls.constructAssetGameAssetIdUrl(assetId, assetTypeId, isPackage) + return BASE_GAMEASSET_URL:format(baseDomain, assetId, assetTypeId, isPackage) +end + +return Urls \ No newline at end of file diff --git a/BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Utils/getTimeString.lua b/BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Utils/getTimeString.lua new file mode 100644 index 0000000000..3cb586f375 --- /dev/null +++ b/BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Utils/getTimeString.lua @@ -0,0 +1,11 @@ +-- return mm:ss format. +return function(seconds) + assert(type(seconds) == "number", "seconds must be a number") + + local isNegative = seconds < 0 + local adjustedSeconds = math.abs(seconds) + local min = math.floor(adjustedSeconds / 60) + local sec = math.floor(adjustedSeconds % 60) + + return string.format("%s%d:%02d", isNegative and "-" or "", min, sec) +end \ No newline at end of file diff --git a/BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Utils/getTimeString.spec.lua b/BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Utils/getTimeString.spec.lua new file mode 100644 index 0000000000..d24dd1953c --- /dev/null +++ b/BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/Utils/getTimeString.spec.lua @@ -0,0 +1,51 @@ +return function() + local getTimeString = require(script.Parent.getTimeString) + + it("should return a string", function() + local result = getTimeString(0) + + expect(result).to.be.a("string") + end) + + it("should handle 30 seconds correctly", function() + local result = getTimeString(30) + + expect(result).to.equal("0:30") + end) + + it("should ignore decimals", function() + local result = getTimeString(1.5) + + expect(result).to.equal("0:01") + end) + + it("should ignore negative decimals", function() + local result = getTimeString(-1.5) + + expect(result).to.equal("-0:01") + end) + + it("should handle 60 seconds correctly", function() + local result = getTimeString(60) + + expect(result).to.equal("1:00") + end) + + it("should handle 90 seconds correctly", function() + local result = getTimeString(90) + + expect(result).to.equal("1:30") + end) + + it("should handle 120 seconds correctly", function() + local result = getTimeString(120) + + expect(result).to.equal("2:00") + end) + + it("should handle 150 seconds correctly", function() + local result = getTimeString(150) + + expect(result).to.equal("2:30") + end) +end \ No newline at end of file diff --git a/BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/createTheme.lua b/BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/createTheme.lua new file mode 100644 index 0000000000..b9dd6a2d70 --- /dev/null +++ b/BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/createTheme.lua @@ -0,0 +1,578 @@ +--[[ + Transforms values from an app theme into a theme usable by the UILibrary. + + Parameters: + table style: + Specifies default style values which will be used to construct the theme. + These include the basic palette colors (backgroundColor, textColor, etc), + as well as other top-level fonts and sizes. Refer to defaultStyle for + a list of style values that can be overridden. + + table overrides (optional): + After the theme is created, overrides can be set for specific elements. + + For example, an overrides table of: + { + checkBox.backgroundColor = Color3.new(1, 1, 1), + } + + Would change the background color of only the Checkbox component. +]] + +local Style = require(script.Parent.StyleDefaults) +local replaceDefaults = require(script.Parent.deepJoin) + +return function(style, overrides) + style = style or {} + overrides = overrides or {} + + style = replaceDefaults(Style.Defaults, style) + assert(Style.isValid(style), "Provided style table could not be validated.") + + -- Theme entries for UILibrary components are defined below + local checkBox = { + font = style.font, + + --TODO: Move texture to StudioSharedUI + backgroundImage = "rbxasset://textures/GameSettings/UncheckedBox.png", + selectedImage = "rbxasset://textures/GameSettings/CheckedBoxLight.png", + + backgroundColor = style.backgroundColor, + titleColor = style.textColor, + } + + local roundFrame = { + --TODO: Move texture to StudioSharedUI + backgroundImage = "rbxasset://textures/StudioToolbox/RoundedBackground.png", + borderImage = "rbxasset://textures/StudioToolbox/RoundedBorder.png", + slice = Rect.new(3, 3, 13, 13), + } + + local dropShadow = { + --TODO: Move texture to StudioSharedUI + image = "rbxasset://textures/StudioUIEditor/resizeHandleDropShadow.png", + } + + local tooltip = { + font = style.font, + textSize = 12, + + backgroundColor = style.itemColor, + borderColor = style.borderColor, + textColor = style.textColor, + shadowColor = style.shadowColor, + shadowTransparency = style.shadowTransparency, + } + + local keyframe = { + Default = { + backgroundColor = style.itemColor, + borderColor = style.borderColor, + + selected = { + backgroundColor = style.selectionColor, + borderColor = style.selectionBorderColor, + }, + }, + + Primary = { + backgroundColor = style.primaryItemColor, + borderColor = style.primaryBorderColor, + + selected = { + backgroundColor = style.primaryHoveredItemColor, + borderColor = style.selectionBorderColor, + }, + }, + } + + local scrubber = { + backgroundColor = style.selectionColor, + image = "", + } + + local scrollingFrame = { + --TODO: Move texture to StudioSharedUI + topImage = "rbxasset://textures/StudioToolbox/ScrollBarTop.png", + midImage = "rbxasset://textures/StudioToolbox/ScrollBarMiddle.png", + bottomImage = "rbxasset://textures/StudioToolbox/ScrollBarBottom.png", + + backgroundColor = style.backgroundColor, + scrollbarColor = style.borderColor, + } + + local radioButton = { + radioButtonBackground = "rbxasset://textures/GameSettings/RadioButton.png", + radioButtonColor = style.separationLineColor, + radioButtonSelected = "rbxasset://textures/ui/LuaApp/icons/ic-blue-dot.png", + textSize = 18, + buttonHeight = 20, + font = style.font, + textColor = style.textColor, + contentPadding = 16, + buttonPadding = 6, + } + + local dropdownMenu = { + borderColor = style.borderColor, + --TODO: Move texture to StudioSharedUI + borderImage = "rbxasset://textures/StudioToolbox/RoundedBorder.png", + } + + local styledDropdown = { + font = style.font, + + backgroundColor = style.backgroundColor, + borderColor = style.borderColor, + textColor = style.textColor, + + --TODO: Move texture to StudioSharedUI + arrowImage = "rbxasset://textures/StudioToolbox/ArrowDownIconWhite.png", + + hovered = { + backgroundColor = style.hoveredItemColor, + textColor = style.hoveredTextColor, + }, + + selected = { + backgroundColor = style.selectionColor, + borderColor = style.selectionBorderColor, + textColor = style.selectedTextColor, + }, + } + + local detailedDropdown = { + font = style.font, + + backgroundColor = style.backgroundColor, + disabled = style.disabledColor, + disabledText = style.dimmerTextColor, + borderColor = style.borderColor, + displayText = style.textColor, + descriptionText = style.subTextColor, + + --TODO: Move texture to StudioSharedUI + arrowImage = "rbxasset://textures/StudioToolbox/ArrowDownIconWhite.png", + + hovered = { + backgroundColor = style.hoveredItemColor, + displayText = style.hoveredTextColor, + }, + + selected = { + backgroundColor = style.selectionColor, + disabled = style.disabledColor, + borderColor = style.selectionBorderColor, + displayText = style.selectedTextColor, + }, + } + + local titledFrame = { + font = style.font, + text = style.subTextColor, + } + + local textBox = { + font = style.font, + background = style.backgroundColor, + disabled = style.disabledColor, + borderDefault = style.borderColor, + borderHover = style.hoverColor, + tooltip = style.dimmerTextColor, + text = style.textColor, + error = style.errorColor, + } + + local textButton = { + font = style.font, + } + + local textEntry = { + textTransparency = { + enabled = 0, + disabled = 0.5 + } + } + + local separator = { + lineColor = style.borderColor, + } + + local treeView = { + elementPadding = 4, + margins = { + left = 2, + top = 2, + right = 2, + bottom = 2, + }, + indentOffset = 8, + scrollbar = replaceDefaults(scrollingFrame, { + scrollbarThickness = 16, + scrollbarPadding = 2, + scrollbarImageColor = style.borderColor, + }), + defaultElementWidth = 140, + } + + local dialog = { + font = style.font, + + background = style.backgroundColor, + textColor = style.textColor, + } + + local bulletPoint = { + font = style.font, + + text = style.textColor, + } + + local button = { + Default = { + font = style.font, + isRound = true, + + backgroundColor = style.itemColor, + textColor = style.textColor, + borderColor = style.borderColor, + + hovered = { + backgroundColor = style.hoveredItemColor, + textColor = style.hoveredTextColor, + borderColor = style.borderColor, + }, + }, + + Primary = { + font = style.font, + isRound = true, + + backgroundColor = style.primaryItemColor, + textColor = style.primaryTextColor, + borderColor = style.primaryBorderColor, + + hovered = { + backgroundColor = style.primaryHoveredItemColor, + textColor = style.primaryHoveredTextColor, + borderColor = style.primaryHoveredBorderColor, + }, + }, + } + + local loadingBar = { + font = style.font, + fontSize = 16, + text = style.textColor, + bar = { + foregroundColor = style.dimmerTextColor, + backgroundColor = style.backgroundColor, + }, + } + + local loadingIndicator = { + baseColor = style.hoveredItemColor, + endColor = style.dimmerTextColor, + } + + local toggleButton = { + defaultWidth = 20, + defaultHeight = 20, + + onImage = "rbxasset://textures/RoactStudioWidgets/toggle_on_light.png", + offImage = "rbxasset://textures/RoactStudioWidgets/toggle_off_light.png", + disabledImage = "rbxasset://textures/RoactStudioWidgets/toggle_disable_light.png", + } + + local hyperlink = { + textSize = 22, + textColor = style.hyperlinkTextColor, + font = style.font, + } + + local assetPreview = { + font = style.font, + textSize = 14, + textSizeMedium = 16, + textSizeLarge = 18, + textSizeTitle= 22, + fontBold = style.font, + background = style.backgroundColor, + + padding = 12, + + assetNameColor = style.textColor, + descriptionTextColor = style.textColor, + + actionBar = { + background = style.backgroundColor, + + button = { + backgroundColor = style.primaryItemColor, + backgroundDisabledColor = style.disabledColor, + backgroundHoveredColor = style.primaryHoveredItemColor + }, + + showMore = { + backgroundColor = style.backgroundColor, + borderColor = style.borderColor + }, + + text = { + color = style.textColor, + colorDisabled = style.disabledColor, + }, + + padding = 12, + centerPadding = 10, + + robuxSize = UDim2.fromOffset(16,16), + + images = { + showMore = "rbxasset://textures/StudioToolbox/AssetPreview/more.png", + robuxSmall = "rbxasset://textures/ui/common/robux_small.png", + colorWhite = Color3.fromRGB(255, 255, 255), + } + }, + + description = { + height = 28, + + searchBarIconSize = 14, + padding = 8, + + backgroundColor = style.backgroundColor, + leftTextColor = style.textColor, + rightTextColor = style.textColor, + lineColor = style.borderColor, + + images = { + searchIcon = "rbxasset://textures/StudioToolbox/Search.png", + }, + }, + + images = { + deleteButton = "rbxasset://textures/StudioToolbox/DeleteButton.png", + scrollbarTopImage = "rbxasset://textures/StudioToolbox/ScrollBarTop.png", + scrollbarMiddleImage = "rbxasset://textures/StudioToolbox/ScrollBarMiddle.png", + scrollbarBottomImage = "rbxasset://textures/StudioToolbox/ScrollBarBottom.png", + thumbUpSmall = "rbxasset://textures/StudioToolbox/AssetPreview/rating_small.png", + }, + + favorites = { + contentColor = Color3.fromRGB(246, 183, 2), + favorited = "rbxasset://textures/StudioToolbox/AssetPreview/star_filled.png", + unfavorited = "rbxasset://textures/StudioToolbox/AssetPreview/star_stroke.png" + }, + + imagePreview = { + background = style.backgroundColor, + textColor = style.textColor, + }, + + modelPreview = { + background = style.backgroundColor, + }, + + thumbnailIconPreview = { + background = style.backgroundColor, + textColor = style.textColor, + + textLabelPadding = 20, + iconSize = 16, + defaultTextLabelHeight = 20, + }, + + treeViewButton = { + buttonSize = 28, + backgroundTrans = 0.25, + backgroundColor = style.background, + backgroundDisabledColor = style.disabledColor, + hierarchy = "rbxasset://textures/StudioToolbox/AssetPreview/hierarchy.png" + }, + + audioPreview = { + backgroundColor = style.backgroundColor, + textColor = style.textColor, + playButton = "rbxasset://textures/StudioToolbox/AssetPreview/play_button.png", + pauseButton = "rbxasset://textures/StudioToolbox/AssetPreview/pause_button.png", + buttonBackgroundColor = style.background, + buttonDisabledBackgroundColor = style.disabledColor, + buttonDisabledBackgroundTransparency = 0.5, + buttonColor = style.textColor, + audioPlay_BG = "rbxasset://textures/StudioToolbox/AssetPreview/audioPlay_BG.png", + audioPlay_BG_Color = Color3.fromRGB(204, 204, 204), + progressBar = Color3.fromRGB(0, 162, 255), + progressBar_BG_Color = style.background, + progressKnob = "rbxasset://textures/DeveloperFramework/slider_knob.png", + progressKnobColor = style.background, + font = style.font, + fontSize = 16, + }, + + videoPreview = { + backgroundColor = style.backgroundColor, + videoBackgroundColor = style.backgroundColor, + playButton = "rbxasset://textures/StudioToolbox/AssetPreview/play_button.png", + pauseButton = "rbxasset://textures/StudioToolbox/AssetPreview/pause_button.png", + pauseOverlayColor =Color3.fromRGB(0, 0, 0), + pauseOverlayTransparency = 0.5, + }, + + vote = { + backgroundTrans = 0.9, + background = style.backgroundColor, + borderColor = style.borderColor, + textColor = style.textColor, + subTextColor = style.subTextColor, + + button = { + backgroundColor = style.itemColor, + backgroundTrans = 0, + disabledColor = Color3.fromRGB(10, 10, 10), + }, + + voteUp = { + backgroundColor = Color3.fromRGB(0, 100, 0), + borderColor = style.borderColor, + }, + + voteDown = { + backgroundColor = Color3.fromRGB(100, 0, 0), + borderColor = style.borderColor, + }, + + images = { + voteDown = "rbxasset://textures/StudioToolbox/AssetPreview/vote_down.png", + voteUp = "rbxasset://textures/StudioToolbox/AssetPreview/vote_up.png", + thumbUp = "rbxasset://textures/StudioToolbox/AssetPreview/rating_large.png" + } + }, + } + + local searchBar = { + backgroundColor = style.backgroundColor, + + text = { + placeholder = { + color = style.dimmerTextColor, + }, + font = style.font, + size = 16, + color = style.textColor, + }, + + divideLine = { + color = style.borderColor, + }, + + border = { + hovered = { + color = style.hoverColor, + }, + selected = { + color = style.selectionBorderColor, + }, + color = style.borderColor, + }, + + buttons = { + iconSize = 14, + size = 28, + inset = 2, + clear = { + color = Color3.fromRGB(184, 184, 184), + }, + + search = { + hovered = { + color = Color3.fromRGB(0, 162, 255), + }, + color = Color3.fromRGB(184, 184, 184), + }, + }, + + images = { + clear = { + hovered = { + image = "rbxasset://textures/StudioSharedUI/clear-hover.png", + }, + image = "rbxasset://textures/StudioSharedUI/clear.png", + }, + + search = { + image = "rbxasset://textures/StudioSharedUI/search.png", + }, + }, + } + + local instanceTreeView = { + font = style.font, + textSize = 14, + + background = style.background, + + treeItemHeight = 16, + treeViewIndent = 20, + + scrollbarPadding = 2, + scrollbarThickness = 8, + + scrollbarTopImage = "rbxasset://textures/StudioToolbox/ScrollBarTop.png", + scrollbarMiddleImage = "rbxasset://textures/StudioToolbox/ScrollBarMiddle.png", + scrollBarBottomImage = "rbxasset://textures/StudioToolbox/ScrollBarBottom.png", + + arrowExpanded = "rbxasset://textures/StudioToolbox/ArrowExpanded.png", + arrowCollapsed = "rbxasset://textures/StudioToolbox/ArrowCollapsed.png", + + elementPadding = 4, + + borderPadding = 15, + + tooltipShowDelay = 0.3, + + arrowColor = style.textColor, + selectedText = style.selectedTextColor, + textColor = style.textColor, + selected = style.selectedTextColor, + hover = style.hoverColor, + } + + local styledTooltip = { + backgroundColor = style.itemColor, + shadowColor = style.shadowColor, + shadowTransparency = style.shadowTransparency, + shadowOffset = Vector2.new(1, 1), + } + + return replaceDefaults({ + assetPreview = assetPreview, + checkBox = checkBox, + roundFrame = roundFrame, + dropShadow = dropShadow, + tooltip = tooltip, + keyframe = keyframe, + scrollingFrame = scrollingFrame, + dropdownMenu = dropdownMenu, + styledDropdown = styledDropdown, + detailedDropdown = detailedDropdown, + titledFrame = titledFrame, + textBox = textBox, + textButton = textButton, + textEntry = textEntry, + separator = separator, + dialog = dialog, + button = button, + scrubber = scrubber, + loadingBar = loadingBar, + loadingIndicator = loadingIndicator, + bulletPoint = bulletPoint, + toggleButton = toggleButton, + radioButton = radioButton, + treeView = treeView, + hyperlink = hyperlink, + instanceTreeView = instanceTreeView, + searchBar = searchBar, + styledTooltip = styledTooltip, + }, overrides) +end \ No newline at end of file diff --git a/BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/deepJoin.lua b/BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/deepJoin.lua new file mode 100644 index 0000000000..8affe568ef --- /dev/null +++ b/BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/deepJoin.lua @@ -0,0 +1,31 @@ +local function deepJoin(t1, t2) + local new = {} + + for key, value in pairs(t1) do + if typeof(value) == "table" then + if t2[key] and typeof(t2[key]) == "table" then + new[key] = deepJoin(value, t2[key]) + else + -- this essentially acts like a deepcopy to prevent + -- references getting all tangled up + new[key] = deepJoin(value, {}) + end + else + new[key] = value + end + end + + for key, value in pairs(t2) do + if typeof(value) == "table" then + if not t1[key] then + new[key] = deepJoin(value, {}) + end + else + new[key] = value + end + end + + return new +end + +return deepJoin \ No newline at end of file diff --git a/BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/deepJoin.spec.lua b/BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/deepJoin.spec.lua new file mode 100644 index 0000000000..de11f7c5d5 --- /dev/null +++ b/BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/deepJoin.spec.lua @@ -0,0 +1,76 @@ +return function() + local Library = script.Parent + local deepJoin = require(Library.deepJoin) + + it("should join two tables together", function() + local tableA = {key1 = "Value1"} + local tableB = {key2 = "Value2"} + + local result = deepJoin(tableA, tableB) + + expect(result.key1).to.equal("Value1") + expect(result.key2).to.equal("Value2") + end) + + it("should add all the entries from the first table", function() + local tableA = {key1 = "Value1", key2 = "Value2"} + local tableB = {} + + local result = deepJoin(tableA, tableB) + + expect(result.key1).to.equal("Value1") + expect(result.key2).to.equal("Value2") + end) + + it("should add all the entries from the second table", function() + local tableA = {} + local tableB = {key1 = "Value1", key2 = "Value2"} + + local result = deepJoin(tableA, tableB) + + expect(result.key1).to.equal("Value1") + expect(result.key2).to.equal("Value2") + end) + + it("should join values in nested tables", function() + local tableA = { + set = { + key1 = "Value1", + }, + } + + local tableB = { + set = { + key2 = "Value2", + }, + } + + local result = deepJoin(tableA, tableB) + + expect(result.set).to.be.ok() + expect(result.set.key1).to.equal("Value1") + expect(result.set.key2).to.equal("Value2") + end) + + it("should prioritize the second table if values overlap", function() + local tableA = { + outsideKey = "Old", + set = { + insideKey = "Old", + }, + } + + local tableB = { + outsideKey = "New", + set = { + insideKey = "New", + }, + } + + local result = deepJoin(tableA, tableB) + + expect(result.outsideKey).to.equal("New") + expect(result.set).to.be.ok() + expect(result.set.insideKey).to.equal("New") + end) +end \ No newline at end of file diff --git a/BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/join.lua b/BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/join.lua new file mode 100644 index 0000000000..2e0d809150 --- /dev/null +++ b/BuiltInPlugins/ConvertToPackage/Packages/UILibrary/_internal/join.lua @@ -0,0 +1,15 @@ +local function join(...) + local new = {} + + for i = 1, select("#", ...) do + local source = select(i, ...) + + for key, value in pairs(source) do + new[key] = value + end + end + + return new +end + +return join \ No newline at end of file diff --git a/BuiltInPlugins/ConvertToPackage/Src/Thunks/UploadConvertToPackageRequest.lua b/BuiltInPlugins/ConvertToPackage/Src/Thunks/UploadConvertToPackageRequest.lua index 8c2824dbec..3ea508a260 100644 --- a/BuiltInPlugins/ConvertToPackage/Src/Thunks/UploadConvertToPackageRequest.lua +++ b/BuiltInPlugins/ConvertToPackage/Src/Thunks/UploadConvertToPackageRequest.lua @@ -2,6 +2,8 @@ This file is responsible for handling the interaction between Studio and the Lua plugin for converting an object to a package. ]] +local FFlagFixConvertToPackageHang = game:DefineFastFlag("FixConvertToPackageHang", false) + local Plugin = script.Parent.Parent.Parent local Constants = require(Plugin.Src.Util.Constants) @@ -10,13 +12,17 @@ local Actions = Plugin.Src.Actions local NetworkError = require(Actions.NetworkError) local SetCurrentScreen = require(Actions.SetCurrentScreen) local UploadResult = require(Actions.UploadResult) + +-- Remove with FFlagFixConvertToPackageHang local Promise = require(Plugin.Packages.Http.Promise) local Urls = require(Plugin.Src.Util.Urls) local StudioService = game:GetService("StudioService") +-- Remove with FFlagFixConvertToPackageHang local function createConvertToPackageUploadPromise(urlToUse) + assert(not FFlagFixConvertToPackageHang) local uploadPromise = Promise.new(function(resolve, reject) spawn(function() local result, errorMessage = StudioService.OnConvertToPackageResult:wait() @@ -40,17 +46,35 @@ end -- groupId, number, default to nil return function(assetid, name, description, genreTypeID, ispublic, allowComments, groupId) return function(store) + -- Remove with FFlagFixConvertToPackageHang local function onSuccess() + assert(not FFlagFixConvertToPackageHang) store:dispatch(UploadResult(true)) end + -- Remove with FFlagFixConvertToPackageHang local function onFailure(errorMessage) + assert(not FFlagFixConvertToPackageHang) store:dispatch(UploadResult(false)) store:dispatch(NetworkError(errorMessage, "uploadRequest")) end store:dispatch(SetCurrentScreen(Constants.SCREENS.UPLOADING_ASSET)) local urlToUse = Urls.constructPostUploadAssetUrl(assetid, "Model", name or "", description or "", genreTypeID, ispublic, allowComments, groupId) - return createConvertToPackageUploadPromise(urlToUse):andThen(onSuccess, onFailure) + + if FFlagFixConvertToPackageHang then + local conn; conn = StudioService.OnConvertToPackageResult:Connect(function(result, errorMessage) + conn:Disconnect() + store:dispatch(UploadResult(result)) + if errorMessage then + store:dispatch(NetworkError(errorMessage, "uploadRequest")) + end + return + end) + StudioService:ConvertToPackageUpload(urlToUse) + return + else + return createConvertToPackageUploadPromise(urlToUse):andThen(onSuccess, onFailure) + end end end diff --git a/BuiltInPlugins/DraftsWidget/Packages/UILibrary/_internal/Utils/Symbol.spec.lua b/BuiltInPlugins/DraftsWidget/Packages/UILibrary/_internal/Utils/Symbol.spec.lua index cde9be065b..f3312055c9 100644 --- a/BuiltInPlugins/DraftsWidget/Packages/UILibrary/_internal/Utils/Symbol.spec.lua +++ b/BuiltInPlugins/DraftsWidget/Packages/UILibrary/_internal/Utils/Symbol.spec.lua @@ -11,7 +11,7 @@ return function() it("should coerce to the given name", function() local symbol = Symbol.named("foo") - expect(tostring(symbol):find("foo")).to.be.ok() + expect(tostring(symbol):match("foo")).to.be.ok() end) it("should be unique when constructed", function() @@ -42,4 +42,4 @@ return function() expect(symbolA).never.to.equal(symbolB) end) end) -end \ No newline at end of file +end diff --git a/BuiltInPlugins/EventEmulator/Packages/Framework/ContextServices/Theme.spec.lua b/BuiltInPlugins/EventEmulator/Packages/Framework/ContextServices/Theme.spec.lua index 22377dc00b..1547fb3c11 100644 --- a/BuiltInPlugins/EventEmulator/Packages/Framework/ContextServices/Theme.spec.lua +++ b/BuiltInPlugins/EventEmulator/Packages/Framework/ContextServices/Theme.spec.lua @@ -11,6 +11,13 @@ return function() local StyleTable = Util.StyleTable local StyleModifier = Util.StyleModifier local Signal = Util.Signal + local FlagsList = Util.Flags.new({ + FFlagRefactorDevFrameworkTheme = {"RefactorDevFrameworkTheme"}, + }) + + if FlagsList:get("FFlagRefactorDevFrameworkTheme") then + return + end local function createTestThemedComponent(render) local TestThemedComponent = Roact.PureComponent:extend("TestThemedComponent") diff --git a/BuiltInPlugins/EventEmulator/Packages/Framework/ContextServices/UILibraryWrapper.spec.lua b/BuiltInPlugins/EventEmulator/Packages/Framework/ContextServices/UILibraryWrapper.spec.lua index 0823cb2280..d4bbb673db 100644 --- a/BuiltInPlugins/EventEmulator/Packages/Framework/ContextServices/UILibraryWrapper.spec.lua +++ b/BuiltInPlugins/EventEmulator/Packages/Framework/ContextServices/UILibraryWrapper.spec.lua @@ -2,8 +2,10 @@ return function() local Framework = script.Parent.Parent local Util = require(Framework.Util) + local Signal = Util.Signal local FlagsList = Util.Flags.new({ FFlagStudioDevFrameworkPackage = {"StudioDevFrameworkPackage"}, + FFlagRefactorDevFrameworkTheme = {"RefactorDevFrameworkTheme"}, }) local isUsedAsPackage = require(Framework.Util.isUsedAsPackage) @@ -18,6 +20,7 @@ return function() local Focus = require(Framework.ContextServices.Focus) local Theme = require(Framework.ContextServices.Theme) local UILibraryWrapper = require(script.Parent.UILibraryWrapper) + local StudioTheme = require(Framework.Style.Themes.StudioTheme) local WrapperStub = Roact.PureComponent:extend("UILibraryWrapper") @@ -29,12 +32,26 @@ return function() Wrapper = WrapperStub } + local function createThemeMock() + + if FlagsList:get("FFlagRefactorDevFrameworkTheme") then + return StudioTheme.mock() + else + local createStyles = function() + return {} + end + local getTheme = function() + return "Light" + end + local themeChanged = Signal.new() + return Theme.mock(createStyles, getTheme, themeChanged) + end + end + it("should expect a Plugin ContextItem provided above", function() local focus = Focus.new(Instance.new("ScreenGui")) local wrapper = UILibraryWrapper.new(UILibraryStub) - local theme = Theme.new(function() - return {} - end) + local theme = createThemeMock() function theme:getUILibraryTheme() return {} end @@ -55,9 +72,7 @@ return function() it("should expect a Focus ContextItem provided above", function() local plugin = Plugin.new({}) local wrapper = UILibraryWrapper.new(UILibraryStub) - local theme = Theme.new(function() - return {} - end) + local theme = createThemeMock() function theme:getUILibraryTheme() return {} end @@ -97,9 +112,7 @@ return function() local plugin = Plugin.new({}) local focus = Focus.new(Instance.new("ScreenGui")) local wrapper = UILibraryWrapper.new(UILibraryStub) - local theme = Theme.new(function() - return {} - end) + local theme = createThemeMock() local element = provide({ plugin, @@ -119,9 +132,7 @@ return function() local plugin = Plugin.new({}) local focus = Focus.new(Instance.new("ScreenGui")) local wrapper = UILibraryWrapper.new(UILibraryStub) - local theme = Theme.new(function() - return {} - end) + local theme = createThemeMock() function theme:getUILibraryTheme() return {} end diff --git a/BuiltInPlugins/EventEmulator/Packages/Framework/StudioUI/TitledFrame/test.spec.lua b/BuiltInPlugins/EventEmulator/Packages/Framework/StudioUI/TitledFrame/test.spec.lua index cff37efea8..f013a391bc 100644 --- a/BuiltInPlugins/EventEmulator/Packages/Framework/StudioUI/TitledFrame/test.spec.lua +++ b/BuiltInPlugins/EventEmulator/Packages/Framework/StudioUI/TitledFrame/test.spec.lua @@ -17,7 +17,7 @@ return function() local function createTestTitledFrame(children, container) local theme if FlagsList:get("FFlagRefactorDevFrameworkTheme") then - theme = StudioTheme.new() + theme = StudioTheme.mock() else theme = Theme.new(function() return { diff --git a/BuiltInPlugins/EventEmulator/Packages/Framework/StudioUI/createPluginWidget.lua b/BuiltInPlugins/EventEmulator/Packages/Framework/StudioUI/createPluginWidget.lua index 85620982d3..b9dd6d3759 100644 --- a/BuiltInPlugins/EventEmulator/Packages/Framework/StudioUI/createPluginWidget.lua +++ b/BuiltInPlugins/EventEmulator/Packages/Framework/StudioUI/createPluginWidget.lua @@ -9,10 +9,12 @@ ]] game:DefineFastFlag("FixDevFrameworkDockWidgetRestore", false) -game:DefineFastFlag("DevFrameworkPluginWidgetEnabledEvent", false) +game:DefineFastFlag("DevFrameworkPluginWidgetEnabledEvent2", false) +game:DefineFastFlag("DevFrameworkPluginWidgetUseSiblingZIndex", false) local FFlagFixDevFrameworkDockWidgetRestore = game:GetFastFlag("FixDevFrameworkDockWidgetRestore") -local FFlagDevFrameworkPluginWidgetEnabledEvent = game:GetFastFlag("DevFrameworkPluginWidgetEnabledEvent") +local FFlagDevFrameworkPluginWidgetEnabledEvent2 = game:GetFastFlag("DevFrameworkPluginWidgetEnabledEvent2") +local FFlagDevFrameworkPluginWidgetUseSiblingZIndex = game:GetFastFlag("DevFrameworkPluginWidgetUseSiblingZIndex") local Framework = script.Parent.Parent local Roact = require(Framework.Parent.Roact) @@ -31,7 +33,11 @@ local function createPluginWidget(componentName, createWidgetFunc) local widget = createWidgetFunc(props) widget.Name = title or "" - widget.ZIndexBehavior = props.ZIndexBehavior or Enum.ZIndexBehavior.Global + if FFlagDevFrameworkPluginWidgetUseSiblingZIndex then + widget.ZIndexBehavior = props.ZIndexBehavior or Enum.ZIndexBehavior.Sibling + else + widget.ZIndexBehavior = props.ZIndexBehavior or Enum.ZIndexBehavior.Global + end if widget:IsA("PluginGui") then widget:BindToClose(onClose) @@ -67,12 +73,13 @@ local function createPluginWidget(componentName, createWidgetFunc) end end - if FFlagDevFrameworkPluginWidgetEnabledEvent then + if FFlagDevFrameworkPluginWidgetEnabledEvent2 then -- Connect to enabled changing *after* restore -- Otherwise users of this will get 2 enabled changes: one from the onRestore, and the same from Roact.Change.Enabled self.widgetEnabledChangedConnection = widget:GetPropertyChangedSignal("Enabled"):Connect(function() - if self.props[Roact.Change.Enabled] then - self.props[Roact.Change.Enabled](self.widget) + local callback = self.props[Roact.Change.Enabled] + if callback and self.widget and self.widget.Enabled ~= self.props.Enabled then + callback(self.widget) end end) end diff --git a/BuiltInPlugins/EventEmulator/Packages/Framework/Style/Stylizer.lua b/BuiltInPlugins/EventEmulator/Packages/Framework/Style/Stylizer.lua index 47a60c5663..228c4b165a 100644 --- a/BuiltInPlugins/EventEmulator/Packages/Framework/Style/Stylizer.lua +++ b/BuiltInPlugins/EventEmulator/Packages/Framework/Style/Stylizer.lua @@ -1,3 +1,5 @@ +--!nocheck + --[[ Wraps theme styles and update logic into a ContextItem. @@ -318,4 +320,4 @@ function Stylizer.mock(t, themeProps, callback) return self end -return Stylizer \ No newline at end of file +return Stylizer diff --git a/BuiltInPlugins/EventEmulator/Packages/Framework/Style/getRawComponentStyle.lua b/BuiltInPlugins/EventEmulator/Packages/Framework/Style/getRawComponentStyle.lua index 7e659ca0c0..5c76de757c 100644 --- a/BuiltInPlugins/EventEmulator/Packages/Framework/Style/getRawComponentStyle.lua +++ b/BuiltInPlugins/EventEmulator/Packages/Framework/Style/getRawComponentStyle.lua @@ -9,6 +9,7 @@ local UIFolderData = require(Framework.UI.UIFolderData) return function(componentName) local componentData = UIFolderData[componentName] or StudioUIFolderData[componentName] local result + if componentData.style then result = require(componentData.style) end diff --git a/BuiltInPlugins/EventEmulator/Packages/Framework/TestHelpers/provideMockContext.lua b/BuiltInPlugins/EventEmulator/Packages/Framework/TestHelpers/provideMockContext.lua index 04cc85664e..10633a0dc7 100644 --- a/BuiltInPlugins/EventEmulator/Packages/Framework/TestHelpers/provideMockContext.lua +++ b/BuiltInPlugins/EventEmulator/Packages/Framework/TestHelpers/provideMockContext.lua @@ -91,7 +91,7 @@ return function(contextItemsList, children) -- Theme local theme if FlagsList:get("FFlagRefactorDevFrameworkTheme") then - theme = StudioTheme.new() + theme = StudioTheme.mock() else theme = ContextServices.Theme.mock(function(theme, getColor) return { diff --git a/BuiltInPlugins/EventEmulator/Packages/Framework/UI/Box/test.spec.lua b/BuiltInPlugins/EventEmulator/Packages/Framework/UI/Box/test.spec.lua index 27baa93e01..edc3b7c984 100644 --- a/BuiltInPlugins/EventEmulator/Packages/Framework/UI/Box/test.spec.lua +++ b/BuiltInPlugins/EventEmulator/Packages/Framework/UI/Box/test.spec.lua @@ -23,7 +23,7 @@ return function() else local theme if FlagsList:get("FFlagRefactorDevFrameworkTheme") then - theme = StudioTheme.new() + theme = StudioTheme.mock() else theme = Theme.new(function() return { diff --git a/BuiltInPlugins/EventEmulator/Packages/Framework/UI/Button/test.spec.lua b/BuiltInPlugins/EventEmulator/Packages/Framework/UI/Button/test.spec.lua index 3ffc122f23..cafabfaa40 100644 --- a/BuiltInPlugins/EventEmulator/Packages/Framework/UI/Button/test.spec.lua +++ b/BuiltInPlugins/EventEmulator/Packages/Framework/UI/Button/test.spec.lua @@ -18,7 +18,7 @@ return function() local function createTestButton(props, children) local theme if FlagsList:get("FFlagRefactorDevFrameworkTheme") then - theme = StudioTheme.new() + theme = StudioTheme.mock() else theme = Theme.new(function() return { diff --git a/BuiltInPlugins/EventEmulator/Packages/Framework/UI/Container/test.spec.lua b/BuiltInPlugins/EventEmulator/Packages/Framework/UI/Container/test.spec.lua index 21e7c8874a..1417100db0 100644 --- a/BuiltInPlugins/EventEmulator/Packages/Framework/UI/Container/test.spec.lua +++ b/BuiltInPlugins/EventEmulator/Packages/Framework/UI/Container/test.spec.lua @@ -18,7 +18,7 @@ return function() local function createTestContainer(props, children) local theme if FlagsList:get("FFlagRefactorDevFrameworkTheme") then - theme = StudioTheme.new() + theme = StudioTheme.mock() else theme = Theme.new(function() return { diff --git a/BuiltInPlugins/EventEmulator/Packages/Framework/UI/DropdownMenu.lua b/BuiltInPlugins/EventEmulator/Packages/Framework/UI/DropdownMenu.lua index 60f9fadd0c..b1e6b3c757 100644 --- a/BuiltInPlugins/EventEmulator/Packages/Framework/UI/DropdownMenu.lua +++ b/BuiltInPlugins/EventEmulator/Packages/Framework/UI/DropdownMenu.lua @@ -44,6 +44,7 @@ local TextLabel = require(UI.TextLabel) local FlagsList = Util.Flags.new({ FFlagRefactorDevFrameworkTheme = {"RefactorDevFrameworkTheme"}, + FFlagRefactorDevFrameworkContextItems = {"RefactorDevFrameworkContextItems"}, }) local DropdownMenu = Roact.PureComponent:extend("DropdownMenu") @@ -201,7 +202,7 @@ function DropdownMenu:renderMenu() local width = style.Width local offset = prioritize(style.Offset, Vector2.new(0, 0)) - local pluginGui = props.Focus:getTarget() + local pluginGui = FlagsList:get("FFlagRefactorDevFrameworkContextItems") and props.Focus:get() or props.Focus:getTarget() local menuPositionAndSize = self.getPositionAndSize(pluginGui, width, offset) local x = menuPositionAndSize.X diff --git a/BuiltInPlugins/EventEmulator/Packages/Framework/UI/FakeLoadingBar/test.spec.lua b/BuiltInPlugins/EventEmulator/Packages/Framework/UI/FakeLoadingBar/test.spec.lua index 4533d1b2a4..1306596adc 100644 --- a/BuiltInPlugins/EventEmulator/Packages/Framework/UI/FakeLoadingBar/test.spec.lua +++ b/BuiltInPlugins/EventEmulator/Packages/Framework/UI/FakeLoadingBar/test.spec.lua @@ -17,7 +17,7 @@ return function() local function createTestFakeLoadingBar(props, children) local theme if FlagsList:get("FFlagRefactorDevFrameworkTheme") then - theme = StudioTheme.new() + theme = StudioTheme.mock() else theme = Theme.new(function() return { diff --git a/BuiltInPlugins/EventEmulator/Packages/Framework/UI/Image/test.spec.lua b/BuiltInPlugins/EventEmulator/Packages/Framework/UI/Image/test.spec.lua index c5aa507a3a..6860be79dd 100644 --- a/BuiltInPlugins/EventEmulator/Packages/Framework/UI/Image/test.spec.lua +++ b/BuiltInPlugins/EventEmulator/Packages/Framework/UI/Image/test.spec.lua @@ -17,7 +17,7 @@ return function() local function createTestImageDecoration() local theme if FlagsList:get("FFlagRefactorDevFrameworkTheme") then - theme = StudioTheme.new() + theme = StudioTheme.mock() else theme = Theme.new(function() return { diff --git a/BuiltInPlugins/EventEmulator/Packages/Framework/UI/InstanceTreeView/InstanceTreeRow.lua b/BuiltInPlugins/EventEmulator/Packages/Framework/UI/InstanceTreeView/InstanceTreeRow.lua index 8000e57785..6110490a54 100644 --- a/BuiltInPlugins/EventEmulator/Packages/Framework/UI/InstanceTreeView/InstanceTreeRow.lua +++ b/BuiltInPlugins/EventEmulator/Packages/Framework/UI/InstanceTreeView/InstanceTreeRow.lua @@ -25,7 +25,7 @@ local TextLabel = require(UI.TextLabel) Typecheck.wrap(InstanceTreeRow, script) local function getClassIcon(instance) - local StudioService = game:GetService("StudioService") + local StudioService = game:GetService("StudioService") local className = instance.ClassName if instance.IsA then if instance:IsA("JointInstance") and className == "ManualWeld" or className == "ManualGlue" then @@ -73,9 +73,10 @@ function InstanceTreeRow:render() local arrowSize = style.Arrow.Size local padding = style.IconPadding local iconInfo = getClassIcon(item) - local iconSize = iconInfo.ImageRectSize.X + -- Default iconSize to (0, 0) as ImageRectSize is unavailable in Roblox CLI + local iconSize = iconInfo.ImageRectSize or Vector2.new() local labelOffset = indent + arrowSize + 2 * padding - local textOffset = iconSize + 3 * padding + local textOffset = iconSize.X + 3 * padding return Roact.createElement(Container, { Size = UDim2.new(1, -indent, 0, style.RowHeight), @@ -105,10 +106,10 @@ function InstanceTreeRow:render() Size = UDim2.new(1, -arrowSize, 1, 0), }, { Icon = Roact.createElement("ImageLabel", { - Size = UDim2.fromOffset(iconSize, iconInfo.ImageRectSize.Y), + Size = UDim2.fromOffset(iconSize.X, iconSize.Y), BackgroundTransparency = 1, Image = iconInfo.Image, - ImageRectSize = iconInfo.ImageRectSize, + ImageRectSize = iconSize, ImageRectOffset = iconInfo.ImageRectOffset, Position = UDim2.new(0, padding, 0.5, 0), AnchorPoint = Vector2.new(0, 0.5) diff --git a/BuiltInPlugins/EventEmulator/Packages/Framework/UI/LinkText/test.spec.lua b/BuiltInPlugins/EventEmulator/Packages/Framework/UI/LinkText/test.spec.lua index d72d172041..37cecf30b1 100644 --- a/BuiltInPlugins/EventEmulator/Packages/Framework/UI/LinkText/test.spec.lua +++ b/BuiltInPlugins/EventEmulator/Packages/Framework/UI/LinkText/test.spec.lua @@ -19,7 +19,7 @@ return function() local mouse = Mouse.new({}) local theme if FlagsList:get("FFlagRefactorDevFrameworkTheme") then - theme = StudioTheme.new() + theme = StudioTheme.mock() else theme = Theme.new(function() return { diff --git a/BuiltInPlugins/EventEmulator/Packages/Framework/UI/LoadingBar/test.spec.lua b/BuiltInPlugins/EventEmulator/Packages/Framework/UI/LoadingBar/test.spec.lua index ea437b136c..c898cde0f4 100644 --- a/BuiltInPlugins/EventEmulator/Packages/Framework/UI/LoadingBar/test.spec.lua +++ b/BuiltInPlugins/EventEmulator/Packages/Framework/UI/LoadingBar/test.spec.lua @@ -18,7 +18,7 @@ return function() local function createTestLoadingBar(props, children) local theme if FlagsList:get("FFlagRefactorDevFrameworkTheme") then - theme = StudioTheme.new() + theme = StudioTheme.mock() else theme = Theme.new(function() return { diff --git a/BuiltInPlugins/EventEmulator/Packages/Framework/UI/RadioButton/test.spec.lua b/BuiltInPlugins/EventEmulator/Packages/Framework/UI/RadioButton/test.spec.lua index 31e2c12c77..4e578229a9 100644 --- a/BuiltInPlugins/EventEmulator/Packages/Framework/UI/RadioButton/test.spec.lua +++ b/BuiltInPlugins/EventEmulator/Packages/Framework/UI/RadioButton/test.spec.lua @@ -23,7 +23,7 @@ return function() local function createTestRadioButton(props, children) local theme if FlagsList:get("FFlagRefactorDevFrameworkTheme") then - theme = StudioTheme.new() + theme = StudioTheme.mock() else theme = Theme.new(function() return { diff --git a/BuiltInPlugins/EventEmulator/Packages/Framework/UI/RoundBox/test.spec.lua b/BuiltInPlugins/EventEmulator/Packages/Framework/UI/RoundBox/test.spec.lua index 10b16f79a0..d9715cf163 100644 --- a/BuiltInPlugins/EventEmulator/Packages/Framework/UI/RoundBox/test.spec.lua +++ b/BuiltInPlugins/EventEmulator/Packages/Framework/UI/RoundBox/test.spec.lua @@ -17,7 +17,7 @@ return function() local function createTestRoundBoxDecoration() local theme if FlagsList:get("FFlagRefactorDevFrameworkTheme") then - theme = StudioTheme.new() + theme = StudioTheme.mock() else theme = Theme.new(function() return { diff --git a/BuiltInPlugins/EventEmulator/Packages/Framework/UI/TextLabel/test.spec.lua b/BuiltInPlugins/EventEmulator/Packages/Framework/UI/TextLabel/test.spec.lua index a403d8b7d3..5f739cb55f 100644 --- a/BuiltInPlugins/EventEmulator/Packages/Framework/UI/TextLabel/test.spec.lua +++ b/BuiltInPlugins/EventEmulator/Packages/Framework/UI/TextLabel/test.spec.lua @@ -17,7 +17,7 @@ return function() local function createTestTextLabelDecoration() local theme if FlagsList:get("FFlagRefactorDevFrameworkTheme") then - theme = StudioTheme.new() + theme = StudioTheme.mock() else theme = Theme.new(function() return { diff --git a/BuiltInPlugins/EventEmulator/Packages/Framework/UI/ToggleButton/test.spec.lua b/BuiltInPlugins/EventEmulator/Packages/Framework/UI/ToggleButton/test.spec.lua index 0d712bdf45..c4db80d801 100644 --- a/BuiltInPlugins/EventEmulator/Packages/Framework/UI/ToggleButton/test.spec.lua +++ b/BuiltInPlugins/EventEmulator/Packages/Framework/UI/ToggleButton/test.spec.lua @@ -27,7 +27,7 @@ return function() local mouse = Mouse.new({}) local theme if FlagsList:get("FFlagRefactorDevFrameworkTheme") then - theme = StudioTheme.new() + theme = StudioTheme.mock() else theme = Theme.new(function() return { diff --git a/BuiltInPlugins/EventEmulator/Packages/Framework/UI/Tooltip/test.spec.lua b/BuiltInPlugins/EventEmulator/Packages/Framework/UI/Tooltip/test.spec.lua index 984329d7eb..8f7a0a6fc9 100644 --- a/BuiltInPlugins/EventEmulator/Packages/Framework/UI/Tooltip/test.spec.lua +++ b/BuiltInPlugins/EventEmulator/Packages/Framework/UI/Tooltip/test.spec.lua @@ -25,7 +25,7 @@ return function() local focus = Focus.new(target) local theme if FlagsList:get("FFlagRefactorDevFrameworkTheme") then - theme = StudioTheme.new() + theme = StudioTheme.mock() else theme = Theme.new(function() return { diff --git a/BuiltInPlugins/EventEmulator/Packages/Framework/UI/TreeView/test.spec.lua b/BuiltInPlugins/EventEmulator/Packages/Framework/UI/TreeView/test.spec.lua index 953aef3483..8d06ee8a86 100644 --- a/BuiltInPlugins/EventEmulator/Packages/Framework/UI/TreeView/test.spec.lua +++ b/BuiltInPlugins/EventEmulator/Packages/Framework/UI/TreeView/test.spec.lua @@ -63,7 +63,7 @@ return function() local function createTreeView() local theme if FlagsList:get("FFlagRefactorDevFrameworkTheme") then - theme = StudioTheme.new() + theme = StudioTheme.mock() else theme = Theme.new(function() return { diff --git a/BuiltInPlugins/EventEmulator/Packages/Framework/Util.lua b/BuiltInPlugins/EventEmulator/Packages/Framework/Util.lua index 5454c404f1..6a3e12c089 100644 --- a/BuiltInPlugins/EventEmulator/Packages/Framework/Util.lua +++ b/BuiltInPlugins/EventEmulator/Packages/Framework/Util.lua @@ -19,6 +19,7 @@ return strict({ FitFrame = require(script.FitFrame), Flags = require(script.Flags), + getTestVariation = require(script.getTestVariation), Immutable = require(script.Immutable), LayoutOrderIterator = require(script.LayoutOrderIterator), Math = require(script.Math), diff --git a/BuiltInPlugins/EventEmulator/Packages/Framework/Util/CrossPluginCommunication.lua b/BuiltInPlugins/EventEmulator/Packages/Framework/Util/CrossPluginCommunication.lua index 37caa1bece..d3b82f5783 100644 --- a/BuiltInPlugins/EventEmulator/Packages/Framework/Util/CrossPluginCommunication.lua +++ b/BuiltInPlugins/EventEmulator/Packages/Framework/Util/CrossPluginCommunication.lua @@ -22,10 +22,12 @@ CrossPluginCommunication.__index = CrossPluginCommunication CrossPluginCommunication.BASE_FOLDER_NAME = "CrossPluginCommunication" -function CrossPluginCommunication.new(pluginNamespace) +function CrossPluginCommunication.new(pluginNamespace, hostService) assert(t.string(pluginNamespace), "pluginNamespace must be a string") + assert(t.optional(t.instance(pluginNamespace)), "hostService must be an instance if defined") local self = { + hostService = hostService or RobloxPluginGuiService, pluginNamespace = pluginNamespace } @@ -49,7 +51,7 @@ function CrossPluginCommunication:ensureFolderExists(parent, name) end function CrossPluginCommunication:getNamespaceFolder() - local base = self:ensureFolderExists(RobloxPluginGuiService, self.BASE_FOLDER_NAME) + local base = self:ensureFolderExists(self.hostService, self.BASE_FOLDER_NAME) return self:ensureFolderExists(base, self.pluginNamespace) end diff --git a/BuiltInPlugins/EventEmulator/Packages/Framework/Util/CrossPluginCommunication.spec.lua b/BuiltInPlugins/EventEmulator/Packages/Framework/Util/CrossPluginCommunication.spec.lua index 60338f2b0c..f3c1035f50 100644 --- a/BuiltInPlugins/EventEmulator/Packages/Framework/Util/CrossPluginCommunication.spec.lua +++ b/BuiltInPlugins/EventEmulator/Packages/Framework/Util/CrossPluginCommunication.spec.lua @@ -1,5 +1,3 @@ -local RobloxPluginGuiService = game:GetService("RobloxPluginGuiService") - local Framework = script.Parent.Parent local CrossPluginCommunication = require(Framework.Util.CrossPluginCommunication) @@ -8,8 +6,10 @@ return function() -- TODO DEVTOOLS-4397: Move cleanup to afterEach hook after the dependency structure is fixed + local hostService = workspace + local function createTestComms() - return CrossPluginCommunication.new(TEST_NAMESPACE) + return CrossPluginCommunication.new(TEST_NAMESPACE, hostService) end describe("Invoke", function() @@ -32,7 +32,7 @@ return function() comms:OnInvoke("blah", function() end) - local namespace = RobloxPluginGuiService:FindFirstChild(CrossPluginCommunication.BASE_FOLDER_NAME):FindFirstChild(TEST_NAMESPACE) + local namespace = hostService:FindFirstChild(CrossPluginCommunication.BASE_FOLDER_NAME):FindFirstChild(TEST_NAMESPACE) assert(namespace) diff --git a/BuiltInPlugins/EventEmulator/Packages/Framework/Util/Palette.spec.lua b/BuiltInPlugins/EventEmulator/Packages/Framework/Util/Palette.spec.lua index 8794c80717..02911b17c7 100644 --- a/BuiltInPlugins/EventEmulator/Packages/Framework/Util/Palette.spec.lua +++ b/BuiltInPlugins/EventEmulator/Packages/Framework/Util/Palette.spec.lua @@ -1,5 +1,16 @@ return function() - local Palette = require(script.Parent.Palette) + + local Util = script.Parent + local Flags = require(Util.Flags) + local FlagsList = Flags.new({ + FFlagRefactorDevFrameworkTheme = {"RefactorDevFrameworkTheme"}, + }) + + if FlagsList:get("FFlagRefactorDevFrameworkTheme") then + return + end + + local Palette = require(Util.Palette) it("should be a table of color StyleValues", function() expect(Palette).to.be.ok() diff --git a/BuiltInPlugins/EventEmulator/Packages/Framework/Util/Typecheck/t.lua b/BuiltInPlugins/EventEmulator/Packages/Framework/Util/Typecheck/t.lua index 44c8d50af8..045d7ac0dd 100644 --- a/BuiltInPlugins/EventEmulator/Packages/Framework/Util/Typecheck/t.lua +++ b/BuiltInPlugins/EventEmulator/Packages/Framework/Util/Typecheck/t.lua @@ -1,3 +1,5 @@ +--!nocheck + --[[ DeveloperFramework uses t from Osyris https://github.com/osyrisrblx/t diff --git a/BuiltInPlugins/EventEmulator/Packages/Framework/Util/getTestVariation.lua b/BuiltInPlugins/EventEmulator/Packages/Framework/Util/getTestVariation.lua new file mode 100644 index 0000000000..d09873db1d --- /dev/null +++ b/BuiltInPlugins/EventEmulator/Packages/Framework/Util/getTestVariation.lua @@ -0,0 +1,30 @@ +-- This function hides ABTestService away and provides a quick interface for getting a test's variation. +-- Should the requested test not exist, it will return the Control group. + +local ABTestService = game:GetService("ABTestService") + +local AB_TEST_GROUP_CONTROL = "Control" + +-- abTestName : (string) the name of test. ex) "AllUsers.Studio.ExampleTestName" +-- abTestService : (table, optional) an optional override for ABTestService +-- RETURNS : (int) the variation that the user is in. 0 = Control +return function(abTestName, abTestService) + assert(type(abTestName) == "string", "Expected abTestName to be a string") + if abTestService then + assert(type(abTestService.GetVariant) == "function", "Expected the abTestService object to have a GetVariant member function") + else + abTestService = ABTestService + end + + local variation = abTestService:GetVariant(abTestName) + if variation == AB_TEST_GROUP_CONTROL then + return 0 + else + local _, _, variationNumber = string.find(variation, "Variation(%d+)") + if not variationNumber then + return 0 + end + + return tonumber(variationNumber) + end +end diff --git a/BuiltInPlugins/EventEmulator/Packages/Framework/Util/getTestVariation.spec.lua b/BuiltInPlugins/EventEmulator/Packages/Framework/Util/getTestVariation.spec.lua new file mode 100644 index 0000000000..5febceee91 --- /dev/null +++ b/BuiltInPlugins/EventEmulator/Packages/Framework/Util/getTestVariation.spec.lua @@ -0,0 +1,41 @@ +return function() + local getTestVariation = require(script.Parent.getTestVariation) + local VARIATION_NAME_CONTROL = "Control" + + local function createTestService(result) + local DebugABService = {} + + function DebugABService:GetVariant(testName) + return result + end + + return DebugABService + end + + it("should expect some test name", function() + expect(function() + getTestVariation() + end).to.throw() + end) + + it("should return zero when in the control group", function() + local debugABTestService = createTestService(VARIATION_NAME_CONTROL) + local variation = getTestVariation("AllUsers.Studio.ShouldShowZero", debugABTestService) + expect(variation).to.equal(0) + end) + + it("should return the number of the variation when not in the control group", function() + local debugABTestService = createTestService("Variation1") + local variation = getTestVariation("AllUsers.Studio.ShouldShowNumber", debugABTestService) + expect(variation).to.equal(1) + + -- there aren't any variations past 8, but it should work with any number + debugABTestService = createTestService("Variation23") + variation = getTestVariation("AllUsers.Studio.ShouldShowNumber", debugABTestService) + expect(variation).to.equal(23) + + debugABTestService = createTestService("Variation456") + variation = getTestVariation("AllUsers.Studio.ShouldShowNumber", debugABTestService) + expect(variation).to.equal(456) + end) +end \ No newline at end of file diff --git a/BuiltInPlugins/FrameworkCompanion/Bin/main.server.lua b/BuiltInPlugins/FrameworkCompanion/Bin/main.server.lua index f36d64a5e2..b2d4f44b13 100644 --- a/BuiltInPlugins/FrameworkCompanion/Bin/main.server.lua +++ b/BuiltInPlugins/FrameworkCompanion/Bin/main.server.lua @@ -4,40 +4,22 @@ ]] local FFlagDebugEnableDevFrameworkCompanion = game:DefineFastFlag("DebugEnableDevFrameworkCompanion", false) -if not game:GetService("StudioService"):HasInternalPermission() and not FFlagDebugEnableDevFrameworkCompanion then - return -end local Plugin = script.Parent.Parent -local Roact = require(Plugin.Packages.Roact) ---[[ - Since the symlink for DeveloperFramework as a path dependency is not - checked in, we need to ensure the DeveloperFramework folder is in the correct - location in the package index. We do this by including the folder in the Rojo - config and then moving it to the correct location at runtime here. We remove - any existing DeveloperFramework folder which may be present as a result of - rotrieve install being ran locally and creating a gitignored symlink. -]] -local function devFrameworkLoadingPatch() - local existingPackage = Plugin.Packages._Index.DeveloperFramework:FindFirstChild("DeveloperFramework") - if existingPackage then - existingPackage:Destroy() - end - local frameworkFolder = Plugin.Packages.DEPRECATED_Framework - frameworkFolder.Name = "DeveloperFramework" - frameworkFolder.Parent = Plugin.Packages._Index.DeveloperFramework +local DebugFlags = require(Plugin.Src.Util.DebugFlags) +if DebugFlags.RunningUnderCLI() then + return end --- TODO DEVTOOLS-4458: Replace this with Rotriever & Rojo sub-project linking solution -devFrameworkLoadingPatch() +if not game:GetService("StudioService"):HasInternalPermission() and not FFlagDebugEnableDevFrameworkCompanion then + return +end -Roact.setGlobalConfig({ - elementTracing = true, - propValidation = true, - typeChecks = true -}) +local commonInit = require(Plugin.Src.Util.commonInit) +commonInit() +local Roact = require(Plugin.Packages.Roact) local MainPlugin = require(Plugin.Src.MainPlugin) local handle diff --git a/BuiltInPlugins/FrameworkCompanion/Bin/runTests.server.lua b/BuiltInPlugins/FrameworkCompanion/Bin/runTests.server.lua index 3ccdde2616..1da0c799d4 100644 --- a/BuiltInPlugins/FrameworkCompanion/Bin/runTests.server.lua +++ b/BuiltInPlugins/FrameworkCompanion/Bin/runTests.server.lua @@ -1,16 +1,41 @@ --- Note: Make sure this boolean is false for production code. --- Code reviewer: if you see this variable as true, say something! -local SHOULD_RUN_TESTS = false +local FFlagRefactorDevFrameworkTheme = game:GetFastFlag("RefactorDevFrameworkTheme") -if SHOULD_RUN_TESTS then - local Plugin = script.Parent.Parent - local TestsFolderPlugin = Plugin.Src +local Plugin = script.Parent.Parent +local commonInit = require(Plugin.Src.Util.commonInit) +commonInit() + +local Framework = require(Plugin.Packages.Framework) +local DebugFlags = require(Plugin.Src.Util.DebugFlags) +if DebugFlags.RunningUnderCLI() or DebugFlags.RunTests() then + -- Requiring TestEZ initialises TestService, so we require it under the condition local TestEZ = require(Plugin.Packages.Dev.TestEZ) local TestBootstrap = TestEZ.TestBootstrap local TextReporter = TestEZ.Reporters.TextReporter + local TestsFolderPlugin = Plugin.Src + + if FFlagRefactorDevFrameworkTheme then + print("----- All " .. Plugin.Name .. " Tests ------") + TestBootstrap:run({TestsFolderPlugin}, TextReporter) + print("----------------------------------") + + if DebugFlags.RunDeveloperFrameworkTests() then + print("") + print("----- All DeveloperFramework Tests ------") + Framework.TestHelpers.runFrameworkTests(TestEZ) + print("----------------------------------") + end + else + --[[ + We do not support mocking the old theme system. Skip tests rather than refactoring it + due to its impending removal. + ]] + print("Skipping tests due to run without FFlagRefactorDevFrameworkTheme") + end +end - print("----- All DevFramework Companion Tests ------") - TestBootstrap:run(TestsFolderPlugin, TextReporter) - print("----------------------------------") +if DebugFlags.RunningUnderCLI() then + pcall(function() + game:GetService("ProcessService"):ExitAsync(0) + end) end diff --git a/BuiltInPlugins/FrameworkCompanion/Packages/DEPRECATED_Framework/ContextServices/Theme.spec.lua b/BuiltInPlugins/FrameworkCompanion/Packages/DEPRECATED_Framework/ContextServices/Theme.spec.lua index 22377dc00b..1547fb3c11 100644 --- a/BuiltInPlugins/FrameworkCompanion/Packages/DEPRECATED_Framework/ContextServices/Theme.spec.lua +++ b/BuiltInPlugins/FrameworkCompanion/Packages/DEPRECATED_Framework/ContextServices/Theme.spec.lua @@ -11,6 +11,13 @@ return function() local StyleTable = Util.StyleTable local StyleModifier = Util.StyleModifier local Signal = Util.Signal + local FlagsList = Util.Flags.new({ + FFlagRefactorDevFrameworkTheme = {"RefactorDevFrameworkTheme"}, + }) + + if FlagsList:get("FFlagRefactorDevFrameworkTheme") then + return + end local function createTestThemedComponent(render) local TestThemedComponent = Roact.PureComponent:extend("TestThemedComponent") diff --git a/BuiltInPlugins/FrameworkCompanion/Packages/DEPRECATED_Framework/ContextServices/UILibraryWrapper.spec.lua b/BuiltInPlugins/FrameworkCompanion/Packages/DEPRECATED_Framework/ContextServices/UILibraryWrapper.spec.lua index 0823cb2280..d4bbb673db 100644 --- a/BuiltInPlugins/FrameworkCompanion/Packages/DEPRECATED_Framework/ContextServices/UILibraryWrapper.spec.lua +++ b/BuiltInPlugins/FrameworkCompanion/Packages/DEPRECATED_Framework/ContextServices/UILibraryWrapper.spec.lua @@ -2,8 +2,10 @@ return function() local Framework = script.Parent.Parent local Util = require(Framework.Util) + local Signal = Util.Signal local FlagsList = Util.Flags.new({ FFlagStudioDevFrameworkPackage = {"StudioDevFrameworkPackage"}, + FFlagRefactorDevFrameworkTheme = {"RefactorDevFrameworkTheme"}, }) local isUsedAsPackage = require(Framework.Util.isUsedAsPackage) @@ -18,6 +20,7 @@ return function() local Focus = require(Framework.ContextServices.Focus) local Theme = require(Framework.ContextServices.Theme) local UILibraryWrapper = require(script.Parent.UILibraryWrapper) + local StudioTheme = require(Framework.Style.Themes.StudioTheme) local WrapperStub = Roact.PureComponent:extend("UILibraryWrapper") @@ -29,12 +32,26 @@ return function() Wrapper = WrapperStub } + local function createThemeMock() + + if FlagsList:get("FFlagRefactorDevFrameworkTheme") then + return StudioTheme.mock() + else + local createStyles = function() + return {} + end + local getTheme = function() + return "Light" + end + local themeChanged = Signal.new() + return Theme.mock(createStyles, getTheme, themeChanged) + end + end + it("should expect a Plugin ContextItem provided above", function() local focus = Focus.new(Instance.new("ScreenGui")) local wrapper = UILibraryWrapper.new(UILibraryStub) - local theme = Theme.new(function() - return {} - end) + local theme = createThemeMock() function theme:getUILibraryTheme() return {} end @@ -55,9 +72,7 @@ return function() it("should expect a Focus ContextItem provided above", function() local plugin = Plugin.new({}) local wrapper = UILibraryWrapper.new(UILibraryStub) - local theme = Theme.new(function() - return {} - end) + local theme = createThemeMock() function theme:getUILibraryTheme() return {} end @@ -97,9 +112,7 @@ return function() local plugin = Plugin.new({}) local focus = Focus.new(Instance.new("ScreenGui")) local wrapper = UILibraryWrapper.new(UILibraryStub) - local theme = Theme.new(function() - return {} - end) + local theme = createThemeMock() local element = provide({ plugin, @@ -119,9 +132,7 @@ return function() local plugin = Plugin.new({}) local focus = Focus.new(Instance.new("ScreenGui")) local wrapper = UILibraryWrapper.new(UILibraryStub) - local theme = Theme.new(function() - return {} - end) + local theme = createThemeMock() function theme:getUILibraryTheme() return {} end diff --git a/BuiltInPlugins/FrameworkCompanion/Packages/DEPRECATED_Framework/StudioUI/TitledFrame/test.spec.lua b/BuiltInPlugins/FrameworkCompanion/Packages/DEPRECATED_Framework/StudioUI/TitledFrame/test.spec.lua index cff37efea8..f013a391bc 100644 --- a/BuiltInPlugins/FrameworkCompanion/Packages/DEPRECATED_Framework/StudioUI/TitledFrame/test.spec.lua +++ b/BuiltInPlugins/FrameworkCompanion/Packages/DEPRECATED_Framework/StudioUI/TitledFrame/test.spec.lua @@ -17,7 +17,7 @@ return function() local function createTestTitledFrame(children, container) local theme if FlagsList:get("FFlagRefactorDevFrameworkTheme") then - theme = StudioTheme.new() + theme = StudioTheme.mock() else theme = Theme.new(function() return { diff --git a/BuiltInPlugins/FrameworkCompanion/Packages/DEPRECATED_Framework/StudioUI/createPluginWidget.lua b/BuiltInPlugins/FrameworkCompanion/Packages/DEPRECATED_Framework/StudioUI/createPluginWidget.lua index 85620982d3..b9dd6d3759 100644 --- a/BuiltInPlugins/FrameworkCompanion/Packages/DEPRECATED_Framework/StudioUI/createPluginWidget.lua +++ b/BuiltInPlugins/FrameworkCompanion/Packages/DEPRECATED_Framework/StudioUI/createPluginWidget.lua @@ -9,10 +9,12 @@ ]] game:DefineFastFlag("FixDevFrameworkDockWidgetRestore", false) -game:DefineFastFlag("DevFrameworkPluginWidgetEnabledEvent", false) +game:DefineFastFlag("DevFrameworkPluginWidgetEnabledEvent2", false) +game:DefineFastFlag("DevFrameworkPluginWidgetUseSiblingZIndex", false) local FFlagFixDevFrameworkDockWidgetRestore = game:GetFastFlag("FixDevFrameworkDockWidgetRestore") -local FFlagDevFrameworkPluginWidgetEnabledEvent = game:GetFastFlag("DevFrameworkPluginWidgetEnabledEvent") +local FFlagDevFrameworkPluginWidgetEnabledEvent2 = game:GetFastFlag("DevFrameworkPluginWidgetEnabledEvent2") +local FFlagDevFrameworkPluginWidgetUseSiblingZIndex = game:GetFastFlag("DevFrameworkPluginWidgetUseSiblingZIndex") local Framework = script.Parent.Parent local Roact = require(Framework.Parent.Roact) @@ -31,7 +33,11 @@ local function createPluginWidget(componentName, createWidgetFunc) local widget = createWidgetFunc(props) widget.Name = title or "" - widget.ZIndexBehavior = props.ZIndexBehavior or Enum.ZIndexBehavior.Global + if FFlagDevFrameworkPluginWidgetUseSiblingZIndex then + widget.ZIndexBehavior = props.ZIndexBehavior or Enum.ZIndexBehavior.Sibling + else + widget.ZIndexBehavior = props.ZIndexBehavior or Enum.ZIndexBehavior.Global + end if widget:IsA("PluginGui") then widget:BindToClose(onClose) @@ -67,12 +73,13 @@ local function createPluginWidget(componentName, createWidgetFunc) end end - if FFlagDevFrameworkPluginWidgetEnabledEvent then + if FFlagDevFrameworkPluginWidgetEnabledEvent2 then -- Connect to enabled changing *after* restore -- Otherwise users of this will get 2 enabled changes: one from the onRestore, and the same from Roact.Change.Enabled self.widgetEnabledChangedConnection = widget:GetPropertyChangedSignal("Enabled"):Connect(function() - if self.props[Roact.Change.Enabled] then - self.props[Roact.Change.Enabled](self.widget) + local callback = self.props[Roact.Change.Enabled] + if callback and self.widget and self.widget.Enabled ~= self.props.Enabled then + callback(self.widget) end end) end diff --git a/BuiltInPlugins/FrameworkCompanion/Packages/DEPRECATED_Framework/Style/Stylizer.lua b/BuiltInPlugins/FrameworkCompanion/Packages/DEPRECATED_Framework/Style/Stylizer.lua index 47a60c5663..228c4b165a 100644 --- a/BuiltInPlugins/FrameworkCompanion/Packages/DEPRECATED_Framework/Style/Stylizer.lua +++ b/BuiltInPlugins/FrameworkCompanion/Packages/DEPRECATED_Framework/Style/Stylizer.lua @@ -1,3 +1,5 @@ +--!nocheck + --[[ Wraps theme styles and update logic into a ContextItem. @@ -318,4 +320,4 @@ function Stylizer.mock(t, themeProps, callback) return self end -return Stylizer \ No newline at end of file +return Stylizer diff --git a/BuiltInPlugins/FrameworkCompanion/Packages/DEPRECATED_Framework/Style/getRawComponentStyle.lua b/BuiltInPlugins/FrameworkCompanion/Packages/DEPRECATED_Framework/Style/getRawComponentStyle.lua index 7e659ca0c0..5c76de757c 100644 --- a/BuiltInPlugins/FrameworkCompanion/Packages/DEPRECATED_Framework/Style/getRawComponentStyle.lua +++ b/BuiltInPlugins/FrameworkCompanion/Packages/DEPRECATED_Framework/Style/getRawComponentStyle.lua @@ -9,6 +9,7 @@ local UIFolderData = require(Framework.UI.UIFolderData) return function(componentName) local componentData = UIFolderData[componentName] or StudioUIFolderData[componentName] local result + if componentData.style then result = require(componentData.style) end diff --git a/BuiltInPlugins/FrameworkCompanion/Packages/DEPRECATED_Framework/TestHelpers/provideMockContext.lua b/BuiltInPlugins/FrameworkCompanion/Packages/DEPRECATED_Framework/TestHelpers/provideMockContext.lua index 04cc85664e..10633a0dc7 100644 --- a/BuiltInPlugins/FrameworkCompanion/Packages/DEPRECATED_Framework/TestHelpers/provideMockContext.lua +++ b/BuiltInPlugins/FrameworkCompanion/Packages/DEPRECATED_Framework/TestHelpers/provideMockContext.lua @@ -91,7 +91,7 @@ return function(contextItemsList, children) -- Theme local theme if FlagsList:get("FFlagRefactorDevFrameworkTheme") then - theme = StudioTheme.new() + theme = StudioTheme.mock() else theme = ContextServices.Theme.mock(function(theme, getColor) return { diff --git a/BuiltInPlugins/FrameworkCompanion/Packages/DEPRECATED_Framework/UI/Box/test.spec.lua b/BuiltInPlugins/FrameworkCompanion/Packages/DEPRECATED_Framework/UI/Box/test.spec.lua index 27baa93e01..edc3b7c984 100644 --- a/BuiltInPlugins/FrameworkCompanion/Packages/DEPRECATED_Framework/UI/Box/test.spec.lua +++ b/BuiltInPlugins/FrameworkCompanion/Packages/DEPRECATED_Framework/UI/Box/test.spec.lua @@ -23,7 +23,7 @@ return function() else local theme if FlagsList:get("FFlagRefactorDevFrameworkTheme") then - theme = StudioTheme.new() + theme = StudioTheme.mock() else theme = Theme.new(function() return { diff --git a/BuiltInPlugins/FrameworkCompanion/Packages/DEPRECATED_Framework/UI/Button/test.spec.lua b/BuiltInPlugins/FrameworkCompanion/Packages/DEPRECATED_Framework/UI/Button/test.spec.lua index 3ffc122f23..cafabfaa40 100644 --- a/BuiltInPlugins/FrameworkCompanion/Packages/DEPRECATED_Framework/UI/Button/test.spec.lua +++ b/BuiltInPlugins/FrameworkCompanion/Packages/DEPRECATED_Framework/UI/Button/test.spec.lua @@ -18,7 +18,7 @@ return function() local function createTestButton(props, children) local theme if FlagsList:get("FFlagRefactorDevFrameworkTheme") then - theme = StudioTheme.new() + theme = StudioTheme.mock() else theme = Theme.new(function() return { diff --git a/BuiltInPlugins/FrameworkCompanion/Packages/DEPRECATED_Framework/UI/Container/test.spec.lua b/BuiltInPlugins/FrameworkCompanion/Packages/DEPRECATED_Framework/UI/Container/test.spec.lua index 21e7c8874a..1417100db0 100644 --- a/BuiltInPlugins/FrameworkCompanion/Packages/DEPRECATED_Framework/UI/Container/test.spec.lua +++ b/BuiltInPlugins/FrameworkCompanion/Packages/DEPRECATED_Framework/UI/Container/test.spec.lua @@ -18,7 +18,7 @@ return function() local function createTestContainer(props, children) local theme if FlagsList:get("FFlagRefactorDevFrameworkTheme") then - theme = StudioTheme.new() + theme = StudioTheme.mock() else theme = Theme.new(function() return { diff --git a/BuiltInPlugins/FrameworkCompanion/Packages/DEPRECATED_Framework/UI/DropdownMenu.lua b/BuiltInPlugins/FrameworkCompanion/Packages/DEPRECATED_Framework/UI/DropdownMenu.lua index 60f9fadd0c..b1e6b3c757 100644 --- a/BuiltInPlugins/FrameworkCompanion/Packages/DEPRECATED_Framework/UI/DropdownMenu.lua +++ b/BuiltInPlugins/FrameworkCompanion/Packages/DEPRECATED_Framework/UI/DropdownMenu.lua @@ -44,6 +44,7 @@ local TextLabel = require(UI.TextLabel) local FlagsList = Util.Flags.new({ FFlagRefactorDevFrameworkTheme = {"RefactorDevFrameworkTheme"}, + FFlagRefactorDevFrameworkContextItems = {"RefactorDevFrameworkContextItems"}, }) local DropdownMenu = Roact.PureComponent:extend("DropdownMenu") @@ -201,7 +202,7 @@ function DropdownMenu:renderMenu() local width = style.Width local offset = prioritize(style.Offset, Vector2.new(0, 0)) - local pluginGui = props.Focus:getTarget() + local pluginGui = FlagsList:get("FFlagRefactorDevFrameworkContextItems") and props.Focus:get() or props.Focus:getTarget() local menuPositionAndSize = self.getPositionAndSize(pluginGui, width, offset) local x = menuPositionAndSize.X diff --git a/BuiltInPlugins/FrameworkCompanion/Packages/DEPRECATED_Framework/UI/FakeLoadingBar/test.spec.lua b/BuiltInPlugins/FrameworkCompanion/Packages/DEPRECATED_Framework/UI/FakeLoadingBar/test.spec.lua index 4533d1b2a4..1306596adc 100644 --- a/BuiltInPlugins/FrameworkCompanion/Packages/DEPRECATED_Framework/UI/FakeLoadingBar/test.spec.lua +++ b/BuiltInPlugins/FrameworkCompanion/Packages/DEPRECATED_Framework/UI/FakeLoadingBar/test.spec.lua @@ -17,7 +17,7 @@ return function() local function createTestFakeLoadingBar(props, children) local theme if FlagsList:get("FFlagRefactorDevFrameworkTheme") then - theme = StudioTheme.new() + theme = StudioTheme.mock() else theme = Theme.new(function() return { diff --git a/BuiltInPlugins/FrameworkCompanion/Packages/DEPRECATED_Framework/UI/Image/test.spec.lua b/BuiltInPlugins/FrameworkCompanion/Packages/DEPRECATED_Framework/UI/Image/test.spec.lua index c5aa507a3a..6860be79dd 100644 --- a/BuiltInPlugins/FrameworkCompanion/Packages/DEPRECATED_Framework/UI/Image/test.spec.lua +++ b/BuiltInPlugins/FrameworkCompanion/Packages/DEPRECATED_Framework/UI/Image/test.spec.lua @@ -17,7 +17,7 @@ return function() local function createTestImageDecoration() local theme if FlagsList:get("FFlagRefactorDevFrameworkTheme") then - theme = StudioTheme.new() + theme = StudioTheme.mock() else theme = Theme.new(function() return { diff --git a/BuiltInPlugins/FrameworkCompanion/Packages/DEPRECATED_Framework/UI/InstanceTreeView/InstanceTreeRow.lua b/BuiltInPlugins/FrameworkCompanion/Packages/DEPRECATED_Framework/UI/InstanceTreeView/InstanceTreeRow.lua index 8000e57785..6110490a54 100644 --- a/BuiltInPlugins/FrameworkCompanion/Packages/DEPRECATED_Framework/UI/InstanceTreeView/InstanceTreeRow.lua +++ b/BuiltInPlugins/FrameworkCompanion/Packages/DEPRECATED_Framework/UI/InstanceTreeView/InstanceTreeRow.lua @@ -25,7 +25,7 @@ local TextLabel = require(UI.TextLabel) Typecheck.wrap(InstanceTreeRow, script) local function getClassIcon(instance) - local StudioService = game:GetService("StudioService") + local StudioService = game:GetService("StudioService") local className = instance.ClassName if instance.IsA then if instance:IsA("JointInstance") and className == "ManualWeld" or className == "ManualGlue" then @@ -73,9 +73,10 @@ function InstanceTreeRow:render() local arrowSize = style.Arrow.Size local padding = style.IconPadding local iconInfo = getClassIcon(item) - local iconSize = iconInfo.ImageRectSize.X + -- Default iconSize to (0, 0) as ImageRectSize is unavailable in Roblox CLI + local iconSize = iconInfo.ImageRectSize or Vector2.new() local labelOffset = indent + arrowSize + 2 * padding - local textOffset = iconSize + 3 * padding + local textOffset = iconSize.X + 3 * padding return Roact.createElement(Container, { Size = UDim2.new(1, -indent, 0, style.RowHeight), @@ -105,10 +106,10 @@ function InstanceTreeRow:render() Size = UDim2.new(1, -arrowSize, 1, 0), }, { Icon = Roact.createElement("ImageLabel", { - Size = UDim2.fromOffset(iconSize, iconInfo.ImageRectSize.Y), + Size = UDim2.fromOffset(iconSize.X, iconSize.Y), BackgroundTransparency = 1, Image = iconInfo.Image, - ImageRectSize = iconInfo.ImageRectSize, + ImageRectSize = iconSize, ImageRectOffset = iconInfo.ImageRectOffset, Position = UDim2.new(0, padding, 0.5, 0), AnchorPoint = Vector2.new(0, 0.5) diff --git a/BuiltInPlugins/FrameworkCompanion/Packages/DEPRECATED_Framework/UI/LinkText/test.spec.lua b/BuiltInPlugins/FrameworkCompanion/Packages/DEPRECATED_Framework/UI/LinkText/test.spec.lua index d72d172041..37cecf30b1 100644 --- a/BuiltInPlugins/FrameworkCompanion/Packages/DEPRECATED_Framework/UI/LinkText/test.spec.lua +++ b/BuiltInPlugins/FrameworkCompanion/Packages/DEPRECATED_Framework/UI/LinkText/test.spec.lua @@ -19,7 +19,7 @@ return function() local mouse = Mouse.new({}) local theme if FlagsList:get("FFlagRefactorDevFrameworkTheme") then - theme = StudioTheme.new() + theme = StudioTheme.mock() else theme = Theme.new(function() return { diff --git a/BuiltInPlugins/FrameworkCompanion/Packages/DEPRECATED_Framework/UI/LoadingBar/test.spec.lua b/BuiltInPlugins/FrameworkCompanion/Packages/DEPRECATED_Framework/UI/LoadingBar/test.spec.lua index ea437b136c..c898cde0f4 100644 --- a/BuiltInPlugins/FrameworkCompanion/Packages/DEPRECATED_Framework/UI/LoadingBar/test.spec.lua +++ b/BuiltInPlugins/FrameworkCompanion/Packages/DEPRECATED_Framework/UI/LoadingBar/test.spec.lua @@ -18,7 +18,7 @@ return function() local function createTestLoadingBar(props, children) local theme if FlagsList:get("FFlagRefactorDevFrameworkTheme") then - theme = StudioTheme.new() + theme = StudioTheme.mock() else theme = Theme.new(function() return { diff --git a/BuiltInPlugins/FrameworkCompanion/Packages/DEPRECATED_Framework/UI/RadioButton/test.spec.lua b/BuiltInPlugins/FrameworkCompanion/Packages/DEPRECATED_Framework/UI/RadioButton/test.spec.lua index 31e2c12c77..4e578229a9 100644 --- a/BuiltInPlugins/FrameworkCompanion/Packages/DEPRECATED_Framework/UI/RadioButton/test.spec.lua +++ b/BuiltInPlugins/FrameworkCompanion/Packages/DEPRECATED_Framework/UI/RadioButton/test.spec.lua @@ -23,7 +23,7 @@ return function() local function createTestRadioButton(props, children) local theme if FlagsList:get("FFlagRefactorDevFrameworkTheme") then - theme = StudioTheme.new() + theme = StudioTheme.mock() else theme = Theme.new(function() return { diff --git a/BuiltInPlugins/FrameworkCompanion/Packages/DEPRECATED_Framework/UI/RoundBox/test.spec.lua b/BuiltInPlugins/FrameworkCompanion/Packages/DEPRECATED_Framework/UI/RoundBox/test.spec.lua index 10b16f79a0..d9715cf163 100644 --- a/BuiltInPlugins/FrameworkCompanion/Packages/DEPRECATED_Framework/UI/RoundBox/test.spec.lua +++ b/BuiltInPlugins/FrameworkCompanion/Packages/DEPRECATED_Framework/UI/RoundBox/test.spec.lua @@ -17,7 +17,7 @@ return function() local function createTestRoundBoxDecoration() local theme if FlagsList:get("FFlagRefactorDevFrameworkTheme") then - theme = StudioTheme.new() + theme = StudioTheme.mock() else theme = Theme.new(function() return { diff --git a/BuiltInPlugins/FrameworkCompanion/Packages/DEPRECATED_Framework/UI/TextLabel/test.spec.lua b/BuiltInPlugins/FrameworkCompanion/Packages/DEPRECATED_Framework/UI/TextLabel/test.spec.lua index a403d8b7d3..5f739cb55f 100644 --- a/BuiltInPlugins/FrameworkCompanion/Packages/DEPRECATED_Framework/UI/TextLabel/test.spec.lua +++ b/BuiltInPlugins/FrameworkCompanion/Packages/DEPRECATED_Framework/UI/TextLabel/test.spec.lua @@ -17,7 +17,7 @@ return function() local function createTestTextLabelDecoration() local theme if FlagsList:get("FFlagRefactorDevFrameworkTheme") then - theme = StudioTheme.new() + theme = StudioTheme.mock() else theme = Theme.new(function() return { diff --git a/BuiltInPlugins/FrameworkCompanion/Packages/DEPRECATED_Framework/UI/ToggleButton/test.spec.lua b/BuiltInPlugins/FrameworkCompanion/Packages/DEPRECATED_Framework/UI/ToggleButton/test.spec.lua index 0d712bdf45..c4db80d801 100644 --- a/BuiltInPlugins/FrameworkCompanion/Packages/DEPRECATED_Framework/UI/ToggleButton/test.spec.lua +++ b/BuiltInPlugins/FrameworkCompanion/Packages/DEPRECATED_Framework/UI/ToggleButton/test.spec.lua @@ -27,7 +27,7 @@ return function() local mouse = Mouse.new({}) local theme if FlagsList:get("FFlagRefactorDevFrameworkTheme") then - theme = StudioTheme.new() + theme = StudioTheme.mock() else theme = Theme.new(function() return { diff --git a/BuiltInPlugins/FrameworkCompanion/Packages/DEPRECATED_Framework/UI/Tooltip/test.spec.lua b/BuiltInPlugins/FrameworkCompanion/Packages/DEPRECATED_Framework/UI/Tooltip/test.spec.lua index 984329d7eb..8f7a0a6fc9 100644 --- a/BuiltInPlugins/FrameworkCompanion/Packages/DEPRECATED_Framework/UI/Tooltip/test.spec.lua +++ b/BuiltInPlugins/FrameworkCompanion/Packages/DEPRECATED_Framework/UI/Tooltip/test.spec.lua @@ -25,7 +25,7 @@ return function() local focus = Focus.new(target) local theme if FlagsList:get("FFlagRefactorDevFrameworkTheme") then - theme = StudioTheme.new() + theme = StudioTheme.mock() else theme = Theme.new(function() return { diff --git a/BuiltInPlugins/FrameworkCompanion/Packages/DEPRECATED_Framework/UI/TreeView/test.spec.lua b/BuiltInPlugins/FrameworkCompanion/Packages/DEPRECATED_Framework/UI/TreeView/test.spec.lua index 953aef3483..8d06ee8a86 100644 --- a/BuiltInPlugins/FrameworkCompanion/Packages/DEPRECATED_Framework/UI/TreeView/test.spec.lua +++ b/BuiltInPlugins/FrameworkCompanion/Packages/DEPRECATED_Framework/UI/TreeView/test.spec.lua @@ -63,7 +63,7 @@ return function() local function createTreeView() local theme if FlagsList:get("FFlagRefactorDevFrameworkTheme") then - theme = StudioTheme.new() + theme = StudioTheme.mock() else theme = Theme.new(function() return { diff --git a/BuiltInPlugins/FrameworkCompanion/Packages/DEPRECATED_Framework/Util.lua b/BuiltInPlugins/FrameworkCompanion/Packages/DEPRECATED_Framework/Util.lua index 5454c404f1..6a3e12c089 100644 --- a/BuiltInPlugins/FrameworkCompanion/Packages/DEPRECATED_Framework/Util.lua +++ b/BuiltInPlugins/FrameworkCompanion/Packages/DEPRECATED_Framework/Util.lua @@ -19,6 +19,7 @@ return strict({ FitFrame = require(script.FitFrame), Flags = require(script.Flags), + getTestVariation = require(script.getTestVariation), Immutable = require(script.Immutable), LayoutOrderIterator = require(script.LayoutOrderIterator), Math = require(script.Math), diff --git a/BuiltInPlugins/FrameworkCompanion/Packages/DEPRECATED_Framework/Util/CrossPluginCommunication.lua b/BuiltInPlugins/FrameworkCompanion/Packages/DEPRECATED_Framework/Util/CrossPluginCommunication.lua index 37caa1bece..d3b82f5783 100644 --- a/BuiltInPlugins/FrameworkCompanion/Packages/DEPRECATED_Framework/Util/CrossPluginCommunication.lua +++ b/BuiltInPlugins/FrameworkCompanion/Packages/DEPRECATED_Framework/Util/CrossPluginCommunication.lua @@ -22,10 +22,12 @@ CrossPluginCommunication.__index = CrossPluginCommunication CrossPluginCommunication.BASE_FOLDER_NAME = "CrossPluginCommunication" -function CrossPluginCommunication.new(pluginNamespace) +function CrossPluginCommunication.new(pluginNamespace, hostService) assert(t.string(pluginNamespace), "pluginNamespace must be a string") + assert(t.optional(t.instance(pluginNamespace)), "hostService must be an instance if defined") local self = { + hostService = hostService or RobloxPluginGuiService, pluginNamespace = pluginNamespace } @@ -49,7 +51,7 @@ function CrossPluginCommunication:ensureFolderExists(parent, name) end function CrossPluginCommunication:getNamespaceFolder() - local base = self:ensureFolderExists(RobloxPluginGuiService, self.BASE_FOLDER_NAME) + local base = self:ensureFolderExists(self.hostService, self.BASE_FOLDER_NAME) return self:ensureFolderExists(base, self.pluginNamespace) end diff --git a/BuiltInPlugins/FrameworkCompanion/Packages/DEPRECATED_Framework/Util/CrossPluginCommunication.spec.lua b/BuiltInPlugins/FrameworkCompanion/Packages/DEPRECATED_Framework/Util/CrossPluginCommunication.spec.lua index 60338f2b0c..f3c1035f50 100644 --- a/BuiltInPlugins/FrameworkCompanion/Packages/DEPRECATED_Framework/Util/CrossPluginCommunication.spec.lua +++ b/BuiltInPlugins/FrameworkCompanion/Packages/DEPRECATED_Framework/Util/CrossPluginCommunication.spec.lua @@ -1,5 +1,3 @@ -local RobloxPluginGuiService = game:GetService("RobloxPluginGuiService") - local Framework = script.Parent.Parent local CrossPluginCommunication = require(Framework.Util.CrossPluginCommunication) @@ -8,8 +6,10 @@ return function() -- TODO DEVTOOLS-4397: Move cleanup to afterEach hook after the dependency structure is fixed + local hostService = workspace + local function createTestComms() - return CrossPluginCommunication.new(TEST_NAMESPACE) + return CrossPluginCommunication.new(TEST_NAMESPACE, hostService) end describe("Invoke", function() @@ -32,7 +32,7 @@ return function() comms:OnInvoke("blah", function() end) - local namespace = RobloxPluginGuiService:FindFirstChild(CrossPluginCommunication.BASE_FOLDER_NAME):FindFirstChild(TEST_NAMESPACE) + local namespace = hostService:FindFirstChild(CrossPluginCommunication.BASE_FOLDER_NAME):FindFirstChild(TEST_NAMESPACE) assert(namespace) diff --git a/BuiltInPlugins/FrameworkCompanion/Packages/DEPRECATED_Framework/Util/Palette.spec.lua b/BuiltInPlugins/FrameworkCompanion/Packages/DEPRECATED_Framework/Util/Palette.spec.lua index 8794c80717..02911b17c7 100644 --- a/BuiltInPlugins/FrameworkCompanion/Packages/DEPRECATED_Framework/Util/Palette.spec.lua +++ b/BuiltInPlugins/FrameworkCompanion/Packages/DEPRECATED_Framework/Util/Palette.spec.lua @@ -1,5 +1,16 @@ return function() - local Palette = require(script.Parent.Palette) + + local Util = script.Parent + local Flags = require(Util.Flags) + local FlagsList = Flags.new({ + FFlagRefactorDevFrameworkTheme = {"RefactorDevFrameworkTheme"}, + }) + + if FlagsList:get("FFlagRefactorDevFrameworkTheme") then + return + end + + local Palette = require(Util.Palette) it("should be a table of color StyleValues", function() expect(Palette).to.be.ok() diff --git a/BuiltInPlugins/FrameworkCompanion/Packages/DEPRECATED_Framework/Util/Typecheck/t.lua b/BuiltInPlugins/FrameworkCompanion/Packages/DEPRECATED_Framework/Util/Typecheck/t.lua index 44c8d50af8..045d7ac0dd 100644 --- a/BuiltInPlugins/FrameworkCompanion/Packages/DEPRECATED_Framework/Util/Typecheck/t.lua +++ b/BuiltInPlugins/FrameworkCompanion/Packages/DEPRECATED_Framework/Util/Typecheck/t.lua @@ -1,3 +1,5 @@ +--!nocheck + --[[ DeveloperFramework uses t from Osyris https://github.com/osyrisrblx/t diff --git a/BuiltInPlugins/FrameworkCompanion/Packages/DEPRECATED_Framework/Util/getTestVariation.lua b/BuiltInPlugins/FrameworkCompanion/Packages/DEPRECATED_Framework/Util/getTestVariation.lua new file mode 100644 index 0000000000..d09873db1d --- /dev/null +++ b/BuiltInPlugins/FrameworkCompanion/Packages/DEPRECATED_Framework/Util/getTestVariation.lua @@ -0,0 +1,30 @@ +-- This function hides ABTestService away and provides a quick interface for getting a test's variation. +-- Should the requested test not exist, it will return the Control group. + +local ABTestService = game:GetService("ABTestService") + +local AB_TEST_GROUP_CONTROL = "Control" + +-- abTestName : (string) the name of test. ex) "AllUsers.Studio.ExampleTestName" +-- abTestService : (table, optional) an optional override for ABTestService +-- RETURNS : (int) the variation that the user is in. 0 = Control +return function(abTestName, abTestService) + assert(type(abTestName) == "string", "Expected abTestName to be a string") + if abTestService then + assert(type(abTestService.GetVariant) == "function", "Expected the abTestService object to have a GetVariant member function") + else + abTestService = ABTestService + end + + local variation = abTestService:GetVariant(abTestName) + if variation == AB_TEST_GROUP_CONTROL then + return 0 + else + local _, _, variationNumber = string.find(variation, "Variation(%d+)") + if not variationNumber then + return 0 + end + + return tonumber(variationNumber) + end +end diff --git a/BuiltInPlugins/FrameworkCompanion/Packages/DEPRECATED_Framework/Util/getTestVariation.spec.lua b/BuiltInPlugins/FrameworkCompanion/Packages/DEPRECATED_Framework/Util/getTestVariation.spec.lua new file mode 100644 index 0000000000..5febceee91 --- /dev/null +++ b/BuiltInPlugins/FrameworkCompanion/Packages/DEPRECATED_Framework/Util/getTestVariation.spec.lua @@ -0,0 +1,41 @@ +return function() + local getTestVariation = require(script.Parent.getTestVariation) + local VARIATION_NAME_CONTROL = "Control" + + local function createTestService(result) + local DebugABService = {} + + function DebugABService:GetVariant(testName) + return result + end + + return DebugABService + end + + it("should expect some test name", function() + expect(function() + getTestVariation() + end).to.throw() + end) + + it("should return zero when in the control group", function() + local debugABTestService = createTestService(VARIATION_NAME_CONTROL) + local variation = getTestVariation("AllUsers.Studio.ShouldShowZero", debugABTestService) + expect(variation).to.equal(0) + end) + + it("should return the number of the variation when not in the control group", function() + local debugABTestService = createTestService("Variation1") + local variation = getTestVariation("AllUsers.Studio.ShouldShowNumber", debugABTestService) + expect(variation).to.equal(1) + + -- there aren't any variations past 8, but it should work with any number + debugABTestService = createTestService("Variation23") + variation = getTestVariation("AllUsers.Studio.ShouldShowNumber", debugABTestService) + expect(variation).to.equal(23) + + debugABTestService = createTestService("Variation456") + variation = getTestVariation("AllUsers.Studio.ShouldShowNumber", debugABTestService) + expect(variation).to.equal(456) + end) +end \ No newline at end of file diff --git a/BuiltInPlugins/FrameworkCompanion/Src/Components/StylesList.spec.lua b/BuiltInPlugins/FrameworkCompanion/Src/Components/StylesList.spec.lua index f2273885cc..69f09280fa 100644 --- a/BuiltInPlugins/FrameworkCompanion/Src/Components/StylesList.spec.lua +++ b/BuiltInPlugins/FrameworkCompanion/Src/Components/StylesList.spec.lua @@ -7,6 +7,7 @@ return function() it("should create and destroy without errors", function() local element = MockWrap(Roact.createElement(StylesList, { Header = "Test", + ComponentName = "TextLabel", Styles = {}, })) local instance = Roact.mount(element) @@ -17,6 +18,7 @@ return function() local container = Instance.new("Folder") local element = MockWrap(Roact.createElement(StylesList, { Header = "Test", + ComponentName = "TextLabel", Styles = { Default = {}, }, @@ -36,6 +38,8 @@ return function() local container = Instance.new("Folder") local element = MockWrap(Roact.createElement(StylesList, { Header = "Test", + -- This can be any DeveloperFramework component with a '&' style override + ComponentName = "Button", Styles = { Default = {}, Item = {}, diff --git a/BuiltInPlugins/FrameworkCompanion/Src/MockWrap.lua b/BuiltInPlugins/FrameworkCompanion/Src/MockWrap.lua index a84097e654..54a445cb8a 100644 --- a/BuiltInPlugins/FrameworkCompanion/Src/MockWrap.lua +++ b/BuiltInPlugins/FrameworkCompanion/Src/MockWrap.lua @@ -12,6 +12,8 @@ local Plugin = script.Parent.Parent local Roact = require(Plugin.Packages.Roact) local Rodux = require(Plugin.Packages.Rodux) +local Framework = require(Plugin.Packages.Framework) +local Signal = Framework.Util.Signal local MainReducer = require(Plugin.Src.Reducers.MainReducer) local ContextServices = require(Plugin.Packages.Framework).ContextServices local MakeTheme = require(Plugin.Src.Resources.MakeTheme) @@ -24,6 +26,7 @@ end local function mockPlugin(container) local plugin = {} + plugin.Name = "FrameworkCompanionMock" function plugin:GetMouse() return {} end @@ -41,6 +44,11 @@ local function mockPlugin(container) function plugin:CreateDockWidgetPluginGui() return createScreenGui() end + function plugin:CreatePluginAction() + return { + Triggered = Signal.new() + } + end return plugin end @@ -55,8 +63,14 @@ end function MockPlugin:render() return ContextServices.provide({ ContextServices.Plugin.new(self.plugin), + ContextServices.PluginActions.new(self.plugin, { + { + id = "rerunLastStory", + text = "MOCK", + } + }), ContextServices.Mouse.new({}), - MakeTheme(), + MakeTheme(true), ContextServices.Focus.new(self.target), ContextServices.Store.new(self.store), }, self.props[Roact.Children]) diff --git a/BuiltInPlugins/FrameworkCompanion/Src/Resources/MakeTheme.lua b/BuiltInPlugins/FrameworkCompanion/Src/Resources/MakeTheme.lua index a657bb6ce6..00e23e432d 100644 --- a/BuiltInPlugins/FrameworkCompanion/Src/Resources/MakeTheme.lua +++ b/BuiltInPlugins/FrameworkCompanion/Src/Resources/MakeTheme.lua @@ -29,9 +29,16 @@ local BaseTheme = FrameworkStyle.Themes.BaseTheme local StudioTheme = FrameworkStyle.Themes.StudioTheme local ui = FrameworkStyle.ComponentSymbols -local function makeTheme() +local function makeTheme(shouldMock) if FlagsList:get("FFlagRefactorDevFrameworkTheme") then - local styleRoot = StudioTheme.new() + + local styleRoot + if shouldMock then + styleRoot = StudioTheme.mock() + else + styleRoot = StudioTheme.new() + end + return styleRoot:extend({ [StyleKey.TypeTextColor] = Color3.fromRGB(0, 162, 255), diff --git a/BuiltInPlugins/FrameworkCompanion/Src/Util/DebugFlags.lua b/BuiltInPlugins/FrameworkCompanion/Src/Util/DebugFlags.lua new file mode 100644 index 0000000000..82e89a4efd --- /dev/null +++ b/BuiltInPlugins/FrameworkCompanion/Src/Util/DebugFlags.lua @@ -0,0 +1,20 @@ +local Workspace = game:GetService("Workspace") + +local FLAGS_FOLDER = "FrameworkCompanionFlags" + +local function defineFlag(flagName, default) + default = default or false + return function() + local folder = Workspace:FindFirstChild(FLAGS_FOLDER) + if not folder or not folder:FindFirstChild(flagName) then + return default + end + return folder[flagName].Value + end +end + +local DebugFlags = {} +DebugFlags.RunDeveloperFrameworkTests = defineFlag("RunDeveloperFrameworkTests") +DebugFlags.RunningUnderCLI = defineFlag("RunningUnderCLI") +DebugFlags.RunTests = defineFlag("RunTests") +return DebugFlags diff --git a/BuiltInPlugins/FrameworkCompanion/Src/Util/commonInit.lua b/BuiltInPlugins/FrameworkCompanion/Src/Util/commonInit.lua new file mode 100644 index 0000000000..e09b76f53e --- /dev/null +++ b/BuiltInPlugins/FrameworkCompanion/Src/Util/commonInit.lua @@ -0,0 +1,41 @@ +--[[ + Performs common initialisation for FrameworkCompanion at most once. +]] +local commonInitCalled = false + +return function() + if commonInitCalled then + return + end + commonInitCalled = true + + local Plugin = script.Parent.Parent.Parent + local Roact = require(Plugin.Packages.Roact) + + --[[ + Since the symlink for DeveloperFramework as a path dependency is not + checked in, we need to ensure the DeveloperFramework folder is in the correct + location in the package index. We do this by including the folder in the Rojo + config and then moving it to the correct location at runtime here. We remove + any existing DeveloperFramework folder which may be present as a result of + rotrieve install being ran locally and creating a gitignored symlink. + ]] + local function devFrameworkLoadingPatch() + local existingPackage = Plugin.Packages._Index.DeveloperFramework:FindFirstChild("DeveloperFramework") + if existingPackage then + existingPackage:Destroy() + end + local frameworkFolder = Plugin.Packages.DEPRECATED_Framework + frameworkFolder.Name = "DeveloperFramework" + frameworkFolder.Parent = Plugin.Packages._Index.DeveloperFramework + end + + -- TODO DEVTOOLS-4458: Replace this with Rotriever & Rojo sub-project linking solution + devFrameworkLoadingPatch() + + Roact.setGlobalConfig({ + elementTracing = true, + propValidation = true, + typeChecks = true + }) +end \ No newline at end of file diff --git a/BuiltInPlugins/GameSettings/Framework/ContextServices/Theme.spec.lua b/BuiltInPlugins/GameSettings/Framework/ContextServices/Theme.spec.lua index 22377dc00b..1547fb3c11 100644 --- a/BuiltInPlugins/GameSettings/Framework/ContextServices/Theme.spec.lua +++ b/BuiltInPlugins/GameSettings/Framework/ContextServices/Theme.spec.lua @@ -11,6 +11,13 @@ return function() local StyleTable = Util.StyleTable local StyleModifier = Util.StyleModifier local Signal = Util.Signal + local FlagsList = Util.Flags.new({ + FFlagRefactorDevFrameworkTheme = {"RefactorDevFrameworkTheme"}, + }) + + if FlagsList:get("FFlagRefactorDevFrameworkTheme") then + return + end local function createTestThemedComponent(render) local TestThemedComponent = Roact.PureComponent:extend("TestThemedComponent") diff --git a/BuiltInPlugins/GameSettings/Framework/ContextServices/UILibraryWrapper.spec.lua b/BuiltInPlugins/GameSettings/Framework/ContextServices/UILibraryWrapper.spec.lua index 0823cb2280..d4bbb673db 100644 --- a/BuiltInPlugins/GameSettings/Framework/ContextServices/UILibraryWrapper.spec.lua +++ b/BuiltInPlugins/GameSettings/Framework/ContextServices/UILibraryWrapper.spec.lua @@ -2,8 +2,10 @@ return function() local Framework = script.Parent.Parent local Util = require(Framework.Util) + local Signal = Util.Signal local FlagsList = Util.Flags.new({ FFlagStudioDevFrameworkPackage = {"StudioDevFrameworkPackage"}, + FFlagRefactorDevFrameworkTheme = {"RefactorDevFrameworkTheme"}, }) local isUsedAsPackage = require(Framework.Util.isUsedAsPackage) @@ -18,6 +20,7 @@ return function() local Focus = require(Framework.ContextServices.Focus) local Theme = require(Framework.ContextServices.Theme) local UILibraryWrapper = require(script.Parent.UILibraryWrapper) + local StudioTheme = require(Framework.Style.Themes.StudioTheme) local WrapperStub = Roact.PureComponent:extend("UILibraryWrapper") @@ -29,12 +32,26 @@ return function() Wrapper = WrapperStub } + local function createThemeMock() + + if FlagsList:get("FFlagRefactorDevFrameworkTheme") then + return StudioTheme.mock() + else + local createStyles = function() + return {} + end + local getTheme = function() + return "Light" + end + local themeChanged = Signal.new() + return Theme.mock(createStyles, getTheme, themeChanged) + end + end + it("should expect a Plugin ContextItem provided above", function() local focus = Focus.new(Instance.new("ScreenGui")) local wrapper = UILibraryWrapper.new(UILibraryStub) - local theme = Theme.new(function() - return {} - end) + local theme = createThemeMock() function theme:getUILibraryTheme() return {} end @@ -55,9 +72,7 @@ return function() it("should expect a Focus ContextItem provided above", function() local plugin = Plugin.new({}) local wrapper = UILibraryWrapper.new(UILibraryStub) - local theme = Theme.new(function() - return {} - end) + local theme = createThemeMock() function theme:getUILibraryTheme() return {} end @@ -97,9 +112,7 @@ return function() local plugin = Plugin.new({}) local focus = Focus.new(Instance.new("ScreenGui")) local wrapper = UILibraryWrapper.new(UILibraryStub) - local theme = Theme.new(function() - return {} - end) + local theme = createThemeMock() local element = provide({ plugin, @@ -119,9 +132,7 @@ return function() local plugin = Plugin.new({}) local focus = Focus.new(Instance.new("ScreenGui")) local wrapper = UILibraryWrapper.new(UILibraryStub) - local theme = Theme.new(function() - return {} - end) + local theme = createThemeMock() function theme:getUILibraryTheme() return {} end diff --git a/BuiltInPlugins/GameSettings/Framework/StudioUI/TitledFrame/test.spec.lua b/BuiltInPlugins/GameSettings/Framework/StudioUI/TitledFrame/test.spec.lua index cff37efea8..f013a391bc 100644 --- a/BuiltInPlugins/GameSettings/Framework/StudioUI/TitledFrame/test.spec.lua +++ b/BuiltInPlugins/GameSettings/Framework/StudioUI/TitledFrame/test.spec.lua @@ -17,7 +17,7 @@ return function() local function createTestTitledFrame(children, container) local theme if FlagsList:get("FFlagRefactorDevFrameworkTheme") then - theme = StudioTheme.new() + theme = StudioTheme.mock() else theme = Theme.new(function() return { diff --git a/BuiltInPlugins/GameSettings/Framework/StudioUI/createPluginWidget.lua b/BuiltInPlugins/GameSettings/Framework/StudioUI/createPluginWidget.lua index 85620982d3..b9dd6d3759 100644 --- a/BuiltInPlugins/GameSettings/Framework/StudioUI/createPluginWidget.lua +++ b/BuiltInPlugins/GameSettings/Framework/StudioUI/createPluginWidget.lua @@ -9,10 +9,12 @@ ]] game:DefineFastFlag("FixDevFrameworkDockWidgetRestore", false) -game:DefineFastFlag("DevFrameworkPluginWidgetEnabledEvent", false) +game:DefineFastFlag("DevFrameworkPluginWidgetEnabledEvent2", false) +game:DefineFastFlag("DevFrameworkPluginWidgetUseSiblingZIndex", false) local FFlagFixDevFrameworkDockWidgetRestore = game:GetFastFlag("FixDevFrameworkDockWidgetRestore") -local FFlagDevFrameworkPluginWidgetEnabledEvent = game:GetFastFlag("DevFrameworkPluginWidgetEnabledEvent") +local FFlagDevFrameworkPluginWidgetEnabledEvent2 = game:GetFastFlag("DevFrameworkPluginWidgetEnabledEvent2") +local FFlagDevFrameworkPluginWidgetUseSiblingZIndex = game:GetFastFlag("DevFrameworkPluginWidgetUseSiblingZIndex") local Framework = script.Parent.Parent local Roact = require(Framework.Parent.Roact) @@ -31,7 +33,11 @@ local function createPluginWidget(componentName, createWidgetFunc) local widget = createWidgetFunc(props) widget.Name = title or "" - widget.ZIndexBehavior = props.ZIndexBehavior or Enum.ZIndexBehavior.Global + if FFlagDevFrameworkPluginWidgetUseSiblingZIndex then + widget.ZIndexBehavior = props.ZIndexBehavior or Enum.ZIndexBehavior.Sibling + else + widget.ZIndexBehavior = props.ZIndexBehavior or Enum.ZIndexBehavior.Global + end if widget:IsA("PluginGui") then widget:BindToClose(onClose) @@ -67,12 +73,13 @@ local function createPluginWidget(componentName, createWidgetFunc) end end - if FFlagDevFrameworkPluginWidgetEnabledEvent then + if FFlagDevFrameworkPluginWidgetEnabledEvent2 then -- Connect to enabled changing *after* restore -- Otherwise users of this will get 2 enabled changes: one from the onRestore, and the same from Roact.Change.Enabled self.widgetEnabledChangedConnection = widget:GetPropertyChangedSignal("Enabled"):Connect(function() - if self.props[Roact.Change.Enabled] then - self.props[Roact.Change.Enabled](self.widget) + local callback = self.props[Roact.Change.Enabled] + if callback and self.widget and self.widget.Enabled ~= self.props.Enabled then + callback(self.widget) end end) end diff --git a/BuiltInPlugins/GameSettings/Framework/Style/Stylizer.lua b/BuiltInPlugins/GameSettings/Framework/Style/Stylizer.lua index 47a60c5663..228c4b165a 100644 --- a/BuiltInPlugins/GameSettings/Framework/Style/Stylizer.lua +++ b/BuiltInPlugins/GameSettings/Framework/Style/Stylizer.lua @@ -1,3 +1,5 @@ +--!nocheck + --[[ Wraps theme styles and update logic into a ContextItem. @@ -318,4 +320,4 @@ function Stylizer.mock(t, themeProps, callback) return self end -return Stylizer \ No newline at end of file +return Stylizer diff --git a/BuiltInPlugins/GameSettings/Framework/Style/getRawComponentStyle.lua b/BuiltInPlugins/GameSettings/Framework/Style/getRawComponentStyle.lua index 7e659ca0c0..5c76de757c 100644 --- a/BuiltInPlugins/GameSettings/Framework/Style/getRawComponentStyle.lua +++ b/BuiltInPlugins/GameSettings/Framework/Style/getRawComponentStyle.lua @@ -9,6 +9,7 @@ local UIFolderData = require(Framework.UI.UIFolderData) return function(componentName) local componentData = UIFolderData[componentName] or StudioUIFolderData[componentName] local result + if componentData.style then result = require(componentData.style) end diff --git a/BuiltInPlugins/GameSettings/Framework/TestHelpers/provideMockContext.lua b/BuiltInPlugins/GameSettings/Framework/TestHelpers/provideMockContext.lua index 04cc85664e..10633a0dc7 100644 --- a/BuiltInPlugins/GameSettings/Framework/TestHelpers/provideMockContext.lua +++ b/BuiltInPlugins/GameSettings/Framework/TestHelpers/provideMockContext.lua @@ -91,7 +91,7 @@ return function(contextItemsList, children) -- Theme local theme if FlagsList:get("FFlagRefactorDevFrameworkTheme") then - theme = StudioTheme.new() + theme = StudioTheme.mock() else theme = ContextServices.Theme.mock(function(theme, getColor) return { diff --git a/BuiltInPlugins/GameSettings/Framework/UI/Box/test.spec.lua b/BuiltInPlugins/GameSettings/Framework/UI/Box/test.spec.lua index 27baa93e01..edc3b7c984 100644 --- a/BuiltInPlugins/GameSettings/Framework/UI/Box/test.spec.lua +++ b/BuiltInPlugins/GameSettings/Framework/UI/Box/test.spec.lua @@ -23,7 +23,7 @@ return function() else local theme if FlagsList:get("FFlagRefactorDevFrameworkTheme") then - theme = StudioTheme.new() + theme = StudioTheme.mock() else theme = Theme.new(function() return { diff --git a/BuiltInPlugins/GameSettings/Framework/UI/Button/test.spec.lua b/BuiltInPlugins/GameSettings/Framework/UI/Button/test.spec.lua index 3ffc122f23..cafabfaa40 100644 --- a/BuiltInPlugins/GameSettings/Framework/UI/Button/test.spec.lua +++ b/BuiltInPlugins/GameSettings/Framework/UI/Button/test.spec.lua @@ -18,7 +18,7 @@ return function() local function createTestButton(props, children) local theme if FlagsList:get("FFlagRefactorDevFrameworkTheme") then - theme = StudioTheme.new() + theme = StudioTheme.mock() else theme = Theme.new(function() return { diff --git a/BuiltInPlugins/GameSettings/Framework/UI/Container/test.spec.lua b/BuiltInPlugins/GameSettings/Framework/UI/Container/test.spec.lua index 21e7c8874a..1417100db0 100644 --- a/BuiltInPlugins/GameSettings/Framework/UI/Container/test.spec.lua +++ b/BuiltInPlugins/GameSettings/Framework/UI/Container/test.spec.lua @@ -18,7 +18,7 @@ return function() local function createTestContainer(props, children) local theme if FlagsList:get("FFlagRefactorDevFrameworkTheme") then - theme = StudioTheme.new() + theme = StudioTheme.mock() else theme = Theme.new(function() return { diff --git a/BuiltInPlugins/GameSettings/Framework/UI/DropdownMenu.lua b/BuiltInPlugins/GameSettings/Framework/UI/DropdownMenu.lua index 60f9fadd0c..b1e6b3c757 100644 --- a/BuiltInPlugins/GameSettings/Framework/UI/DropdownMenu.lua +++ b/BuiltInPlugins/GameSettings/Framework/UI/DropdownMenu.lua @@ -44,6 +44,7 @@ local TextLabel = require(UI.TextLabel) local FlagsList = Util.Flags.new({ FFlagRefactorDevFrameworkTheme = {"RefactorDevFrameworkTheme"}, + FFlagRefactorDevFrameworkContextItems = {"RefactorDevFrameworkContextItems"}, }) local DropdownMenu = Roact.PureComponent:extend("DropdownMenu") @@ -201,7 +202,7 @@ function DropdownMenu:renderMenu() local width = style.Width local offset = prioritize(style.Offset, Vector2.new(0, 0)) - local pluginGui = props.Focus:getTarget() + local pluginGui = FlagsList:get("FFlagRefactorDevFrameworkContextItems") and props.Focus:get() or props.Focus:getTarget() local menuPositionAndSize = self.getPositionAndSize(pluginGui, width, offset) local x = menuPositionAndSize.X diff --git a/BuiltInPlugins/GameSettings/Framework/UI/FakeLoadingBar/test.spec.lua b/BuiltInPlugins/GameSettings/Framework/UI/FakeLoadingBar/test.spec.lua index 4533d1b2a4..1306596adc 100644 --- a/BuiltInPlugins/GameSettings/Framework/UI/FakeLoadingBar/test.spec.lua +++ b/BuiltInPlugins/GameSettings/Framework/UI/FakeLoadingBar/test.spec.lua @@ -17,7 +17,7 @@ return function() local function createTestFakeLoadingBar(props, children) local theme if FlagsList:get("FFlagRefactorDevFrameworkTheme") then - theme = StudioTheme.new() + theme = StudioTheme.mock() else theme = Theme.new(function() return { diff --git a/BuiltInPlugins/GameSettings/Framework/UI/Image/test.spec.lua b/BuiltInPlugins/GameSettings/Framework/UI/Image/test.spec.lua index c5aa507a3a..6860be79dd 100644 --- a/BuiltInPlugins/GameSettings/Framework/UI/Image/test.spec.lua +++ b/BuiltInPlugins/GameSettings/Framework/UI/Image/test.spec.lua @@ -17,7 +17,7 @@ return function() local function createTestImageDecoration() local theme if FlagsList:get("FFlagRefactorDevFrameworkTheme") then - theme = StudioTheme.new() + theme = StudioTheme.mock() else theme = Theme.new(function() return { diff --git a/BuiltInPlugins/GameSettings/Framework/UI/InstanceTreeView/InstanceTreeRow.lua b/BuiltInPlugins/GameSettings/Framework/UI/InstanceTreeView/InstanceTreeRow.lua index 8000e57785..6110490a54 100644 --- a/BuiltInPlugins/GameSettings/Framework/UI/InstanceTreeView/InstanceTreeRow.lua +++ b/BuiltInPlugins/GameSettings/Framework/UI/InstanceTreeView/InstanceTreeRow.lua @@ -25,7 +25,7 @@ local TextLabel = require(UI.TextLabel) Typecheck.wrap(InstanceTreeRow, script) local function getClassIcon(instance) - local StudioService = game:GetService("StudioService") + local StudioService = game:GetService("StudioService") local className = instance.ClassName if instance.IsA then if instance:IsA("JointInstance") and className == "ManualWeld" or className == "ManualGlue" then @@ -73,9 +73,10 @@ function InstanceTreeRow:render() local arrowSize = style.Arrow.Size local padding = style.IconPadding local iconInfo = getClassIcon(item) - local iconSize = iconInfo.ImageRectSize.X + -- Default iconSize to (0, 0) as ImageRectSize is unavailable in Roblox CLI + local iconSize = iconInfo.ImageRectSize or Vector2.new() local labelOffset = indent + arrowSize + 2 * padding - local textOffset = iconSize + 3 * padding + local textOffset = iconSize.X + 3 * padding return Roact.createElement(Container, { Size = UDim2.new(1, -indent, 0, style.RowHeight), @@ -105,10 +106,10 @@ function InstanceTreeRow:render() Size = UDim2.new(1, -arrowSize, 1, 0), }, { Icon = Roact.createElement("ImageLabel", { - Size = UDim2.fromOffset(iconSize, iconInfo.ImageRectSize.Y), + Size = UDim2.fromOffset(iconSize.X, iconSize.Y), BackgroundTransparency = 1, Image = iconInfo.Image, - ImageRectSize = iconInfo.ImageRectSize, + ImageRectSize = iconSize, ImageRectOffset = iconInfo.ImageRectOffset, Position = UDim2.new(0, padding, 0.5, 0), AnchorPoint = Vector2.new(0, 0.5) diff --git a/BuiltInPlugins/GameSettings/Framework/UI/LinkText/test.spec.lua b/BuiltInPlugins/GameSettings/Framework/UI/LinkText/test.spec.lua index d72d172041..37cecf30b1 100644 --- a/BuiltInPlugins/GameSettings/Framework/UI/LinkText/test.spec.lua +++ b/BuiltInPlugins/GameSettings/Framework/UI/LinkText/test.spec.lua @@ -19,7 +19,7 @@ return function() local mouse = Mouse.new({}) local theme if FlagsList:get("FFlagRefactorDevFrameworkTheme") then - theme = StudioTheme.new() + theme = StudioTheme.mock() else theme = Theme.new(function() return { diff --git a/BuiltInPlugins/GameSettings/Framework/UI/LoadingBar/test.spec.lua b/BuiltInPlugins/GameSettings/Framework/UI/LoadingBar/test.spec.lua index ea437b136c..c898cde0f4 100644 --- a/BuiltInPlugins/GameSettings/Framework/UI/LoadingBar/test.spec.lua +++ b/BuiltInPlugins/GameSettings/Framework/UI/LoadingBar/test.spec.lua @@ -18,7 +18,7 @@ return function() local function createTestLoadingBar(props, children) local theme if FlagsList:get("FFlagRefactorDevFrameworkTheme") then - theme = StudioTheme.new() + theme = StudioTheme.mock() else theme = Theme.new(function() return { diff --git a/BuiltInPlugins/GameSettings/Framework/UI/RadioButton/test.spec.lua b/BuiltInPlugins/GameSettings/Framework/UI/RadioButton/test.spec.lua index 31e2c12c77..4e578229a9 100644 --- a/BuiltInPlugins/GameSettings/Framework/UI/RadioButton/test.spec.lua +++ b/BuiltInPlugins/GameSettings/Framework/UI/RadioButton/test.spec.lua @@ -23,7 +23,7 @@ return function() local function createTestRadioButton(props, children) local theme if FlagsList:get("FFlagRefactorDevFrameworkTheme") then - theme = StudioTheme.new() + theme = StudioTheme.mock() else theme = Theme.new(function() return { diff --git a/BuiltInPlugins/GameSettings/Framework/UI/RoundBox/test.spec.lua b/BuiltInPlugins/GameSettings/Framework/UI/RoundBox/test.spec.lua index 10b16f79a0..d9715cf163 100644 --- a/BuiltInPlugins/GameSettings/Framework/UI/RoundBox/test.spec.lua +++ b/BuiltInPlugins/GameSettings/Framework/UI/RoundBox/test.spec.lua @@ -17,7 +17,7 @@ return function() local function createTestRoundBoxDecoration() local theme if FlagsList:get("FFlagRefactorDevFrameworkTheme") then - theme = StudioTheme.new() + theme = StudioTheme.mock() else theme = Theme.new(function() return { diff --git a/BuiltInPlugins/GameSettings/Framework/UI/TextLabel/test.spec.lua b/BuiltInPlugins/GameSettings/Framework/UI/TextLabel/test.spec.lua index a403d8b7d3..5f739cb55f 100644 --- a/BuiltInPlugins/GameSettings/Framework/UI/TextLabel/test.spec.lua +++ b/BuiltInPlugins/GameSettings/Framework/UI/TextLabel/test.spec.lua @@ -17,7 +17,7 @@ return function() local function createTestTextLabelDecoration() local theme if FlagsList:get("FFlagRefactorDevFrameworkTheme") then - theme = StudioTheme.new() + theme = StudioTheme.mock() else theme = Theme.new(function() return { diff --git a/BuiltInPlugins/GameSettings/Framework/UI/ToggleButton/test.spec.lua b/BuiltInPlugins/GameSettings/Framework/UI/ToggleButton/test.spec.lua index 0d712bdf45..c4db80d801 100644 --- a/BuiltInPlugins/GameSettings/Framework/UI/ToggleButton/test.spec.lua +++ b/BuiltInPlugins/GameSettings/Framework/UI/ToggleButton/test.spec.lua @@ -27,7 +27,7 @@ return function() local mouse = Mouse.new({}) local theme if FlagsList:get("FFlagRefactorDevFrameworkTheme") then - theme = StudioTheme.new() + theme = StudioTheme.mock() else theme = Theme.new(function() return { diff --git a/BuiltInPlugins/GameSettings/Framework/UI/Tooltip/test.spec.lua b/BuiltInPlugins/GameSettings/Framework/UI/Tooltip/test.spec.lua index 984329d7eb..8f7a0a6fc9 100644 --- a/BuiltInPlugins/GameSettings/Framework/UI/Tooltip/test.spec.lua +++ b/BuiltInPlugins/GameSettings/Framework/UI/Tooltip/test.spec.lua @@ -25,7 +25,7 @@ return function() local focus = Focus.new(target) local theme if FlagsList:get("FFlagRefactorDevFrameworkTheme") then - theme = StudioTheme.new() + theme = StudioTheme.mock() else theme = Theme.new(function() return { diff --git a/BuiltInPlugins/GameSettings/Framework/UI/TreeView/test.spec.lua b/BuiltInPlugins/GameSettings/Framework/UI/TreeView/test.spec.lua index 953aef3483..8d06ee8a86 100644 --- a/BuiltInPlugins/GameSettings/Framework/UI/TreeView/test.spec.lua +++ b/BuiltInPlugins/GameSettings/Framework/UI/TreeView/test.spec.lua @@ -63,7 +63,7 @@ return function() local function createTreeView() local theme if FlagsList:get("FFlagRefactorDevFrameworkTheme") then - theme = StudioTheme.new() + theme = StudioTheme.mock() else theme = Theme.new(function() return { diff --git a/BuiltInPlugins/GameSettings/Framework/Util.lua b/BuiltInPlugins/GameSettings/Framework/Util.lua index 5454c404f1..6a3e12c089 100644 --- a/BuiltInPlugins/GameSettings/Framework/Util.lua +++ b/BuiltInPlugins/GameSettings/Framework/Util.lua @@ -19,6 +19,7 @@ return strict({ FitFrame = require(script.FitFrame), Flags = require(script.Flags), + getTestVariation = require(script.getTestVariation), Immutable = require(script.Immutable), LayoutOrderIterator = require(script.LayoutOrderIterator), Math = require(script.Math), diff --git a/BuiltInPlugins/GameSettings/Framework/Util/CrossPluginCommunication.lua b/BuiltInPlugins/GameSettings/Framework/Util/CrossPluginCommunication.lua index 37caa1bece..d3b82f5783 100644 --- a/BuiltInPlugins/GameSettings/Framework/Util/CrossPluginCommunication.lua +++ b/BuiltInPlugins/GameSettings/Framework/Util/CrossPluginCommunication.lua @@ -22,10 +22,12 @@ CrossPluginCommunication.__index = CrossPluginCommunication CrossPluginCommunication.BASE_FOLDER_NAME = "CrossPluginCommunication" -function CrossPluginCommunication.new(pluginNamespace) +function CrossPluginCommunication.new(pluginNamespace, hostService) assert(t.string(pluginNamespace), "pluginNamespace must be a string") + assert(t.optional(t.instance(pluginNamespace)), "hostService must be an instance if defined") local self = { + hostService = hostService or RobloxPluginGuiService, pluginNamespace = pluginNamespace } @@ -49,7 +51,7 @@ function CrossPluginCommunication:ensureFolderExists(parent, name) end function CrossPluginCommunication:getNamespaceFolder() - local base = self:ensureFolderExists(RobloxPluginGuiService, self.BASE_FOLDER_NAME) + local base = self:ensureFolderExists(self.hostService, self.BASE_FOLDER_NAME) return self:ensureFolderExists(base, self.pluginNamespace) end diff --git a/BuiltInPlugins/GameSettings/Framework/Util/CrossPluginCommunication.spec.lua b/BuiltInPlugins/GameSettings/Framework/Util/CrossPluginCommunication.spec.lua index 60338f2b0c..f3c1035f50 100644 --- a/BuiltInPlugins/GameSettings/Framework/Util/CrossPluginCommunication.spec.lua +++ b/BuiltInPlugins/GameSettings/Framework/Util/CrossPluginCommunication.spec.lua @@ -1,5 +1,3 @@ -local RobloxPluginGuiService = game:GetService("RobloxPluginGuiService") - local Framework = script.Parent.Parent local CrossPluginCommunication = require(Framework.Util.CrossPluginCommunication) @@ -8,8 +6,10 @@ return function() -- TODO DEVTOOLS-4397: Move cleanup to afterEach hook after the dependency structure is fixed + local hostService = workspace + local function createTestComms() - return CrossPluginCommunication.new(TEST_NAMESPACE) + return CrossPluginCommunication.new(TEST_NAMESPACE, hostService) end describe("Invoke", function() @@ -32,7 +32,7 @@ return function() comms:OnInvoke("blah", function() end) - local namespace = RobloxPluginGuiService:FindFirstChild(CrossPluginCommunication.BASE_FOLDER_NAME):FindFirstChild(TEST_NAMESPACE) + local namespace = hostService:FindFirstChild(CrossPluginCommunication.BASE_FOLDER_NAME):FindFirstChild(TEST_NAMESPACE) assert(namespace) diff --git a/BuiltInPlugins/GameSettings/Framework/Util/Palette.spec.lua b/BuiltInPlugins/GameSettings/Framework/Util/Palette.spec.lua index 8794c80717..02911b17c7 100644 --- a/BuiltInPlugins/GameSettings/Framework/Util/Palette.spec.lua +++ b/BuiltInPlugins/GameSettings/Framework/Util/Palette.spec.lua @@ -1,5 +1,16 @@ return function() - local Palette = require(script.Parent.Palette) + + local Util = script.Parent + local Flags = require(Util.Flags) + local FlagsList = Flags.new({ + FFlagRefactorDevFrameworkTheme = {"RefactorDevFrameworkTheme"}, + }) + + if FlagsList:get("FFlagRefactorDevFrameworkTheme") then + return + end + + local Palette = require(Util.Palette) it("should be a table of color StyleValues", function() expect(Palette).to.be.ok() diff --git a/BuiltInPlugins/GameSettings/Framework/Util/Typecheck/t.lua b/BuiltInPlugins/GameSettings/Framework/Util/Typecheck/t.lua index 44c8d50af8..045d7ac0dd 100644 --- a/BuiltInPlugins/GameSettings/Framework/Util/Typecheck/t.lua +++ b/BuiltInPlugins/GameSettings/Framework/Util/Typecheck/t.lua @@ -1,3 +1,5 @@ +--!nocheck + --[[ DeveloperFramework uses t from Osyris https://github.com/osyrisrblx/t diff --git a/BuiltInPlugins/GameSettings/Framework/Util/getTestVariation.lua b/BuiltInPlugins/GameSettings/Framework/Util/getTestVariation.lua new file mode 100644 index 0000000000..d09873db1d --- /dev/null +++ b/BuiltInPlugins/GameSettings/Framework/Util/getTestVariation.lua @@ -0,0 +1,30 @@ +-- This function hides ABTestService away and provides a quick interface for getting a test's variation. +-- Should the requested test not exist, it will return the Control group. + +local ABTestService = game:GetService("ABTestService") + +local AB_TEST_GROUP_CONTROL = "Control" + +-- abTestName : (string) the name of test. ex) "AllUsers.Studio.ExampleTestName" +-- abTestService : (table, optional) an optional override for ABTestService +-- RETURNS : (int) the variation that the user is in. 0 = Control +return function(abTestName, abTestService) + assert(type(abTestName) == "string", "Expected abTestName to be a string") + if abTestService then + assert(type(abTestService.GetVariant) == "function", "Expected the abTestService object to have a GetVariant member function") + else + abTestService = ABTestService + end + + local variation = abTestService:GetVariant(abTestName) + if variation == AB_TEST_GROUP_CONTROL then + return 0 + else + local _, _, variationNumber = string.find(variation, "Variation(%d+)") + if not variationNumber then + return 0 + end + + return tonumber(variationNumber) + end +end diff --git a/BuiltInPlugins/GameSettings/Framework/Util/getTestVariation.spec.lua b/BuiltInPlugins/GameSettings/Framework/Util/getTestVariation.spec.lua new file mode 100644 index 0000000000..5febceee91 --- /dev/null +++ b/BuiltInPlugins/GameSettings/Framework/Util/getTestVariation.spec.lua @@ -0,0 +1,41 @@ +return function() + local getTestVariation = require(script.Parent.getTestVariation) + local VARIATION_NAME_CONTROL = "Control" + + local function createTestService(result) + local DebugABService = {} + + function DebugABService:GetVariant(testName) + return result + end + + return DebugABService + end + + it("should expect some test name", function() + expect(function() + getTestVariation() + end).to.throw() + end) + + it("should return zero when in the control group", function() + local debugABTestService = createTestService(VARIATION_NAME_CONTROL) + local variation = getTestVariation("AllUsers.Studio.ShouldShowZero", debugABTestService) + expect(variation).to.equal(0) + end) + + it("should return the number of the variation when not in the control group", function() + local debugABTestService = createTestService("Variation1") + local variation = getTestVariation("AllUsers.Studio.ShouldShowNumber", debugABTestService) + expect(variation).to.equal(1) + + -- there aren't any variations past 8, but it should work with any number + debugABTestService = createTestService("Variation23") + variation = getTestVariation("AllUsers.Studio.ShouldShowNumber", debugABTestService) + expect(variation).to.equal(23) + + debugABTestService = createTestService("Variation456") + variation = getTestVariation("AllUsers.Studio.ShouldShowNumber", debugABTestService) + expect(variation).to.equal(456) + end) +end \ No newline at end of file diff --git a/BuiltInPlugins/GameSettings/Pages/AvatarPage/Components/AssetsPanel.lua b/BuiltInPlugins/GameSettings/Pages/AvatarPage/Components/AssetsPanel.lua index 9318b49c86..e32886ec1e 100644 --- a/BuiltInPlugins/GameSettings/Pages/AvatarPage/Components/AssetsPanel.lua +++ b/BuiltInPlugins/GameSettings/Pages/AvatarPage/Components/AssetsPanel.lua @@ -92,7 +92,8 @@ local createInputRow = function(self, label, assetTypeId, layoutOrderIterator) Mouse = self.props.Mouse, SetValue = function(text) - local id = string.len(string.gsub(text, " ", "")) > 0 and tonumber(text) or 0 + local whitespaceStrippedText, _ = string.gsub(text, " ", "") + local id = string.len(whitespaceStrippedText) > 0 and tonumber(text) or 0 if id ~= assetId then local newTemplateModel = StateModelTemplate.makeCopy(template) @@ -156,4 +157,4 @@ createRowsForClothes = function(self, tableToPopulate, layoutOrder, localized) createRowsForAssets(self, tableToPopulate, layoutOrder, clothingTitle, inputRowsData) end -return AssetsPanel \ No newline at end of file +return AssetsPanel diff --git a/BuiltInPlugins/GameSettings/Pages/AvatarPage/Util/StateModelTemplate.lua b/BuiltInPlugins/GameSettings/Pages/AvatarPage/Util/StateModelTemplate.lua index fb5fa20c11..c424555b7a 100644 --- a/BuiltInPlugins/GameSettings/Pages/AvatarPage/Util/StateModelTemplate.lua +++ b/BuiltInPlugins/GameSettings/Pages/AvatarPage/Util/StateModelTemplate.lua @@ -343,6 +343,8 @@ function Template:getAsset(assetTypeID) if self.AssetsOverrides[assetTypeID] then return self.AssetsOverrides[assetTypeID].assetID, self.AssetsOverrides[assetTypeID].isPlayerChoice end + + return nil, nil end createAssetsTable = function(self) @@ -411,4 +413,4 @@ areScalesEqual = function(scales, otherScales) return UtilityFunctionsTable.countDictionaryKeys(scales) == UtilityFunctionsTable.countDictionaryKeys(otherScales) end -return Template \ No newline at end of file +return Template diff --git a/BuiltInPlugins/GameSettings/Pages/BasicInfoPage/BasicInfo.lua b/BuiltInPlugins/GameSettings/Pages/BasicInfoPage/BasicInfo.lua index 74c9154429..65f00e0a4d 100644 --- a/BuiltInPlugins/GameSettings/Pages/BasicInfoPage/BasicInfo.lua +++ b/BuiltInPlugins/GameSettings/Pages/BasicInfoPage/BasicInfo.lua @@ -158,7 +158,7 @@ local function saveSettings(store, contextItems) store:dispatch(AddErrors({name = "Moderated"})) end - error() + error("Game name was moderated") end end end, @@ -175,7 +175,7 @@ local function saveSettings(store, contextItems) store:dispatch(AddErrors({description = "Moderated"})) end - error() + error("Game description was moderated") end end end, @@ -311,7 +311,7 @@ local function dispatchChanges(setValue, dispatch) NameChanged = function(text) dispatch(AddChange("name", text)) local nameLength = string.len(text) - if nameLength == 0 or string.len(string.gsub(text, " ", "")) == 0 then + if nameLength == 0 or string.len((string.gsub(text, " ", ""))) == 0 then dispatch(AddErrors({name = "Empty"})) elseif nameLength > MAX_NAME_LENGTH then dispatch(AddErrors({name = "TooLong"})) diff --git a/BuiltInPlugins/GameSettings/Pages/OptionsPage/Options.lua b/BuiltInPlugins/GameSettings/Pages/OptionsPage/Options.lua index b1ad260690..93c9b2ce2d 100644 --- a/BuiltInPlugins/GameSettings/Pages/OptionsPage/Options.lua +++ b/BuiltInPlugins/GameSettings/Pages/OptionsPage/Options.lua @@ -237,7 +237,7 @@ Options = RoactRodux.connect( return settingFromState(state.Settings, propName) end - return loadValuesToProps(getValue, state) + return loadValuesToProps(getValue) end, function(dispatch) @@ -253,4 +253,4 @@ Options = RoactRodux.connect( Options.LocalizationId = LOCALIZATION_ID -return Options \ No newline at end of file +return Options diff --git a/BuiltInPlugins/GameSettings/Pages/SecurityPage/Security.lua b/BuiltInPlugins/GameSettings/Pages/SecurityPage/Security.lua index e602518e23..608c2153ab 100644 --- a/BuiltInPlugins/GameSettings/Pages/SecurityPage/Security.lua +++ b/BuiltInPlugins/GameSettings/Pages/SecurityPage/Security.lua @@ -171,7 +171,7 @@ Security = RoactRodux.connect( return settingFromState(state.Settings, propName) end - return loadValuesToProps(getValue, state) + return loadValuesToProps(getValue) end, function(dispatch) @@ -187,4 +187,4 @@ Security = RoactRodux.connect( Security.LocalizationId = LOCALIZATION_ID -return Security \ No newline at end of file +return Security diff --git a/BuiltInPlugins/GameSettings/Pages/WorldPage/World.lua b/BuiltInPlugins/GameSettings/Pages/WorldPage/World.lua index 5af8fc6378..9077c25c4b 100644 --- a/BuiltInPlugins/GameSettings/Pages/WorldPage/World.lua +++ b/BuiltInPlugins/GameSettings/Pages/WorldPage/World.lua @@ -347,10 +347,10 @@ World = RoactRodux.connect( end end - return dispatchChanges(setValue, dispatch) + return dispatchChanges(setValue) end )(World) World.LocalizationId = LOCALIZATION_ID -return World \ No newline at end of file +return World diff --git a/BuiltInPlugins/GameSettings/Promise.lua b/BuiltInPlugins/GameSettings/Promise.lua index a16f7e5fe5..3e3e3104d4 100644 --- a/BuiltInPlugins/GameSettings/Promise.lua +++ b/BuiltInPlugins/GameSettings/Promise.lua @@ -411,4 +411,4 @@ function Promise.prototype:_reject(...) end end -return Promise \ No newline at end of file +return Promise diff --git a/BuiltInPlugins/GameSettings/RoactStudioWidgets/Internal/Theme.lua b/BuiltInPlugins/GameSettings/RoactStudioWidgets/Internal/Theme.lua index dd863dc3e3..9b4ff8c591 100644 --- a/BuiltInPlugins/GameSettings/RoactStudioWidgets/Internal/Theme.lua +++ b/BuiltInPlugins/GameSettings/RoactStudioWidgets/Internal/Theme.lua @@ -2,8 +2,6 @@ Theme helper functions ]] -local Constants = require(script.Parent.Constants) - local getColor = nil local Theme = {} @@ -95,4 +93,4 @@ function getColor(styleGuideColor, modifier) return settings().Studio.Theme:GetColor(styleGuideColor, modifier) end -return Theme \ No newline at end of file +return Theme diff --git a/BuiltInPlugins/GameSettings/RoactStudioWidgets/RadioButtonSet.lua b/BuiltInPlugins/GameSettings/RoactStudioWidgets/RadioButtonSet.lua index 21d55587c5..0409a6f7c7 100644 --- a/BuiltInPlugins/GameSettings/RoactStudioWidgets/RadioButtonSet.lua +++ b/BuiltInPlugins/GameSettings/RoactStudioWidgets/RadioButtonSet.lua @@ -38,7 +38,7 @@ local function RadioButtonSet(props) local buttons = props.Buttons local numButtons = #buttons - local children = { + local children: { [any]: any } = { Layout = Roact.createElement("UIListLayout", { Padding = UDim.new(0, RADIO_BUTTON_PADDING), SortOrder = Enum.SortOrder.LayoutOrder, @@ -63,7 +63,6 @@ local function RadioButtonSet(props) end local allRadioButtonsHeight = 0 - local nextLayoutOrder = 1 for i, button in ipairs(buttons) do table.insert(children, Roact.createElement(RadioButton, { Title = button.Title, @@ -81,7 +80,6 @@ local function RadioButtonSet(props) allRadioButtonsHeight = allRadioButtonsHeight + Constants.RADIO_BUTTON_SIZE allRadioButtonsHeight = allRadioButtonsHeight + ((nil ~= button.Description) and Constants.RADIO_BUTTON_SIZE or 0) - nextLayoutOrder = i + 1 end if (props.SubDescription) then @@ -129,4 +127,4 @@ getStyle = function(props) return style end -return RadioButtonSet \ No newline at end of file +return RadioButtonSet diff --git a/BuiltInPlugins/GameSettings/RoactStudioWidgets/Text.lua b/BuiltInPlugins/GameSettings/RoactStudioWidgets/Text.lua index cec733a46a..bcb750a062 100644 --- a/BuiltInPlugins/GameSettings/RoactStudioWidgets/Text.lua +++ b/BuiltInPlugins/GameSettings/RoactStudioWidgets/Text.lua @@ -16,7 +16,6 @@ local Roact = require(script.Parent.Internal.RequireRoact) local ThemeChangeListener = require(script.Parent.Internal.ThemeChangeListener) local Theme = require(script.Parent.Internal.Theme) -local TextUtil = require(script.Parent.Internal.Text) local Constants = require(script.Parent.Internal.Constants) local getStyle = nil @@ -48,9 +47,9 @@ end getStyle = function(props) local style = { - TextColor = props.Style and self.props.Style.TextColor or Theme.getMainTextColor() + TextColor = props.Style and props.Style.TextColor or Theme.getMainTextColor() } return style end -return Text \ No newline at end of file +return Text diff --git a/BuiltInPlugins/GameSettings/RoactStudioWidgets/TitledFrame.lua b/BuiltInPlugins/GameSettings/RoactStudioWidgets/TitledFrame.lua index c9f1405d9e..e8f41465a8 100644 --- a/BuiltInPlugins/GameSettings/RoactStudioWidgets/TitledFrame.lua +++ b/BuiltInPlugins/GameSettings/RoactStudioWidgets/TitledFrame.lua @@ -13,7 +13,6 @@ ]] local Roact = require(script.Parent.Internal.RequireRoact) -local Constants = require(script.Parent.Internal.Constants) local ThemeChangeListener = require(script.Parent.Internal.ThemeChangeListener) local Theme = require(script.Parent.Internal.Theme) @@ -67,4 +66,4 @@ getStyle = function(props) return style end -return TitledFrame \ No newline at end of file +return TitledFrame diff --git a/BuiltInPlugins/GameSettings/Src/Components/Dropdown.lua b/BuiltInPlugins/GameSettings/Src/Components/Dropdown.lua index 94f64873e3..14151ec639 100644 --- a/BuiltInPlugins/GameSettings/Src/Components/Dropdown.lua +++ b/BuiltInPlugins/GameSettings/Src/Components/Dropdown.lua @@ -50,6 +50,8 @@ local function findCurrentTitle(entries, currentId) end end end + + return nil end function Dropdown:init(props) @@ -270,4 +272,4 @@ ContextServices.mapToProps(Dropdown, { Mouse = ContextServices.Mouse, }) -return Dropdown \ No newline at end of file +return Dropdown diff --git a/BuiltInPlugins/GameSettings/Src/Components/SettingsPages/SettingsPage.lua b/BuiltInPlugins/GameSettings/Src/Components/SettingsPages/SettingsPage.lua index 650a81dc4f..212c47d246 100644 --- a/BuiltInPlugins/GameSettings/Src/Components/SettingsPages/SettingsPage.lua +++ b/BuiltInPlugins/GameSettings/Src/Components/SettingsPages/SettingsPage.lua @@ -128,6 +128,8 @@ function SettingsPage:render() }, self.props.CreateChildren()), }) end + + return nil end ContextServices.mapToProps(SettingsPage, { @@ -159,4 +161,4 @@ SettingsPage = RoactRodux.connect( end )(SettingsPage) -return SettingsPage \ No newline at end of file +return SettingsPage diff --git a/BuiltInPlugins/GameSettings/Src/Controllers/GroupMetadataController.lua b/BuiltInPlugins/GameSettings/Src/Controllers/GroupMetadataController.lua index e27cb8c24b..202e619e78 100644 --- a/BuiltInPlugins/GameSettings/Src/Controllers/GroupMetadataController.lua +++ b/BuiltInPlugins/GameSettings/Src/Controllers/GroupMetadataController.lua @@ -1,10 +1,3 @@ -local Plugin = script.Parent.Parent.Parent -local Util = require(Plugin.Framework.Util) - -local FileUtils = require(Plugin.Src.Util.FileUtils) - -local Promise = Util.Promise - local GroupMetadataController = {} GroupMetadataController.__index = GroupMetadataController @@ -130,4 +123,4 @@ function GroupMetadataController:getRolesets(groupId) return rolesetMetadata end -return GroupMetadataController \ No newline at end of file +return GroupMetadataController diff --git a/BuiltInPlugins/GameSettings/Src/Util/AssetOverrides.lua b/BuiltInPlugins/GameSettings/Src/Util/AssetOverrides.lua index cab578c075..e8329d4182 100644 --- a/BuiltInPlugins/GameSettings/Src/Util/AssetOverrides.lua +++ b/BuiltInPlugins/GameSettings/Src/Util/AssetOverrides.lua @@ -51,7 +51,7 @@ function AssetOverrides.getErrors(assetOverridesData) local result = nil for _, subTab in pairs(assetOverridesData) do if not subTab.isPlayerChoice then - local isAssetIDSpecified = nil ~= subTab.assetID and 0 ~= subTab.assetID and "0" ~= subTab.assetID and string.len(string.gsub(subTab.assetID, " ", "")) > 0 + local isAssetIDSpecified = nil ~= subTab.assetID and 0 ~= subTab.assetID and "0" ~= subTab.assetID and string.len((string.gsub(subTab.assetID, " ", ""))) > 0 if not isAssetIDSpecified then result = result or {} result[subTab.assetTypeID] = "OverrideEmpty" -- OverrideEmpty is a key into a localization table @@ -61,4 +61,4 @@ function AssetOverrides.getErrors(assetOverridesData) return result end -return AssetOverrides \ No newline at end of file +return AssetOverrides diff --git a/BuiltInPlugins/GameSettings/UILibrary/_internal/Utils/Symbol.spec.lua b/BuiltInPlugins/GameSettings/UILibrary/_internal/Utils/Symbol.spec.lua index cde9be065b..f3312055c9 100644 --- a/BuiltInPlugins/GameSettings/UILibrary/_internal/Utils/Symbol.spec.lua +++ b/BuiltInPlugins/GameSettings/UILibrary/_internal/Utils/Symbol.spec.lua @@ -11,7 +11,7 @@ return function() it("should coerce to the given name", function() local symbol = Symbol.named("foo") - expect(tostring(symbol):find("foo")).to.be.ok() + expect(tostring(symbol):match("foo")).to.be.ok() end) it("should be unique when constructed", function() @@ -42,4 +42,4 @@ return function() expect(symbolA).never.to.equal(symbolB) end) end) -end \ No newline at end of file +end diff --git a/BuiltInPlugins/LocalizationTools/Packages/Framework.lua b/BuiltInPlugins/LocalizationTools/Packages/Framework.lua index d1c77a7116..401dae2663 100644 --- a/BuiltInPlugins/LocalizationTools/Packages/Framework.lua +++ b/BuiltInPlugins/LocalizationTools/Packages/Framework.lua @@ -7,6 +7,7 @@ return strict({ Http = require(script.Http), RobloxAPI = require(script.RobloxAPI), StudioUI = require(script.StudioUI), + Style = require(script.Style), TestHelpers = require(script.TestHelpers), UI = require(script.UI), Util = require(script.Util), diff --git a/BuiltInPlugins/LocalizationTools/Packages/Framework/ContextServices.lua b/BuiltInPlugins/LocalizationTools/Packages/Framework/ContextServices.lua index aec15f6aa5..1eda5fb68d 100644 --- a/BuiltInPlugins/LocalizationTools/Packages/Framework/ContextServices.lua +++ b/BuiltInPlugins/LocalizationTools/Packages/Framework/ContextServices.lua @@ -1,7 +1,7 @@ --[[ Public interface for ContextServices ]] - +local Framework = script.Parent local Src = script local strict = require(Src.Parent.Util.strict) @@ -21,6 +21,7 @@ local Plugin = require(Src.Plugin) local PluginActions = require(Src.PluginActions) local Provider = require(Src.Provider) local Store = require(Src.Store) +local Stylizer = require(Framework.Style.Stylizer) local Theme = require(Src.Theme) local UILibraryWrapper = require(Src.UILibraryWrapper) @@ -43,6 +44,7 @@ local ContextServices = strict({ Plugin = Plugin, PluginActions = PluginActions, Provider = Provider, + Stylizer = Stylizer, Store = Store, Theme = Theme, UILibraryWrapper = UILibraryWrapper, diff --git a/BuiltInPlugins/LocalizationTools/Packages/Framework/ContextServices/ContextItem.lua b/BuiltInPlugins/LocalizationTools/Packages/Framework/ContextServices/ContextItem.lua index 45aebb4a0b..4d4672b6b1 100644 --- a/BuiltInPlugins/LocalizationTools/Packages/Framework/ContextServices/ContextItem.lua +++ b/BuiltInPlugins/LocalizationTools/Packages/Framework/ContextServices/ContextItem.lua @@ -92,6 +92,13 @@ function ContextItem:createProvider() error(message, 0) end +--[[ + Cleans up the context item (e.g. disconnecting from events). + Optional override +]] +function ContextItem:destroy() +end + function ContextItem:__tostring() return tostring(self.__name) end @@ -110,7 +117,8 @@ end callback getChangedSignal: any => Signal optional Should return a signal for this context item to connect to. When that signal fires, this context item updates. If not provided, then the context item will be static - calllabck verifyNewItem: A callback fired when the simple ContextItem is being created for verification purposes. + callback verifyNewItem: A callback fired when the simple ContextItem is being created for verification purposes. + callback destroy: Optional function to destroy the wrapped object ]] function ContextItem:createSimple(name, options) assert(name, "ContextItem:createSimple expects a name parameter") @@ -144,6 +152,11 @@ function ContextItem:createSimple(name, options) self._connection:Disconnect() self._connection = nil end + + if options.destroy then + options.destroy(self._obj) + end + self._obj = nil end function SimpleContextItem:get() diff --git a/BuiltInPlugins/LocalizationTools/Packages/Framework/ContextServices/FastFlags.lua b/BuiltInPlugins/LocalizationTools/Packages/Framework/ContextServices/FastFlags.lua index 9645edae58..c65f15e479 100644 --- a/BuiltInPlugins/LocalizationTools/Packages/Framework/ContextServices/FastFlags.lua +++ b/BuiltInPlugins/LocalizationTools/Packages/Framework/ContextServices/FastFlags.lua @@ -27,7 +27,7 @@ function FastFlags.new(featuresMap, overrides) local flags = Flags.new(featuresMap) for featureName, isOn in pairs(overrides) do - flags:setLocalOverride(featuresName, isOn) + flags:setLocalOverride(featureName, isOn) end local self = { diff --git a/BuiltInPlugins/LocalizationTools/Packages/Framework/ContextServices/FastFlags.spec.lua b/BuiltInPlugins/LocalizationTools/Packages/Framework/ContextServices/FastFlags.spec.lua index e475804219..51166e1c3c 100644 --- a/BuiltInPlugins/LocalizationTools/Packages/Framework/ContextServices/FastFlags.spec.lua +++ b/BuiltInPlugins/LocalizationTools/Packages/Framework/ContextServices/FastFlags.spec.lua @@ -4,7 +4,6 @@ return function() local mapToProps = require(Framework.ContextServices.mapToProps) local provide = require(Framework.ContextServices.provide) - local Flags = require(Framework.Util).Flags local FastFlags = require(script.Parent.FastFlags) it("should construct just fine with no arguments", function() @@ -58,4 +57,4 @@ return function() expect(didRender).to.equal(true) end) -end \ No newline at end of file +end diff --git a/BuiltInPlugins/LocalizationTools/Packages/Framework/ContextServices/Localization.lua b/BuiltInPlugins/LocalizationTools/Packages/Framework/ContextServices/Localization.lua index c3280ddcc1..74e9154969 100644 --- a/BuiltInPlugins/LocalizationTools/Packages/Framework/ContextServices/Localization.lua +++ b/BuiltInPlugins/LocalizationTools/Packages/Framework/ContextServices/Localization.lua @@ -268,6 +268,14 @@ function Localization.mock() end } + local currentLocale = 0 + local localeIDs = {"en-us", "es", "es-es", "ko", "ja"} + local function getLocale() + currentLocale = (currentLocale + 1) % 5 + local nextLocale = localeIDs[currentLocale] + return nextLocale + end + -- create a mock localization object for tests return Localization.new({ -- create a mock resource file that mimics the real thing @@ -278,8 +286,9 @@ function Localization.mock() -- for tests, don't connect to any system signals to ensure stuff doesn't change mid test overrideLocaleChangedSignal = Signal.new(), + getLocale = getLocale, }) end -return Localization \ No newline at end of file +return Localization diff --git a/BuiltInPlugins/LocalizationTools/Packages/Framework/ContextServices/Mouse.spec.lua b/BuiltInPlugins/LocalizationTools/Packages/Framework/ContextServices/Mouse.spec.lua index 94f3f1c1aa..c0abe8041d 100644 --- a/BuiltInPlugins/LocalizationTools/Packages/Framework/ContextServices/Mouse.spec.lua +++ b/BuiltInPlugins/LocalizationTools/Packages/Framework/ContextServices/Mouse.spec.lua @@ -54,7 +54,7 @@ return function() mouse:__pushCursor("PointingHand") mouse:__pushCursor("PointingHand", 2) mouse:__resetCursor() - expect(next(mouse.cursors)).never.to.be.ok() + expect((next(mouse.cursors))).never.to.be.ok() end) it("should be a stack", function() @@ -94,4 +94,4 @@ return function() expect(test.Icon).to.equal("rbxasset://SystemCursors/Arrow") end) end) -end \ No newline at end of file +end diff --git a/BuiltInPlugins/LocalizationTools/Packages/Framework/ContextServices/PluginActions.spec.lua b/BuiltInPlugins/LocalizationTools/Packages/Framework/ContextServices/PluginActions.spec.lua index d6c4b52bb4..798ee03a52 100644 --- a/BuiltInPlugins/LocalizationTools/Packages/Framework/ContextServices/PluginActions.spec.lua +++ b/BuiltInPlugins/LocalizationTools/Packages/Framework/ContextServices/PluginActions.spec.lua @@ -18,12 +18,12 @@ return function() local Roact = require(Framework.Parent.Roact) local provide = require(Framework.ContextServices.provide) local mapToProps = require(Framework.ContextServices.mapToProps) - local mockPlugin = require(Framework.TestHelpers.Services.mockPlugin) + local MockPlugin = require(Framework.TestHelpers.Instances.MockPlugin) local PluginActions = require(script.Parent.PluginActions) it("should be providable as a ContextItem and call CreatePluginAction", function() - local plugin = mockPlugin.new() + local plugin = MockPlugin.new() local spy, wrapped = Spy.new(function(self, id) @@ -82,4 +82,4 @@ return function() Roact.unmount(instance) end) -end \ No newline at end of file +end diff --git a/BuiltInPlugins/LocalizationTools/Packages/Framework/ContextServices/Theme.spec.lua b/BuiltInPlugins/LocalizationTools/Packages/Framework/ContextServices/Theme.spec.lua index 22377dc00b..1547fb3c11 100644 --- a/BuiltInPlugins/LocalizationTools/Packages/Framework/ContextServices/Theme.spec.lua +++ b/BuiltInPlugins/LocalizationTools/Packages/Framework/ContextServices/Theme.spec.lua @@ -11,6 +11,13 @@ return function() local StyleTable = Util.StyleTable local StyleModifier = Util.StyleModifier local Signal = Util.Signal + local FlagsList = Util.Flags.new({ + FFlagRefactorDevFrameworkTheme = {"RefactorDevFrameworkTheme"}, + }) + + if FlagsList:get("FFlagRefactorDevFrameworkTheme") then + return + end local function createTestThemedComponent(render) local TestThemedComponent = Roact.PureComponent:extend("TestThemedComponent") diff --git a/BuiltInPlugins/LocalizationTools/Packages/Framework/ContextServices/UILibraryWrapper.lua b/BuiltInPlugins/LocalizationTools/Packages/Framework/ContextServices/UILibraryWrapper.lua index 13fa54244a..58a768f0bf 100644 --- a/BuiltInPlugins/LocalizationTools/Packages/Framework/ContextServices/UILibraryWrapper.lua +++ b/BuiltInPlugins/LocalizationTools/Packages/Framework/ContextServices/UILibraryWrapper.lua @@ -9,17 +9,23 @@ UILibraryWrapper expects to be provided after a Theme, Plugin, and Focus ContextItem. ]] - -local noGetThemeError = [[ -UILibraryProvider expects Theme to have a 'getUILibraryTheme' instance function.]] - local Framework = script.Parent.Parent local Util = require(Framework.Util) local FlagsList = Util.Flags.new({ FFlagStudioDevFrameworkPackage = {"StudioDevFrameworkPackage"}, FFlagRefactorDevFrameworkContextItems = {"RefactorDevFrameworkContextItems"}, + FFlagRefactorDevFrameworkTheme = {"RefactorDevFrameworkTheme"}, }) +local noGetThemeError +if FlagsList:get("FFlagRefactorDevFrameworkTheme") then + noGetThemeError = [[ + UILibraryProvider expects Stylizer to have a 'getUILibraryTheme' instance function.]] +else + noGetThemeError = [[ + UILibraryProvider expects Theme to have a 'getUILibraryTheme' instance function.]] +end + local isUsedAsPackage = require(Framework.Util.isUsedAsPackage) local shouldGetUILibraryFromParent = not FlagsList:get("FFlagStudioDevFrameworkPackage") or @@ -39,6 +45,7 @@ end local Roact = require(Framework.Parent.Roact) local ContextItem = require(Framework.ContextServices.ContextItem) local mapToProps = require(Framework.ContextServices.mapToProps) +local Stylizer = require(Framework.Style.Stylizer) local Theme = require(Framework.ContextServices.Theme) local Plugin = require(Framework.ContextServices.Plugin) local Focus = require(Framework.ContextServices.Focus) @@ -48,7 +55,12 @@ local UILibraryProvider = Roact.PureComponent:extend("UILibraryProvider") function UILibraryProvider:render() local props = self.props local plugin = props.Plugin - local theme = props.Theme + local theme + if FlagsList:get("FFlagRefactorDevFrameworkTheme") then + theme = props.Stylizer + else + theme = props.Theme + end local focus = props.Focus local UILibrary = props.UILibrary @@ -64,7 +76,8 @@ function UILibraryProvider:render() end mapToProps(UILibraryProvider, { - Theme = Theme, + Stylizer = FlagsList:get("FFlagRefactorDevFrameworkTheme") and Stylizer or nil, + Theme = (not FlagsList:get("FFlagRefactorDevFrameworkTheme")) and Theme or nil, Plugin = Plugin, Focus = Focus, }) diff --git a/BuiltInPlugins/LocalizationTools/Packages/Framework/ContextServices/UILibraryWrapper.spec.lua b/BuiltInPlugins/LocalizationTools/Packages/Framework/ContextServices/UILibraryWrapper.spec.lua index 0823cb2280..d4bbb673db 100644 --- a/BuiltInPlugins/LocalizationTools/Packages/Framework/ContextServices/UILibraryWrapper.spec.lua +++ b/BuiltInPlugins/LocalizationTools/Packages/Framework/ContextServices/UILibraryWrapper.spec.lua @@ -2,8 +2,10 @@ return function() local Framework = script.Parent.Parent local Util = require(Framework.Util) + local Signal = Util.Signal local FlagsList = Util.Flags.new({ FFlagStudioDevFrameworkPackage = {"StudioDevFrameworkPackage"}, + FFlagRefactorDevFrameworkTheme = {"RefactorDevFrameworkTheme"}, }) local isUsedAsPackage = require(Framework.Util.isUsedAsPackage) @@ -18,6 +20,7 @@ return function() local Focus = require(Framework.ContextServices.Focus) local Theme = require(Framework.ContextServices.Theme) local UILibraryWrapper = require(script.Parent.UILibraryWrapper) + local StudioTheme = require(Framework.Style.Themes.StudioTheme) local WrapperStub = Roact.PureComponent:extend("UILibraryWrapper") @@ -29,12 +32,26 @@ return function() Wrapper = WrapperStub } + local function createThemeMock() + + if FlagsList:get("FFlagRefactorDevFrameworkTheme") then + return StudioTheme.mock() + else + local createStyles = function() + return {} + end + local getTheme = function() + return "Light" + end + local themeChanged = Signal.new() + return Theme.mock(createStyles, getTheme, themeChanged) + end + end + it("should expect a Plugin ContextItem provided above", function() local focus = Focus.new(Instance.new("ScreenGui")) local wrapper = UILibraryWrapper.new(UILibraryStub) - local theme = Theme.new(function() - return {} - end) + local theme = createThemeMock() function theme:getUILibraryTheme() return {} end @@ -55,9 +72,7 @@ return function() it("should expect a Focus ContextItem provided above", function() local plugin = Plugin.new({}) local wrapper = UILibraryWrapper.new(UILibraryStub) - local theme = Theme.new(function() - return {} - end) + local theme = createThemeMock() function theme:getUILibraryTheme() return {} end @@ -97,9 +112,7 @@ return function() local plugin = Plugin.new({}) local focus = Focus.new(Instance.new("ScreenGui")) local wrapper = UILibraryWrapper.new(UILibraryStub) - local theme = Theme.new(function() - return {} - end) + local theme = createThemeMock() local element = provide({ plugin, @@ -119,9 +132,7 @@ return function() local plugin = Plugin.new({}) local focus = Focus.new(Instance.new("ScreenGui")) local wrapper = UILibraryWrapper.new(UILibraryStub) - local theme = Theme.new(function() - return {} - end) + local theme = createThemeMock() function theme:getUILibraryTheme() return {} end diff --git a/BuiltInPlugins/LocalizationTools/Packages/Framework/ContextServices/mapToProps.lua b/BuiltInPlugins/LocalizationTools/Packages/Framework/ContextServices/mapToProps.lua index d857093479..c0018d114a 100644 --- a/BuiltInPlugins/LocalizationTools/Packages/Framework/ContextServices/mapToProps.lua +++ b/BuiltInPlugins/LocalizationTools/Packages/Framework/ContextServices/mapToProps.lua @@ -42,14 +42,32 @@ local function mapToProps(component, contextMap) string.format(missingRenderMessage, tostring(component))) assert(contextMap, "mapToProps expects a contextMap table.") + local __initWithContext = component.init component.__renderWithContext = component.render + function component:init(props) + for key, item in pairs(contextMap) do + if item.initConsumer then + item:initConsumer(self) + end + end + if __initWithContext then + __initWithContext(self, props) + end + end + function component:render() return Roact.createElement(Consumer, { ContextMap = contextMap, Render = function(items) - for key, item in pairs(items) do - self.props[key] = item + if items then + for key, item in pairs(items) do + if item.getConsumerItem then + self.props[key] = item:getConsumerItem(self) + else + self.props[key] = item + end + end end return self:__renderWithContext() end, diff --git a/BuiltInPlugins/LocalizationTools/Packages/Framework/ErrorReporter/Backtrace/BacktraceReport.lua b/BuiltInPlugins/LocalizationTools/Packages/Framework/ErrorReporter/Backtrace/BacktraceReport.lua index 4beb882892..0c54a85faa 100644 --- a/BuiltInPlugins/LocalizationTools/Packages/Framework/ErrorReporter/Backtrace/BacktraceReport.lua +++ b/BuiltInPlugins/LocalizationTools/Packages/Framework/ErrorReporter/Backtrace/BacktraceReport.lua @@ -29,9 +29,14 @@ BacktraceReport.__index = BacktraceReport function BacktraceReport.new() -- Return a basic report that has all the required fields + + -- os.date can return nil if given an invalid input; this input is always valid + -- so it is safe to force it to be `any`. + local date: any = os.date("!*t") + local self = { uuid = HttpService:GenerateGUID(false):lower(), - timestamp = os.time(os.date("!*t")), + timestamp = os.time(date), lang = "lua", langVersion = "Roblox" .. _VERSION, agent = "backtrace-Lua", @@ -70,7 +75,7 @@ function BacktraceReport:addAttributes(newAttributes) end function BacktraceReport:addAnnotations(newAnnotations) - assert(self.IAnnotations(newAnnotions), "Expected newAnnotions to be a table") + assert(self.IAnnotations(newAnnotations), "Expected newAnnotations to be a table") self.annotations = Cryo.Dictionary.join(self.annotations or {}, newAnnotations) end diff --git a/BuiltInPlugins/LocalizationTools/Packages/Framework/ErrorReporter/Backtrace/BacktraceReporter.spec.lua b/BuiltInPlugins/LocalizationTools/Packages/Framework/ErrorReporter/Backtrace/BacktraceReporter.spec.lua index 4a6406dcf8..1728ea98b3 100644 --- a/BuiltInPlugins/LocalizationTools/Packages/Framework/ErrorReporter/Backtrace/BacktraceReporter.spec.lua +++ b/BuiltInPlugins/LocalizationTools/Packages/Framework/ErrorReporter/Backtrace/BacktraceReporter.spec.lua @@ -8,7 +8,6 @@ return function() local Util = Framework.Util local Cryo = require(Util.Cryo) local deepEqual = require(Util.deepEqual) - local tutils = require(Util.Typecheck.tutils) local Networking = require(Framework.Http).Networking local mockErrorMessage = "index nil" @@ -413,15 +412,10 @@ return function() describe("updateSharedAnnotations()", function() local requestsSent local reporter - local requestBody beforeEach(function() requestsSent = 0 reporter = BacktraceReporter.new({ networking = createMockNetworking(function(requestObj) - local success, result = pcall(HttpService.JSONDecode, HttpService, requestObj.Body) - if success then - requestBody = result - end requestsSent = requestsSent + 1 return requestBodySuccess end), @@ -433,7 +427,6 @@ return function() reporter:stop() reporter = nil requestsSent = 0 - requestBody = nil end) it("should add the same annotations to all error reports", function() @@ -522,10 +515,8 @@ return function() it("should throw if new annotations are ill-formatted", function() local requestsSent = 0 - local requestBody = nil local reporter = BacktraceReporter.new({ networking = createMockNetworking(function(requestObj) - requestBody = HttpService:JSONDecode(requestObj.Body) requestsSent = requestsSent + 1 return requestBodySuccess end), @@ -556,10 +547,7 @@ return function() requestsSent = 0 reporter = BacktraceReporter.new({ networking = createMockNetworking(function(requestObj) - local success, result = pcall(HttpService.JSONDecode, HttpService, requestObj.Body) - if success then - requestBody = result - end + requestBody = requestObj.Body requestsSent = requestsSent + 1 return requestBodySuccess end), @@ -580,14 +568,16 @@ return function() end) it("should send logs if provided generateLogMethod and error report is successful", function() + local logText = "test log text" + generateLogFunc = function() - return "test log text" + return logText end reporter:reportErrorImmediately(mockErrorMessage, mockErrorStack) expect(requestsSent).to.equal(2) -- one for error, one for log - expect(requestBody[2]).to.equal(logText) + expect(requestBody).to.equal(logText) end) it("should not send log if generateLogMethod did not return a string", function() @@ -602,14 +592,15 @@ return function() it("should not send more than 1 log in logIntervalInSeconds provided", function() logInterval = 2 + local logText = "test log text" generateLogFunc = function() - return "test log text" + return logText end reporter:reportErrorImmediately(mockErrorMessage, mockErrorStack) expect(requestsSent).to.equal(2) -- one for error, one for log - expect(requestBody[2]).to.equal(logText) + expect(requestBody).to.equal(logText) reporter:reportErrorImmediately(mockErrorMessage, mockErrorStack) expect(requestsSent).to.equal(3) -- only one more, the error report @@ -617,4 +608,4 @@ return function() reporter:stop() end) end) -end \ No newline at end of file +end diff --git a/BuiltInPlugins/LocalizationTools/Packages/Framework/ErrorReporter/ErrorQueue.lua b/BuiltInPlugins/LocalizationTools/Packages/Framework/ErrorReporter/ErrorQueue.lua index 0caa0b57bc..34e7050cd6 100644 --- a/BuiltInPlugins/LocalizationTools/Packages/Framework/ErrorReporter/ErrorQueue.lua +++ b/BuiltInPlugins/LocalizationTools/Packages/Framework/ErrorReporter/ErrorQueue.lua @@ -1,7 +1,6 @@ local RunService = game:GetService("RunService") local Framework = script.Parent.Parent -local Cryo = require(Framework.packages.Cryo) -- replace when properly supporting packages local t = require(Framework.Util.Typecheck.t) local DEFAULT_QUEUE_TIME_LIMIT_SECONDS = 30 diff --git a/BuiltInPlugins/LocalizationTools/Packages/Framework/ErrorReporter/ErrorQueue.spec.lua b/BuiltInPlugins/LocalizationTools/Packages/Framework/ErrorReporter/ErrorQueue.spec.lua index 0a5836189d..f118538ed8 100644 --- a/BuiltInPlugins/LocalizationTools/Packages/Framework/ErrorReporter/ErrorQueue.spec.lua +++ b/BuiltInPlugins/LocalizationTools/Packages/Framework/ErrorReporter/ErrorQueue.spec.lua @@ -3,7 +3,6 @@ return function() local Framework = script.Parent.Parent local deepEqual = require(Framework.Util.deepEqual) - local tutils = require(Framework.Util.Typecheck.tutils) local errorsToAdd = { [1] = { diff --git a/BuiltInPlugins/LocalizationTools/Packages/Framework/ErrorReporter/StudioPluginErrorReporter.spec.lua b/BuiltInPlugins/LocalizationTools/Packages/Framework/ErrorReporter/StudioPluginErrorReporter.spec.lua index df525d2cb9..1e0ce2c308 100644 --- a/BuiltInPlugins/LocalizationTools/Packages/Framework/ErrorReporter/StudioPluginErrorReporter.spec.lua +++ b/BuiltInPlugins/LocalizationTools/Packages/Framework/ErrorReporter/StudioPluginErrorReporter.spec.lua @@ -2,7 +2,7 @@ return function() local HttpService = game:GetService("HttpService") local Framework = script.Parent.Parent - local mockPlugin = require(Framework.TestHelpers.Services.mockPlugin) + local MockPlugin = require(Framework.TestHelpers.Instances.MockPlugin) local Networking = require(Framework.Http.Networking) local Signal = require(Framework.Util.Signal) local StudioPluginErrorReporter = require(script.Parent.StudioPluginErrorReporter) @@ -20,7 +20,7 @@ return function() it("should configure its attributes from the appropriate services", function() local testingSecurityLevel = 6 - local testPlugin = mockPlugin.new() + local testPlugin = MockPlugin.new() testPlugin.Name = "builtin_Test.rbxm" local testError = { @@ -132,7 +132,7 @@ return function() }, }, }) - + reporter:report("This is an error", "builtin_test.rbxm") reporter:stop() @@ -168,10 +168,10 @@ return function() } local errorSignal = Signal.new() - local pluginA = mockPlugin.new() + local pluginA = MockPlugin.new() pluginA.Name = "builtin_TestA.rbxm" - local pluginB = mockPlugin.new() + local pluginB = MockPlugin.new() pluginB.Name = "builtin_TestB.rbxm" local reporterA = StudioPluginErrorReporter.new({ @@ -188,15 +188,13 @@ return function() networking = networkingImpl, errorSignal = errorSignal, }) - + local errMsg = "This is an error" local errStack = pluginA.Name .. ".Blah.Foo Line 15 - " .. errMsg - local errSource = "" - local errDetails = "" errorSignal:Fire(errMsg, errStack, "", "", 6) reporterA:stop() reporterB:stop() expect(numCalls).to.equal(1) expect(analyticsCalls).to.equal(1) end) -end \ No newline at end of file +end diff --git a/BuiltInPlugins/LocalizationTools/Packages/Framework/Examples/General.lua b/BuiltInPlugins/LocalizationTools/Packages/Framework/Examples/General.lua index a4260a11cc..d2ab6a12c7 100644 --- a/BuiltInPlugins/LocalizationTools/Packages/Framework/Examples/General.lua +++ b/BuiltInPlugins/LocalizationTools/Packages/Framework/Examples/General.lua @@ -29,23 +29,44 @@ local Plugin = ContextServices.Plugin local UIFolderData = require(Framework.UI.UIFolderData) local StudioUIFolderData = require(Framework.StudioUI.StudioUIFolderData) -local exampleData = { - { - name = "theme", - label = "Theme", - folderPrefix = "examples", - }, - { - name = "localization", - label = "Localization", - folderPrefix = "examples", - }, - { - name = "stylevalue", - label = "StyleValue", - folderPrefix = "examples", - }, -} +local Util = require(Framework.Util) +local FlagsList = Util.Flags.new({ + FFlagRefactorDevFrameworkTheme = {"RefactorDevFrameworkTheme"}, +}) + +local exampleData +if (FlagsList:get("FFlagRefactorDevFrameworkTheme")) then + exampleData = { + { + name = "stylizer", + label = "Stylizer (Theme2)", + folderPrefix = "examples", + }, + { + name = "localization", + label = "Localization", + folderPrefix = "examples", + }, + } +else + exampleData = { + { + name = "theme", + label = "Theme", + folderPrefix = "examples", + }, + { + name = "localization", + label = "Localization", + folderPrefix = "examples", + }, + { + name = "stylevalue", + label = "StyleValue", + folderPrefix = "examples", + }, + } +end local overrideUiExampleName = { ["Container"] = "Container and Decoration", diff --git a/BuiltInPlugins/LocalizationTools/Packages/Framework/Examples/General/stylizer.lua b/BuiltInPlugins/LocalizationTools/Packages/Framework/Examples/General/stylizer.lua new file mode 100644 index 0000000000..d031f015ed --- /dev/null +++ b/BuiltInPlugins/LocalizationTools/Packages/Framework/Examples/General/stylizer.lua @@ -0,0 +1,96 @@ +local Framework = script.Parent.Parent.Parent +local Roact = require(Framework.Parent.Roact) +local ContextServices = require(Framework.ContextServices) +local Plugin = ContextServices.Plugin +local Application = require(script.Application) + +local Dialog = require(Framework.StudioUI).Dialog + +local Cryo = require(Framework.Util.Cryo) + +local FrameworkStyle = require(Framework.Style) +local Colors = FrameworkStyle.Colors +local ui = FrameworkStyle.ComponentSymbols +local StyleKey = FrameworkStyle.StyleKey + +local StudioTheme = require(Framework.Style.Themes.StudioTheme) + +Colors = Cryo.Dictionary.join(Colors, { + Yellow = Color3.new(230, 230, 0), + Red = Color3.fromRGB(255, 0, 0), + Green = Color3.new(0, 255, 0), +}) + + +return function(plugin) + local pluginItem = Plugin.new(plugin) + + local styleRoot = StudioTheme.new() + styleRoot:extend({ + TextColor3 = StyleKey.MainText, + + [ui.Button] = { + BackgroundColor = StyleKey.DialogMainButton, + TextColor3 = StyleKey.Button, + }, + + [ui.Box] = { + BackgroundColor = StyleKey.Mid, + }, + + [ui.Dialog] = { + BackgroundColor = StyleKey.MainBackground, + + ["&Sub"] = { + BackgroundColor = Colors.lighter(Colors.Blue, 0.5), + TextColor3 = Colors.Black; + }, + }, + + Important = { + BackgroundColor = Colors.lighter(Colors.Red, 0.5), + TextColor3 = Colors.Red; + }, + }) + + local DemoApp = Roact.Component:extend("DemoApp") + + function DemoApp:init() + self.state = { + enabled = true, + } + + self.close = function() + self:setState({ + enabled = false, + }) + end + end + + function DemoApp:render() + local enabled = self.state.enabled + if not enabled then + return + end + return ContextServices.provide({pluginItem, styleRoot}, { + Main = Roact.createElement(Dialog, { + Enabled = enabled, + Title = "Stylizer (Theme) Example", + Size = Vector2.new(400, 600), + Resizable = false, + OnClose = self.close, + }, { + App = Roact.createElement(Application) + }) + }) + end + + local element = Roact.createElement(DemoApp) + local handle = Roact.mount(element) + + local function stop() + Roact.unmount(handle) + end + + return stop +end diff --git a/BuiltInPlugins/LocalizationTools/Packages/Framework/Examples/General/stylizer/Application.lua b/BuiltInPlugins/LocalizationTools/Packages/Framework/Examples/General/stylizer/Application.lua new file mode 100644 index 0000000000..770d76cd3f --- /dev/null +++ b/BuiltInPlugins/LocalizationTools/Packages/Framework/Examples/General/stylizer/Application.lua @@ -0,0 +1,67 @@ +--[[ + Application + +]] +local Framework = script.Parent.Parent.Parent.Parent +local ContextServices = require(Framework.ContextServices) +local Roact = require(Framework.Parent.Roact) +local Button = require(script.Parent.Button) +local Dialog = require(script.Parent.Dialog) +local Stylizer = require(Framework.Style).Stylizer + +-- Application +local Application = Roact.PureComponent:extend("Application") + +function Application:init() + self.state = { + className = "Important" + } + + self.changeStyle = function() + local c = "Sub" + if self.state.className == c then + c = "Important" + end + self:setState({ + className = c + }) + end +end + +function Application:render() + return Roact.createElement("Frame", { + Size = UDim2.fromScale(1, 1), + }, { + UIListLayout = Roact.createElement("UIListLayout", { + FillDirection = Enum.FillDirection.Vertical, + HorizontalAlignment = Enum.HorizontalAlignment.Center, + Padding = UDim.new(0, 20), + SortOrder = Enum.SortOrder.LayoutOrder, + VerticalAlignment = Enum.VerticalAlignment.Center, + }), + + Dialog1 = Roact.createElement(Dialog, { + LayoutOrder = 1, + Size = UDim2.fromOffset(300, 150), + Style = self.state.className, + }), + + Dialog2 = Roact.createElement(Dialog, { + LayoutOrder = 2, + Size = UDim2.fromOffset(300, 150), + }), + + Button = Roact.createElement(Button, { + LayoutOrder = 3, + Size = UDim2.fromOffset(300, 60), + Text = "Click to change Style prop for Dialog 2", + OnClick = self.changeStyle, + }) + }) +end + +ContextServices.mapToProps(Application, { + Stylizer = Stylizer +}) + +return Application \ No newline at end of file diff --git a/BuiltInPlugins/LocalizationTools/Packages/Framework/Examples/General/stylizer/Box.lua b/BuiltInPlugins/LocalizationTools/Packages/Framework/Examples/General/stylizer/Box.lua new file mode 100644 index 0000000000..e1554580ab --- /dev/null +++ b/BuiltInPlugins/LocalizationTools/Packages/Framework/Examples/General/stylizer/Box.lua @@ -0,0 +1,38 @@ +--[[ + Box +]] + +local Framework = script.Parent.Parent.Parent.Parent +local ContextServices = require(Framework.ContextServices) +local Roact = require(Framework.Parent.Roact) +local Stylizer = require(Framework.Style).Stylizer + +-- In the component +local Box = Roact.PureComponent:extend("Box") + +function Box:render() + local style = self.props.Stylizer + + return Roact.createElement("Frame", { + Size = self.props.Size, + Position = self.props.Position, + BorderSizePixel = 0, + BackgroundColor3 = style.BackgroundColor, + BackgroundTransparency = 0, + LayoutOrder = self.props.LayoutOrder or 0, + }, { + FrontText = Roact.createElement("TextLabel", { + Text = style:getPathString(), + Size = UDim2.new(1, 0, 1, -60), + BackgroundTransparency = 1, + LayoutOrder = 1, + TextColor3 = style.TextColor3, + }), + }) +end + +ContextServices.mapToProps(Box, { + Stylizer = Stylizer +}) + +return Box \ No newline at end of file diff --git a/BuiltInPlugins/LocalizationTools/Packages/Framework/Examples/General/stylizer/Button.lua b/BuiltInPlugins/LocalizationTools/Packages/Framework/Examples/General/stylizer/Button.lua new file mode 100644 index 0000000000..8e7c36daf6 --- /dev/null +++ b/BuiltInPlugins/LocalizationTools/Packages/Framework/Examples/General/stylizer/Button.lua @@ -0,0 +1,32 @@ +--[[ + Button +]] + +local Framework = script.Parent.Parent.Parent.Parent +local ContextServices = require(Framework.ContextServices) +local Roact = require(Framework.Parent.Roact) +local Stylizer = require(Framework.Style).Stylizer + +-- In the component +local Button = Roact.PureComponent:extend("Button") + +function Button:render() + local style = self.props.Stylizer + + return Roact.createElement("TextButton", { + Size = self.props.Size, + Position = self.props.Position, + BackgroundColor3 = style.BackgroundColor, + BackgroundTransparency = 0, + LayoutOrder = self.props.LayoutOrder or 0, + Text = self.props.Text or style:getPathString(), + TextColor3 = style.TextColor3, + [Roact.Event.Activated] = self.props.OnClick, + }) +end + +ContextServices.mapToProps(Button, { + Stylizer = Stylizer +}) + +return Button \ No newline at end of file diff --git a/BuiltInPlugins/LocalizationTools/Packages/Framework/Examples/General/stylizer/Dialog.lua b/BuiltInPlugins/LocalizationTools/Packages/Framework/Examples/General/stylizer/Dialog.lua new file mode 100644 index 0000000000..dcc65b1db3 --- /dev/null +++ b/BuiltInPlugins/LocalizationTools/Packages/Framework/Examples/General/stylizer/Dialog.lua @@ -0,0 +1,47 @@ +--[[ + Dialog +]] + +local Framework = script.Parent.Parent.Parent.Parent +local ContextServices = require(Framework.ContextServices) +local Roact = require(Framework.Parent.Roact) +local Stylizer = require(Framework.Style).Stylizer +local Box = require(script.Parent.Box) + +-- In the component +local Dialog = Roact.PureComponent:extend("Dialog") + +function Dialog:render() + local style = self.props.Stylizer + + return Roact.createElement("Frame", { + Size = self.props.Size, + Position = self.props.Position, + BackgroundColor3 = style.BackgroundColor, + BackgroundTransparency = 0, + }, { + Layout = Roact.createElement("UIListLayout", { + SortOrder = Enum.SortOrder.LayoutOrder, + HorizontalAlignment = Enum.HorizontalAlignment.Center, + }), + + FrontText = Roact.createElement("TextLabel", { + Text = style:getPathString(), + Size = UDim2.new(1, 0, 1, -60), + BackgroundTransparency = 1, + LayoutOrder = 1, + TextColor3 = style.TextColor3, + }), + + Box = Roact.createElement(Box, { + Position = UDim2.fromOffset(10, 10), + Size = UDim2.new(1, -20, 0, 100), + }) + }) +end + +ContextServices.mapToProps(Dialog, { + Stylizer = Stylizer +}) + +return Dialog diff --git a/BuiltInPlugins/LocalizationTools/Packages/Framework/Http/Networking.lua b/BuiltInPlugins/LocalizationTools/Packages/Framework/Http/Networking.lua index 9801436482..3f59fb1f4a 100644 --- a/BuiltInPlugins/LocalizationTools/Packages/Framework/Http/Networking.lua +++ b/BuiltInPlugins/LocalizationTools/Packages/Framework/Http/Networking.lua @@ -64,7 +64,6 @@ local HttpResponse = require(script.Parent.HttpResponse) local StatusCodes = require(script.Parent.StatusCodes) local FFlagStudioFixFrameworkJsonParsing = game:DefineFastFlag("StudioFixFrameworkJsonParsing", true) -local FFlagStudioFixFrameworkClientErrorRetries = game:DefineFastFlag("StudioFixFrameworkClientErrorRetries", false) local FFlagStudioFixFrameworkNonIdempotentRetries = game:DefineFastFlag("StudioFixFrameworkNonIdempotentRetries", false) local LOGGING_CHANNELS = { @@ -444,15 +443,13 @@ function Networking:handleRetry(requestPromise, numRetries, disableBackoff) return end - if FFlagStudioFixFrameworkClientErrorRetries then - -- Do not retry on HTTP 4xx (client) errors - if errResponse.responseCode >= 400 and errResponse.responseCode < 500 then - if self:_isLoggingEnabled(LOGGING_CHANNELS.RESPONSES) then - print("4xx error response. Rejecting request.") - end - reject(errResponse) - return + -- Do not retry on HTTP 4xx (client) errors + if errResponse.responseCode >= 400 and errResponse.responseCode < 500 then + if self:_isLoggingEnabled(LOGGING_CHANNELS.RESPONSES) then + print("4xx error response. Rejecting request.") end + reject(errResponse) + return end if FFlagStudioFixFrameworkNonIdempotentRetries then diff --git a/BuiltInPlugins/LocalizationTools/Packages/Framework/Http/Networking.spec.lua b/BuiltInPlugins/LocalizationTools/Packages/Framework/Http/Networking.spec.lua index 25379907ae..68395bcab6 100644 --- a/BuiltInPlugins/LocalizationTools/Packages/Framework/Http/Networking.spec.lua +++ b/BuiltInPlugins/LocalizationTools/Packages/Framework/Http/Networking.spec.lua @@ -3,7 +3,6 @@ return function() local HttpService = game:GetService("HttpService") local FFlagStudioFixFrameworkJsonParsing = game:GetFastFlag("StudioFixFrameworkJsonParsing") - local FFlagStudioFixFrameworkClientErrorRetries = game:GetFastFlag("StudioFixFrameworkClientErrorRetries") local FFlagStudioFixFrameworkNonIdempotentRetries = game:GetFastFlag("StudioFixFrameworkNonIdempotentRetries") describe("new()", function() @@ -43,7 +42,7 @@ return function() onRequest = function(requestOptions) callCount = callCount + 1 expect(requestOptions.Url).to.equal("https://www.test.com/fakeApi") - + return { Body = "hello world", Success = true, @@ -177,7 +176,7 @@ return function() n:parseJson(httpPromise):andThen(function(json) didResolve = true end, function(err) - expect(string.find(err, "Can't parse JSON")).to.never.equal(nil) + expect((string.find(err, "Can't parse JSON"))).to.never.equal(nil) didError = true end) @@ -340,32 +339,30 @@ return function() expect(callCount).to.equal(2) -- 1 original + 1 retries end) - if FFlagStudioFixFrameworkClientErrorRetries then - it("should not retry on 4xx errors", function() - local callCount = 0 - - local n = Networking.mock({ - onRequest = function(requestOptions) - callCount = callCount + 1 - return { - Body = "{ \"message\":\"foo\" }", - Success = false, - StatusMessage = "Bad Request", - StatusCode = 400, - } - end, - }) + it("should not retry on 4xx errors", function() + local callCount = 0 - local didError = false - local httpPromise = n:get("https://www.example.com") - n:handleRetry(httpPromise, 3, true):catch(function() - didError = true - end) + local n = Networking.mock({ + onRequest = function(requestOptions) + callCount = callCount + 1 + return { + Body = "{ \"message\":\"foo\" }", + Success = false, + StatusMessage = "Bad Request", + StatusCode = 400, + } + end, + }) - expect(didError).to.equal(true) - expect(callCount).to.equal(1) + local didError = false + local httpPromise = n:get("https://www.example.com") + n:handleRetry(httpPromise, 3, true):catch(function() + didError = true end) - end + + expect(didError).to.equal(true) + expect(callCount).to.equal(1) + end) if FFlagStudioFixFrameworkNonIdempotentRetries then it("should not retry POST requests", function() @@ -687,4 +684,4 @@ return function() expect(callCount).to.equal(1) end) end) -end \ No newline at end of file +end diff --git a/BuiltInPlugins/LocalizationTools/Packages/Framework/RobloxAPI.lua b/BuiltInPlugins/LocalizationTools/Packages/Framework/RobloxAPI.lua index 51a6eea30a..76d02cdab3 100644 --- a/BuiltInPlugins/LocalizationTools/Packages/Framework/RobloxAPI.lua +++ b/BuiltInPlugins/LocalizationTools/Packages/Framework/RobloxAPI.lua @@ -40,7 +40,7 @@ local FFlagDevFrameworkStrictAPITables = game:DefineFastFlag("DevFrameworkStrict -- helper functions -- dir : (Instance) a Folder to dig through -- ... : (Variant) any number of arguments to initialize the children with -local function initDirectoryWithArgs(dir, ...) +local function initDirectoryWithArgs(dir, networkingImpl, baseUrl) --[[ When pointed at an Instance, will recurse through the children to initialize all of the required elements with the arguments supplied to this function. @@ -51,11 +51,11 @@ local function initDirectoryWithArgs(dir, ...) local childrenMap = {} for _, child in ipairs(dir:GetChildren()) do if child.ClassName == "Folder" then - childrenMap[child.Name] = initDirectoryWithArgs(child, ...) + childrenMap[child.Name] = initDirectoryWithArgs(child, networkingImpl, baseUrl) elseif child.ClassName == "ModuleScript" then local targetFunction = require(child) - childrenMap[child.Name] = targetFunction(...) + childrenMap[child.Name] = targetFunction(networkingImpl, baseUrl) else warn(string.format("Unexpected object found when constructing children table : %s", child:GetFullName())) @@ -110,6 +110,7 @@ function RobloxAPI.new(props) Develop = initDirectoryWithArgs(script.Develop, networkingImpl, baseUrl), TranslationRoles = initDirectoryWithArgs(script.TranslationRoles, networkingImpl, baseUrl), WWW = initDirectoryWithArgs(script.WWW, networkingImpl, baseUrl), + ToolboxService = initDirectoryWithArgs(script.ToolboxService, networkingImpl, baseUrl), -- add more endpoint domains here } setmetatable(robloxApi, RobloxAPI) @@ -121,4 +122,4 @@ function RobloxAPI:baseURLHasChineseHost() return StudioService:BaseURLHasChineseHost() end -return RobloxAPI \ No newline at end of file +return RobloxAPI diff --git a/BuiltInPlugins/LocalizationTools/Packages/Framework/RobloxAPI/GameInternationalization/V1/LocalizationTable/Games/assetsGenerationRequest.lua b/BuiltInPlugins/LocalizationTools/Packages/Framework/RobloxAPI/GameInternationalization/V1/LocalizationTable/Games/assetsGenerationRequest.lua index c1d11b0958..faebe00916 100644 --- a/BuiltInPlugins/LocalizationTools/Packages/Framework/RobloxAPI/GameInternationalization/V1/LocalizationTable/Games/assetsGenerationRequest.lua +++ b/BuiltInPlugins/LocalizationTools/Packages/Framework/RobloxAPI/GameInternationalization/V1/LocalizationTable/Games/assetsGenerationRequest.lua @@ -14,10 +14,6 @@ return function(networkingImpl, baseUrl) baseUrl.GAMES_INTERNATIONALIZATION_URL, string.format("v1/localizationtable/games/%d/assets-generation-request", gameId)) - local headers = { - ["Content-Type"] = "application/json" - } - return { getUrl = function() return url @@ -29,4 +25,4 @@ return function(networkingImpl, baseUrl) end, } end -end \ No newline at end of file +end diff --git a/BuiltInPlugins/LocalizationTools/Packages/Framework/RobloxAPI/ToolboxService/V1/Items/details.lua b/BuiltInPlugins/LocalizationTools/Packages/Framework/RobloxAPI/ToolboxService/V1/Items/details.lua new file mode 100644 index 0000000000..040d72eb77 --- /dev/null +++ b/BuiltInPlugins/LocalizationTools/Packages/Framework/RobloxAPI/ToolboxService/V1/Items/details.lua @@ -0,0 +1,51 @@ +--[[ + Returns details of Toolbox items. + Example Request : + { + "items": [ + { + "id": 1, + "itemType": "Asset" + } + ] + } + + Example : + https://apis.roblox.com/toolbox-service/v1/items/details +]] + +local HttpService = game:GetService("HttpService") + +local Framework = script.Parent.Parent.Parent.Parent.Parent +local t = require(Framework.Util.Typecheck.t) + +-- networkingImpl : (Http.Networking) supplied by RobloxAPI.init.lua, a Networking object that makes the network requests +-- baseUrl : (RobloxAPI.Url) supplied by RobloxAPI.init.lua, an object for constructing urls +return function(networkingImpl, baseUrl) + + return function(itemsToRequest) + assert(t.strictInterface({ + items = t.array(t.strictInterface({ + id = t.integer, + itemType = t.string, + })) + })(itemsToRequest), "Request does not match expected format") + + local url = baseUrl.composeUrl(baseUrl.APIS_URL, "toolbox-service/v1/items/details") + + return { + getUrl = function() + return url + end, + + makeRequest = function() + local httpPromise = networkingImpl:post(url, HttpService:JSONEncode(itemsToRequest), { + ["Content-Type"] = "application/json", + }) + -- TODO DEVTOOLS-4914: This will not retry because POST is non-idempotent in REST, but this endpoint is apparently + -- only POST method to facilitate passing a request body, so do we want to retry it? + return networkingImpl:parseJson(networkingImpl:handleRetry(httpPromise)) + end, + } + end +end \ No newline at end of file diff --git a/BuiltInPlugins/LocalizationTools/Packages/Framework/RobloxAPI/Url.lua b/BuiltInPlugins/LocalizationTools/Packages/Framework/RobloxAPI/Url.lua index 66f1bc52b1..9319c34e92 100644 --- a/BuiltInPlugins/LocalizationTools/Packages/Framework/RobloxAPI/Url.lua +++ b/BuiltInPlugins/LocalizationTools/Packages/Framework/RobloxAPI/Url.lua @@ -63,6 +63,7 @@ function Url.new(baseUrl) BASE_URL = _baseUrl, -- https://www.roblox.com/ API_URL = string.format("https://api.%s", _baseDomain), + APIS_URL = string.format("https://apis.%s", _baseDomain), ASSET_GAME_URL = string.format("https://assetgame.%s", _baseDomain), AUTH_URL = string.format("https://auth.%s", _baseDomain), CATALOG_URL = string.format("https://catalog.%s", _baseDomain), @@ -94,7 +95,7 @@ function Url.composeUrl(base, path, args) assert(type(path) == "string", "Expected 'path' to be a string.") if args then assert(type(args) == "table", "Expected 'args' to be a map.") - assert(type(next(args)) == "string", "Expected 'args' to be map, not an array.") + assert(type((next(args))) == "string", "Expected 'args' to be map, not an array.") end -- append a slash to the end if the base doesn't have one @@ -133,4 +134,4 @@ function Url.composeUrl(base, path, args) return string.format("%s%s%s", base, path, argString) end -return Url \ No newline at end of file +return Url diff --git a/BuiltInPlugins/LocalizationTools/Packages/Framework/RobloxAPI/Url.spec.lua b/BuiltInPlugins/LocalizationTools/Packages/Framework/RobloxAPI/Url.spec.lua index b98f32f8f5..bf00b44158 100644 --- a/BuiltInPlugins/LocalizationTools/Packages/Framework/RobloxAPI/Url.spec.lua +++ b/BuiltInPlugins/LocalizationTools/Packages/Framework/RobloxAPI/Url.spec.lua @@ -1,7 +1,6 @@ return function() local ContentProvider = game:GetService("ContentProvider") local RobloxAPI = script.Parent - local FrameworkRoot = RobloxAPI.Parent local Url = require(RobloxAPI.Url) it("has not changed the base url for debugging", function() @@ -19,6 +18,7 @@ return function() it("should construct all of the baseUrls based on the current environment", function() local baseUrl = Url.new("https://www.roblox.com") expect(baseUrl.API_URL).to.equal("https://api.roblox.com/") + expect(baseUrl.APIS_URL).to.equal("https://apis.roblox.com/") expect(baseUrl.ASSET_GAME_URL).to.equal("https://assetgame.roblox.com/") expect(baseUrl.AUTH_URL).to.equal("https://auth.roblox.com/") expect(baseUrl.CATALOG_URL).to.equal("https://catalog.roblox.com/") @@ -96,27 +96,27 @@ return function() expect(function() Url.composeUrl(nil, validPath, validArgs) end).to.throw() expect(function() Url.composeUrl(123, validPath, validArgs) end).to.throw() expect(function() Url.composeUrl({}, validPath, validArgs) end).to.throw() - expect(function() Url.composeUrl(newproxy(), validPath, validArgs) end).to.throw() + expect(function() Url.composeUrl(newproxy(true), validPath, validArgs) end).to.throw() expect(function() Url.composeUrl(true, validPath, validArgs) end).to.throw() -- path expect(function() Url.composeUrl(validBase, nil, validArgs) end).to.throw() expect(function() Url.composeUrl(validBase, 123, validArgs) end).to.throw() expect(function() Url.composeUrl(validBase, {}, validArgs) end).to.throw() - expect(function() Url.composeUrl(validBase, newproxy(), validArgs) end).to.throw() + expect(function() Url.composeUrl(validBase, newproxy(true), validArgs) end).to.throw() expect(function() Url.composeUrl(validBase, true, validArgs) end).to.throw() -- args expect(function() Url.composeUrl(validBase, validPath, 123) end).to.throw() expect(function() Url.composeUrl(validBase, validPath, "123") end).to.throw() - expect(function() Url.composeUrl(validBase, validPath, newproxy()) end).to.throw() + expect(function() Url.composeUrl(validBase, validPath, newproxy(true)) end).to.throw() expect(function() Url.composeUrl(validBase, validPath, true) end).to.throw() end) it("should throw errors for invalid argument datatypes", function() -- userdata expect(function() - Url.composeUrl("https://www.test.com/", "a/b/c", { d = newproxy() }) + Url.composeUrl("https://www.test.com/", "a/b/c", { d = newproxy(true) }) end).to.throw() -- maps @@ -130,4 +130,4 @@ return function() end).to.throw() end) end) -end \ No newline at end of file +end diff --git a/BuiltInPlugins/LocalizationTools/Packages/Framework/RobloxAPI/WWW/Develop/library.lua b/BuiltInPlugins/LocalizationTools/Packages/Framework/RobloxAPI/WWW/Develop/library.lua index 51e81df9dc..b84bc0f6b7 100644 --- a/BuiltInPlugins/LocalizationTools/Packages/Framework/RobloxAPI/WWW/Develop/library.lua +++ b/BuiltInPlugins/LocalizationTools/Packages/Framework/RobloxAPI/WWW/Develop/library.lua @@ -55,7 +55,7 @@ local function assertInEnum(valueName, value, enum) end if enum[value] ~= nil then - error(string.format("Expected %s to be a valid enum value."), 1) + error(string.format("Expected %s to be a valid enum value.", valueName), 1) end end @@ -88,4 +88,4 @@ return function(_, baseUrl) end, } end -end \ No newline at end of file +end diff --git a/BuiltInPlugins/LocalizationTools/Packages/Framework/RobloxAPI/init.spec.lua b/BuiltInPlugins/LocalizationTools/Packages/Framework/RobloxAPI/init.spec.lua index ae5295ba66..ab5675b0d0 100644 --- a/BuiltInPlugins/LocalizationTools/Packages/Framework/RobloxAPI/init.spec.lua +++ b/BuiltInPlugins/LocalizationTools/Packages/Framework/RobloxAPI/init.spec.lua @@ -19,7 +19,7 @@ return function() expect(api).to.be.ok() - local Url = Url.new("https://www.roblox.com") + local url = Url.new("https://www.roblox.com") api = RobloxAPI.new({ baseUrl = url, }) @@ -33,4 +33,4 @@ return function() expect(api).to.be.ok() end) end) -end \ No newline at end of file +end diff --git a/BuiltInPlugins/LocalizationTools/Packages/Framework/StudioUI/PluginButton/test.spec.lua b/BuiltInPlugins/LocalizationTools/Packages/Framework/StudioUI/PluginButton/test.spec.lua index 77e96550f7..1f3f35d834 100644 --- a/BuiltInPlugins/LocalizationTools/Packages/Framework/StudioUI/PluginButton/test.spec.lua +++ b/BuiltInPlugins/LocalizationTools/Packages/Framework/StudioUI/PluginButton/test.spec.lua @@ -3,23 +3,10 @@ return function() local Roact = require(Framework.Parent.Roact) local PluginButton = require(script.Parent) - local function mockToolbar() - local toolbar = { - CreateButton = function() - return { - Click = { - Connect = function() - end, - }, - SetActive = function() - end, - Destroy = function() - end, - } - end, - } + local MockPluginToolbar = require(Framework.TestHelpers.Instances.MockPluginToolbar) - return toolbar + local function mockToolbar() + return MockPluginToolbar.new(nil, "") end it("should create and destroy without errors", function() @@ -55,4 +42,4 @@ return function() Roact.unmount(instance) end).to.throw() end) -end \ No newline at end of file +end diff --git a/BuiltInPlugins/LocalizationTools/Packages/Framework/StudioUI/PluginToolbar.lua b/BuiltInPlugins/LocalizationTools/Packages/Framework/StudioUI/PluginToolbar.lua index 463234b55a..fb2d51d932 100644 --- a/BuiltInPlugins/LocalizationTools/Packages/Framework/StudioUI/PluginToolbar.lua +++ b/BuiltInPlugins/LocalizationTools/Packages/Framework/StudioUI/PluginToolbar.lua @@ -42,6 +42,8 @@ function PluginToolbar:render() if children then return Roact.createFragment(children) end + + return nil end function PluginToolbar:willUnmount() diff --git a/BuiltInPlugins/LocalizationTools/Packages/Framework/StudioUI/PluginToolbar/test.spec.lua b/BuiltInPlugins/LocalizationTools/Packages/Framework/StudioUI/PluginToolbar/test.spec.lua index 4deb1abf05..3094a34fad 100644 --- a/BuiltInPlugins/LocalizationTools/Packages/Framework/StudioUI/PluginToolbar/test.spec.lua +++ b/BuiltInPlugins/LocalizationTools/Packages/Framework/StudioUI/PluginToolbar/test.spec.lua @@ -4,13 +4,10 @@ return function() local PluginToolbar = require(script.Parent) local ContextServices = require(Framework.ContextServices) - local function mockPlugin() - local plugin = { - CreateToolbar = function() - end, - } + local MockPlugin = require(Framework.TestHelpers.Instances.MockPlugin) - return ContextServices.Plugin.new(plugin) + local function mockPlugin() + return ContextServices.Plugin.new(MockPlugin.new()) end it("should create and destroy without errors", function() @@ -88,4 +85,4 @@ return function() Roact.unmount(instance) end) -end \ No newline at end of file +end diff --git a/BuiltInPlugins/LocalizationTools/Packages/Framework/StudioUI/SearchBar.lua b/BuiltInPlugins/LocalizationTools/Packages/Framework/StudioUI/SearchBar.lua index e480f2119e..3712dbed8b 100644 --- a/BuiltInPlugins/LocalizationTools/Packages/Framework/StudioUI/SearchBar.lua +++ b/BuiltInPlugins/LocalizationTools/Packages/Framework/StudioUI/SearchBar.lua @@ -1,9 +1,6 @@ --[[ A search bar component with a single line TextInput and button to request a search. - Required Props: - Theme Theme: A Theme ContextItem, which is provided via mapToProps. - Optional Props: number Width: how wide the search bar is in pixels. number ButtonWidth: how wide the search button is in pixels. @@ -17,7 +14,8 @@ boolean ShowSearchButton: Whether to show the search button at the right of the bar (default true). Style Style: The style with which to render this component. StyleModifier StyleModifier: The StyleModifier index into Style. - + Stylizer Stylizer: A Stylizer ContextItem, which is provided via mapToProps. + Theme Theme: A Theme ContextItem, which is provided via mapToProps. Style Values: Style BackgroundStyle: The style with which to render the background. @@ -26,13 +24,17 @@ number TextSize: The font size of the text in this link. Color3 TextColor: The color of the search term text. ]] - local Framework = script.Parent.Parent local Roact = require(Framework.Parent.Roact) local ContextServices = require(Framework.ContextServices) -local Typecheck = require(Framework.Util).Typecheck + local Util = require(Framework.Util) +local Typecheck = Util.Typecheck local StyleModifier = Util.StyleModifier +local FlagsList = Util.Flags.new({ + FFlagRefactorDevFrameworkTheme = {"RefactorDevFrameworkTheme"}, +}) + local UI = require(Framework.UI) local Button = UI.Button local Container = UI.Container @@ -205,7 +207,12 @@ function SearchBar:render() end local theme = props.Theme - local style = theme:getStyle("Framework", self) + local style + if FlagsList:get("FFlagRefactorDevFrameworkTheme") then + style = props.Stylizer + else + style = theme:getStyle("Framework", self) + end local backgroundStyle = style.BackgroundStyle local padding = style.Padding @@ -282,7 +289,8 @@ function SearchBar:render() end ContextServices.mapToProps(SearchBar, { - Theme = ContextServices.Theme, + Stylizer = FlagsList:get("FFlagRefactorDevFrameworkTheme") and ContextServices.Stylizer or nil, + Theme = (not FlagsList:get("FFlagRefactorDevFrameworkTheme")) and ContextServices.Theme or nil, }) return SearchBar diff --git a/BuiltInPlugins/LocalizationTools/Packages/Framework/StudioUI/SearchBar/style.lua b/BuiltInPlugins/LocalizationTools/Packages/Framework/StudioUI/SearchBar/style.lua index f77b14a209..6ed85a086b 100644 --- a/BuiltInPlugins/LocalizationTools/Packages/Framework/StudioUI/SearchBar/style.lua +++ b/BuiltInPlugins/LocalizationTools/Packages/Framework/StudioUI/SearchBar/style.lua @@ -4,8 +4,14 @@ local UI = require(Framework.UI) local Decoration = UI.Decoration local Util = require(Framework.Util) +local Cryo = Util.Cryo +local deepCopy = Util.deepCopy local Style = Util.Style local StyleModifier = Util.StyleModifier +local StyleKey = require(Framework.Style.StyleKey) +local FlagsList = Util.Flags.new({ + FFlagRefactorDevFrameworkTheme = {"RefactorDevFrameworkTheme"}, +}) local Common = require(Framework.StudioUI.StudioFrameworkStyles.Common) @@ -14,33 +20,51 @@ local RoundBox = require(UIFolderData.RoundBox.style) local FFlagDevFrameworkTextInputContainer = game:GetFastFlag("DevFrameworkTextInputContainer") -return function(theme, getColor) - local common = Common(theme, getColor) - local roundBox = RoundBox(theme, getColor) +local function buttonStyle(image, hoverImage, theme) + local hoverStyle - local function buttonStyle(image, hoverImage) - return Style.new({ - Foreground = Decoration.Image, - ForegroundStyle = { - AnchorPoint = Vector2.new(0.5, 0.5), - Position = UDim2.new(0.5, 0, 0.5, 0), - Color = Color3.fromRGB(184, 184, 184), - Image = image, - Size = UDim2.new(0.6, 0, 0.6, 0), - ScaleType = Enum.ScaleType.Fit - }, - [StyleModifier.Hover] = { - ForegroundStyle = { - Image = hoverImage, - Color = FFlagDevFrameworkTextInputContainer and theme:GetColor("DialogMainButton") or Color3.fromRGB(0, 162, 255) - }, - }, - }) + if FFlagDevFrameworkTextInputContainer then + if FlagsList:get("FFlagRefactorDevFrameworkTheme") then + hoverStyle = StyleKey.DialogMainButton + else + hoverStyle = theme:GetColor("DialogMainButton") + end + else + hoverStyle = Color3.fromRGB(0, 162, 255) end - local Default = Style.extend(common.MainText, common.Border, { - BackgroundColor = common.Background.Color, - BackgroundStyle = roundBox.Default, + local foregroundStyle = { + AnchorPoint = Vector2.new(0.5, 0.5), + Position = UDim2.new(0.5, 0, 0.5, 0), + Color = Color3.fromRGB(184, 184, 184), + Image = image, + Size = UDim2.new(0.6, 0, 0.6, 0), + ScaleType = Enum.ScaleType.Fit + } + + local style = { + Foreground = Decoration.Image, + ForegroundStyle = foregroundStyle, + [StyleModifier.Hover] = { + ForegroundStyle = Cryo.Dictionary.join(foregroundStyle, { + Image = hoverImage, + Color = hoverStyle + }), + }, + } + + if FlagsList:get("FFlagRefactorDevFrameworkTheme") then + return style + else + return Style.new(style) + end +end + +if FlagsList:get("FFlagRefactorDevFrameworkTheme") then + local roundBox = deepCopy(RoundBox) + return { + BackgroundColor = StyleKey.MainBackground, + BackgroundStyle = roundBox, Padding = FFlagDevFrameworkTextInputContainer and { Top = 5, Left = 10, @@ -52,16 +76,47 @@ return function(theme, getColor) Bottom = 0, Right = 10 }, + [StyleModifier.Hover] = { - BackgroundStyle = Style.extend(roundBox.Default, common.BorderHover) + BackgroundStyle = Cryo.Dictionary.join(roundBox, { + BorderColor = StyleKey.DialogMainButton, + }) }, Buttons = { Clear = buttonStyle("rbxasset://textures/StudioSharedUI/clear.png", "rbxasset://textures/StudioSharedUI/clear-hover.png"), Search = buttonStyle("rbxasset://textures/StudioSharedUI/search.png"), }, - }) - - return { - Default = Default, } -end +else + return function(theme, getColor) + local common = Common(theme, getColor) + local roundBox = RoundBox(theme, getColor) + + local Default = Style.extend(common.MainText, common.Border, { + BackgroundColor = common.Background.Color, + BackgroundStyle = roundBox.Default, + Padding = FFlagDevFrameworkTextInputContainer and { + Top = 5, + Left = 10, + Bottom = 5, + Right = 10 + } or { + Top = 0, + Left = 10, + Bottom = 0, + Right = 10 + }, + [StyleModifier.Hover] = { + BackgroundStyle = Style.extend(roundBox.Default, common.BorderHover) + }, + Buttons = { + Clear = buttonStyle("rbxasset://textures/StudioSharedUI/clear.png", "rbxasset://textures/StudioSharedUI/clear-hover.png", theme), + Search = buttonStyle("rbxasset://textures/StudioSharedUI/search.png", nil, theme), + }, + }) + + return { + Default = Default, + } + end +end \ No newline at end of file diff --git a/BuiltInPlugins/LocalizationTools/Packages/Framework/StudioUI/StudioFrameworkStyles.lua b/BuiltInPlugins/LocalizationTools/Packages/Framework/StudioUI/StudioFrameworkStyles.lua index 6a660eb6e0..1116523861 100644 --- a/BuiltInPlugins/LocalizationTools/Packages/Framework/StudioUI/StudioFrameworkStyles.lua +++ b/BuiltInPlugins/LocalizationTools/Packages/Framework/StudioUI/StudioFrameworkStyles.lua @@ -35,12 +35,21 @@ local UIFolderData = require(Framework.UI.UIFolderData) local StudioUIFolderData = require(Framework.StudioUI.StudioUIFolderData) local Util = require(Framework.Util) +local FlagsList = Util.Flags.new({ + FFlagRefactorDevFrameworkTheme = {"RefactorDevFrameworkTheme"}, +}) + local FrameworkStyles = UI.FrameworkStyles local StyleTable = Util.StyleTable local StudioFrameworkStyles = {} + function StudioFrameworkStyles.new(theme, getColor) + if FlagsList:get("FFlagRefactorDevFrameworkTheme") then + return {} + end + assert(theme, "StudioFrameworkStyles.new expects a 'theme' parameter.") assert(type(getColor) == "function", "StudioFrameworkStyles.new expects a 'getColor' function.") local frameworkStyles = FrameworkStyles.new() diff --git a/BuiltInPlugins/LocalizationTools/Packages/Framework/StudioUI/StudioFrameworkStyles.spec.lua b/BuiltInPlugins/LocalizationTools/Packages/Framework/StudioUI/StudioFrameworkStyles.spec.lua index 02a8255021..19347b0066 100644 --- a/BuiltInPlugins/LocalizationTools/Packages/Framework/StudioUI/StudioFrameworkStyles.spec.lua +++ b/BuiltInPlugins/LocalizationTools/Packages/Framework/StudioUI/StudioFrameworkStyles.spec.lua @@ -1,6 +1,16 @@ return function() local StudioFrameworkStyles = require(script.Parent.StudioFrameworkStyles) + local Framework = script.Parent.Parent + local Util = require(Framework.Util) + local FlagsList = Util.Flags.new({ + FFlagRefactorDevFrameworkTheme = {"RefactorDevFrameworkTheme"}, + }) + + if FlagsList:get("FFlagRefactorDevFrameworkTheme") then + return + end + describe("new", function() it("should expect a studio theme", function() expect(function() @@ -30,8 +40,8 @@ return function() for _, entry in pairs(styles) do expect(entry.Default).to.be.ok() - expect(next(entry.Default)).to.be.ok() + expect((next(entry.Default))).to.be.ok() end end) end) -end \ No newline at end of file +end diff --git a/BuiltInPlugins/LocalizationTools/Packages/Framework/StudioUI/StudioFrameworkStyles/Common.lua b/BuiltInPlugins/LocalizationTools/Packages/Framework/StudioUI/StudioFrameworkStyles/Common.lua index e595b72b01..d45a3c4d44 100644 --- a/BuiltInPlugins/LocalizationTools/Packages/Framework/StudioUI/StudioFrameworkStyles/Common.lua +++ b/BuiltInPlugins/LocalizationTools/Packages/Framework/StudioUI/StudioFrameworkStyles/Common.lua @@ -8,51 +8,68 @@ local Util = require(Framework.Util) local Style = Util.Style local StyleValue = Util.StyleValue -return function(theme, getColor) - local MainText = Style.new({ - Font = Enum.Font.SourceSans, - TextSize = 18, - TextColor = theme:GetColor("MainText"), - }) - - local Background = Style.new({ - Color = theme:GetColor("MainBackground"), - }) - - local Border = Style.new({ - BorderColor = theme:GetColor("Border"), - }) - - local BorderHover = Style.new({ - BorderColor = theme:GetColor("DialogMainButton") - }) - - local Scroller = Style.new({ - BackgroundTransparency = 1, - BorderSizePixel = 0, - BackgroundColor3 = theme:GetColor("MainBackground"), - - TopImage = "rbxasset://textures/StudioToolbox/ScrollBarTop.png", - MidImage = "rbxasset://textures/StudioToolbox/ScrollBarMiddle.png", - BottomImage = "rbxasset://textures/StudioToolbox/ScrollBarBottom.png", - - ScrollingEnabled = true, - ScrollingDirection = Enum.ScrollingDirection.Y, - ScrollBarThickness = 8, - ScrollBarImageTransparency = 0.5, - ScrollBarImageColor3 = StyleValue.new("ScrollbarColor", { - Light = Color3.fromRGB(25, 25, 25), - Dark = Color3.fromRGB(204, 204, 204), - }):get(theme.name), - VerticalScrollBarInset = Enum.ScrollBarInset.Always - }) +local StyleKey = require(Framework.Style.StyleKey) +local FlagsList = Util.Flags.new({ + FFlagRefactorDevFrameworkTheme = {"RefactorDevFrameworkTheme"}, +}) + +if FlagsList:get("FFlagRefactorDevFrameworkTheme") then + -- TODO: DEVTOOLS-4908 Refactor everything using MainText so that we can remove Common.lua completely return { - Default = Background, - MainText = MainText, - Background = Background, - Border = Border, - BorderHover = BorderHover, - Scroller = Scroller, + MainText = { + Font = Enum.Font.SourceSans, + TextSize = 18, + TextColor = StyleKey.MainText, + }, } -end +else + return function(theme, getColor) + local MainText = Style.new({ + Font = Enum.Font.SourceSans, + TextSize = 18, + TextColor = theme:GetColor("MainText"), + }) + + local Background = Style.new({ + Color = theme:GetColor("MainBackground"), + }) + + local Border = Style.new({ + BorderColor = theme:GetColor("Border"), + }) + + local BorderHover = Style.new({ + BorderColor = theme:GetColor("DialogMainButton"), + }) + + local Scroller = Style.new({ + BackgroundTransparency = 1, + BorderSizePixel = 0, + BackgroundColor3 = theme:GetColor("MainBackground"), + + TopImage = "rbxasset://textures/StudioToolbox/ScrollBarTop.png", + MidImage = "rbxasset://textures/StudioToolbox/ScrollBarMiddle.png", + BottomImage = "rbxasset://textures/StudioToolbox/ScrollBarBottom.png", + + ScrollingEnabled = true, + ScrollingDirection = Enum.ScrollingDirection.Y, + ScrollBarThickness = 8, + ScrollBarImageTransparency = 0.5, + ScrollBarImageColor3 = StyleValue.new("ScrollbarColor", { + Light = Color3.fromRGB(25, 25, 25), + Dark = Color3.fromRGB(204, 204, 204), + }):get(theme.name), + VerticalScrollBarInset = Enum.ScrollBarInset.Always + }) + + return { + Default = Background, + MainText = MainText, + Background = Background, + Border = Border, + BorderHover = BorderHover, + Scroller = Scroller, + } + end +end \ No newline at end of file diff --git a/BuiltInPlugins/LocalizationTools/Packages/Framework/StudioUI/StyledDialog.lua b/BuiltInPlugins/LocalizationTools/Packages/Framework/StudioUI/StyledDialog.lua index 66e1f41e39..407a5e5d7b 100644 --- a/BuiltInPlugins/LocalizationTools/Packages/Framework/StudioUI/StyledDialog.lua +++ b/BuiltInPlugins/LocalizationTools/Packages/Framework/StudioUI/StyledDialog.lua @@ -8,10 +8,9 @@ callback OnClose: A function which is fired when the X button attached to the widget. callback OnButtonPressed: A function which is called when any of the buttons - are pressed. - Theme Theme: A Theme ContextItem, which is provided via mapToProps. + are pressed. string Title: The title text displayed at the top of the widget. - + Optional Props: boolean Enabled: Whether the widget is currently visible. Vector2 MinSize: The minimum size of the widget, in pixels. @@ -20,16 +19,19 @@ boolean Resizable: Whether the widget can be resized. Style Style: a predefined kind of dialog to use. Enum.ZIndexBehavior ZIndexBehavior: The ZIndexBehavior of the widget. + Stylizer Stylizer: A Stylizer ContextItem, which is provided via mapToProps. + Theme Theme: A Theme ContextItem, which is provided via mapToProps. - Style Values: Color3 BackgroundColor3: Background color of the dialog. ]] - local Framework = script.Parent.Parent local ContextServices = require(Framework.ContextServices) local Roact = require(Framework.Parent.Roact) local Util = require(Framework.Util) +local FlagsList = Util.Flags.new({ + FFlagRefactorDevFrameworkTheme = {"RefactorDevFrameworkTheme"}, +}) local Button = require(Framework.UI.Button) local Container = require(Framework.UI.Container) @@ -43,7 +45,6 @@ local BUTTON_PADDING = 24 local BUTTON_EDGE_PADDING = 70 local CONTENT_PADDING = 24 - local StyledDialog = Roact.PureComponent:extend("StyledDialog") StyledDialog.defaultProps = { @@ -105,7 +106,12 @@ end function StyledDialog:render() local theme = self.props.Theme - local style = theme:getStyle("Framework", self) + local style + if FlagsList:get("FFlagRefactorDevFrameworkTheme") then + style = self.props.Stylizer + else + style = theme:getStyle("Framework", self) + end local backgroundColor = prioritize(self.props.BackgroundColor3, style.Background) local isEnabled = self.props.Enabled @@ -141,8 +147,10 @@ function StyledDialog:render() }) end ContextServices.mapToProps(StyledDialog, { - Theme = ContextServices.Theme, + Stylizer = FlagsList:get("FFlagRefactorDevFrameworkTheme") and ContextServices.Stylizer or nil, + Theme = (not FlagsList:get("FFlagRefactorDevFrameworkTheme")) and ContextServices.Theme or nil, }) + Typecheck.wrap(StyledDialog, script) return StyledDialog diff --git a/BuiltInPlugins/LocalizationTools/Packages/Framework/StudioUI/StyledDialog/example.lua b/BuiltInPlugins/LocalizationTools/Packages/Framework/StudioUI/StyledDialog/example.lua index 0a455e4157..9d869c3757 100644 --- a/BuiltInPlugins/LocalizationTools/Packages/Framework/StudioUI/StyledDialog/example.lua +++ b/BuiltInPlugins/LocalizationTools/Packages/Framework/StudioUI/StyledDialog/example.lua @@ -2,8 +2,6 @@ return function(plugin) local Framework = script.Parent.Parent.Parent local Roact = require(Framework.Parent.Roact) local ContextServices = require(Framework.ContextServices) - local Plugin = ContextServices.Plugin - local Theme = ContextServices.Theme local StudioUI = require(Framework.StudioUI) local Dialog = StudioUI.Dialog @@ -16,13 +14,26 @@ return function(plugin) local TestHelpers = require(Framework.TestHelpers) + local Util = require(Framework.Util) + local FlagsList = Util.Flags.new({ + FFlagRefactorDevFrameworkTheme = {"RefactorDevFrameworkTheme"}, + }) + + local FrameworkStyle = Framework.Style + local StudioTheme = require(FrameworkStyle.Themes.StudioTheme) + local pluginItem = ContextServices.Plugin.new(plugin) - local theme = ContextServices.Theme.new(function(theme, getColor) - return { - Framework = StudioFrameworkStyles.new(theme, getColor), - } - end) - + local theme + if FlagsList:get("FFlagRefactorDevFrameworkTheme") then + theme = StudioTheme.new() + else + theme = ContextServices.Theme.new(function(theme, getColor) + return { + Framework = StudioFrameworkStyles.new(theme, getColor), + } + end) + end + local function renderInPopup(isEnabled, onCloseFunc, children) -- This example was too many layers deep. -- This logic might be reusable across multiple examples @@ -83,7 +94,7 @@ return function(plugin) DefaultButton = Roact.createElement(Button, { Size = UDim2.new(1, 0, 0, 30), - LayoutIndex = 1, + LayoutOrder = 1, Style = "Round", Text = "Open Default Dialog", OnClick = function() @@ -116,10 +127,9 @@ return function(plugin) }), }), - AlertButton = Roact.createElement(Button, { Size = UDim2.new(1, 0, 0, 30), - LayoutIndex = 2, + LayoutOrder = 2, Style = "Round", Text = "Open Alert Dialog", OnClick = function() @@ -152,11 +162,10 @@ return function(plugin) }), }), }), - AcceptCancelButton = Roact.createElement(Button, { Size = UDim2.new(1, 0, 0, 30), - LayoutIndex = 3, + LayoutOrder = 3, Style = "Round", Text = "Open AcceptCancel Dialog", OnClick = function() diff --git a/BuiltInPlugins/LocalizationTools/Packages/Framework/StudioUI/StyledDialog/renderExample.lua b/BuiltInPlugins/LocalizationTools/Packages/Framework/StudioUI/StyledDialog/renderExample.lua index 56827aefc3..566cceb998 100644 --- a/BuiltInPlugins/LocalizationTools/Packages/Framework/StudioUI/StyledDialog/renderExample.lua +++ b/BuiltInPlugins/LocalizationTools/Packages/Framework/StudioUI/StyledDialog/renderExample.lua @@ -5,13 +5,24 @@ local UI = require(Framework.UI) local StudioUI = require(Framework.StudioUI) local Button = UI.Button local StyledDialog = StudioUI.StyledDialog +local StudioTheme = require(Framework.Style.Themes.StudioTheme) + +local Util = require(Framework.Util) +local FlagsList = Util.Flags.new({ + FFlagRefactorDevFrameworkTheme = {"RefactorDevFrameworkTheme"}, +}) local Example = Roact.PureComponent:extend("StyledDialogExample") function Example:render() -- push the same context items into the example local plugin = self.props.Plugin - local theme = self.props.Theme + local theme + if FlagsList:get("FFlagRefactorDevFrameworkTheme") then + theme = StudioTheme.new() + else + theme = self.props.Theme + end return Roact.createElement(Button, { Style = "Round", @@ -49,7 +60,8 @@ function Example:render() end ContextServices.mapToProps(Example, { Plugin = ContextServices.Plugin, - Theme = ContextServices.Theme, + Stylizer = FlagsList:get("FFlagRefactorDevFrameworkTheme") and ContextServices.Stylizer or nil, + Theme = (not FlagsList:get("FFlagRefactorDevFrameworkTheme")) and ContextServices.Theme or nil, }) return Example \ No newline at end of file diff --git a/BuiltInPlugins/LocalizationTools/Packages/Framework/StudioUI/StyledDialog/style.lua b/BuiltInPlugins/LocalizationTools/Packages/Framework/StudioUI/StyledDialog/style.lua index 1b92cbbfff..e2f1759519 100644 --- a/BuiltInPlugins/LocalizationTools/Packages/Framework/StudioUI/StyledDialog/style.lua +++ b/BuiltInPlugins/LocalizationTools/Packages/Framework/StudioUI/StyledDialog/style.lua @@ -1,36 +1,60 @@ local Framework = script.Parent.Parent.Parent +local StyleKey = require(Framework.Style.StyleKey) + local Util = require(Framework.Util) local Style = Util.Style +local FlagsList = Util.Flags.new({ + FFlagRefactorDevFrameworkTheme = {"RefactorDevFrameworkTheme"}, +}) -local UI = require(Framework.UI) -local Decoration = UI.Decoration - -return function(theme, getColor) - - local Default = Style.new({ - Background = theme:GetColor("MainBackground"), +if FlagsList:get("FFlagRefactorDevFrameworkTheme") then + return { + Background = StyleKey.MainBackground, Modal = false, Resizable = false, - }) - local Alert = Style.extend(Default, { - Buttons = { - { Style = "RoundPrimary" }, -- OK + ["&Alert"] = { + Buttons = { + { Style = "RoundPrimary" }, -- OK + }, + Modal = true, }, - Modal = true, - }) - local AcceptCancel = Style.extend(Default, { - Buttons = { - { Style = "RoundPrimary" }, -- OK - { Style = "Round" }, -- Cancel + ["&AcceptCancel"] = { + Buttons = { + { Style = "RoundPrimary" }, -- OK + { Style = "Round" }, -- Cancel + }, }, - }) - - return { - Default = Default, - Alert = Alert, - AcceptCancel = AcceptCancel, } -end +else + return function(theme, getColor) + + local Default = Style.new({ + Background = theme:GetColor("MainBackground"), + Modal = false, + Resizable = false, + }) + + local Alert = Style.extend(Default, { + Buttons = { + { Style = "RoundPrimary" }, -- OK + }, + Modal = true, + }) + + local AcceptCancel = Style.extend(Default, { + Buttons = { + { Style = "RoundPrimary" }, -- OK + { Style = "Round" }, -- Cancel + }, + }) + + return { + Default = Default, + Alert = Alert, + AcceptCancel = AcceptCancel, + } + end +end \ No newline at end of file diff --git a/BuiltInPlugins/LocalizationTools/Packages/Framework/StudioUI/StyledDialog/test.spec.lua b/BuiltInPlugins/LocalizationTools/Packages/Framework/StudioUI/StyledDialog/test.spec.lua index b0775e5956..2f892553ea 100644 --- a/BuiltInPlugins/LocalizationTools/Packages/Framework/StudioUI/StyledDialog/test.spec.lua +++ b/BuiltInPlugins/LocalizationTools/Packages/Framework/StudioUI/StyledDialog/test.spec.lua @@ -2,7 +2,6 @@ return function() local Framework = script.Parent.Parent.Parent local Roact = require(Framework.Parent.Roact) local StyledDialog = require(script.Parent) - local ContextServices = require(Framework.ContextServices) local TestHelpers = require(Framework.TestHelpers) it("should create and destroy without errors", function() @@ -60,4 +59,4 @@ return function() Roact.unmount(instance) end) end) -end \ No newline at end of file +end diff --git a/BuiltInPlugins/LocalizationTools/Packages/Framework/StudioUI/TitledFrame.lua b/BuiltInPlugins/LocalizationTools/Packages/Framework/StudioUI/TitledFrame.lua index 4f3f1d6e41..00d35130fd 100644 --- a/BuiltInPlugins/LocalizationTools/Packages/Framework/StudioUI/TitledFrame.lua +++ b/BuiltInPlugins/LocalizationTools/Packages/Framework/StudioUI/TitledFrame.lua @@ -4,12 +4,13 @@ Required Props: string Title: The title to the left of the content - Theme Theme: A Theme ContextItem, which is provided via mapToProps. Optional Props: number TitleWidth: The pixel pize of the padding between the title and content Enum.FillDirection FillDirection: The direction in which the content is filled. number LayoutOrder: The layoutOrder of this component. + Stylizer Stylizer: A Stylizer ContextItem, which is provided via mapToProps. + Theme Theme: A Theme ContextItem, which is provided via mapToProps. number ZIndex: The render index of this component. ]] local Framework = script.Parent.Parent @@ -18,6 +19,9 @@ local ContextServices = require(Framework.ContextServices) local Util = require(Framework.Util) local Typecheck = Util.Typecheck +local FlagsList = Util.Flags.new({ + FFlagRefactorDevFrameworkTheme = {"RefactorDevFrameworkTheme"}, +}) local FitFrame = require(Framework.Util.FitFrame) local FitFrameVertical = FitFrame.FitFrameVertical @@ -34,7 +38,12 @@ TitledFrame.defaultProps = { function TitledFrame:render() local props = self.props local theme = props.Theme - local style = theme:getStyle("Framework", self) + local style + if FlagsList:get("FFlagRefactorDevFrameworkTheme") then + style = props.Stylizer + else + style = theme:getStyle("Framework", self) + end local font = style.Font local padding = style.Padding @@ -82,7 +91,8 @@ function TitledFrame:render() end ContextServices.mapToProps(TitledFrame, { - Theme = ContextServices.Theme, + Stylizer = FlagsList:get("FFlagRefactorDevFrameworkTheme") and ContextServices.Stylizer or nil, + Theme = (not FlagsList:get("FFlagRefactorDevFrameworkTheme")) and ContextServices.Theme or nil, }) return TitledFrame diff --git a/BuiltInPlugins/LocalizationTools/Packages/Framework/StudioUI/TitledFrame/renderExample.lua b/BuiltInPlugins/LocalizationTools/Packages/Framework/StudioUI/TitledFrame/renderExample.lua index fa7eff2313..88acd338aa 100644 --- a/BuiltInPlugins/LocalizationTools/Packages/Framework/StudioUI/TitledFrame/renderExample.lua +++ b/BuiltInPlugins/LocalizationTools/Packages/Framework/StudioUI/TitledFrame/renderExample.lua @@ -4,6 +4,11 @@ local Roact = require(Framework.Parent.Roact) local StudioUI = require(Framework.StudioUI) local TitledFrame = StudioUI.TitledFrame +local Util = require(Framework.Util) +local FlagsList = Util.Flags.new({ + FFlagRefactorDevFrameworkTheme = {"RefactorDevFrameworkTheme"}, +}) + local Example = Roact.PureComponent:extend("StyledDialogExample") function Example:render() @@ -22,7 +27,8 @@ end ContextServices.mapToProps(Example, { Plugin = ContextServices.Plugin, - Theme = ContextServices.Theme, + Stylizer = FlagsList:get("FFlagRefactorDevFrameworkTheme") and ContextServices.Stylizer or nil, + Theme = (not FlagsList:get("FFlagRefactorDevFrameworkTheme")) and ContextServices.Theme or nil, }) return Example \ No newline at end of file diff --git a/BuiltInPlugins/LocalizationTools/Packages/Framework/StudioUI/TitledFrame/style.lua b/BuiltInPlugins/LocalizationTools/Packages/Framework/StudioUI/TitledFrame/style.lua index ac6b0a646b..bfaa3c423f 100644 --- a/BuiltInPlugins/LocalizationTools/Packages/Framework/StudioUI/TitledFrame/style.lua +++ b/BuiltInPlugins/LocalizationTools/Packages/Framework/StudioUI/TitledFrame/style.lua @@ -3,19 +3,32 @@ local Framework = script.Parent.Parent.Parent local StudioFrameworkStyles = Framework.StudioUI.StudioFrameworkStyles local Common = require(StudioFrameworkStyles.Common) +local StyleKey = require(Framework.Style.StyleKey) + local Util = require(Framework.Util) local Style = Util.Style +local FlagsList = Util.Flags.new({ + FFlagRefactorDevFrameworkTheme = {"RefactorDevFrameworkTheme"}, +}) -return function(theme, getColor) - local common = Common(theme, getColor) - - local Default = Style.extend(common.MainText, { - Padding = 10, - TextSize = 22, - TextColor = theme:GetColor("TitlebarText"), - }) - +if FlagsList:get("FFlagRefactorDevFrameworkTheme") then return { - Default = Default, + Padding = 10, + TextSize = 24, + TextColor = StyleKey.TitlebarText, } -end +else + return function(theme, getColor) + local common = Common(theme, getColor) + + local Default = Style.extend(common.MainText, { + Padding = 10, + TextSize = 22, + TextColor = theme:GetColor("TitlebarText"), + }) + + return { + Default = Default, + } + end +end \ No newline at end of file diff --git a/BuiltInPlugins/LocalizationTools/Packages/Framework/StudioUI/TitledFrame/test.spec.lua b/BuiltInPlugins/LocalizationTools/Packages/Framework/StudioUI/TitledFrame/test.spec.lua index b769b54068..f013a391bc 100644 --- a/BuiltInPlugins/LocalizationTools/Packages/Framework/StudioUI/TitledFrame/test.spec.lua +++ b/BuiltInPlugins/LocalizationTools/Packages/Framework/StudioUI/TitledFrame/test.spec.lua @@ -7,12 +7,24 @@ return function() local TitledFrame = require(script.Parent) local Theme = ContextServices.Theme + local StudioTheme = require(Framework.Style.Themes.StudioTheme) + + local Util = require(Framework.Util) + local FlagsList = Util.Flags.new({ + FFlagRefactorDevFrameworkTheme = {"RefactorDevFrameworkTheme"}, + }) + local function createTestTitledFrame(children, container) - local theme = Theme.new(function() - return { - Framework = FrameworkStyles.new(), - } - end) + local theme + if FlagsList:get("FFlagRefactorDevFrameworkTheme") then + theme = StudioTheme.mock() + else + theme = Theme.new(function() + return { + Framework = FrameworkStyles.new(), + } + end) + end return provide({theme}, { TitledFrame = Roact.createElement(TitledFrame, { Title = "Test", diff --git a/BuiltInPlugins/LocalizationTools/Packages/Framework/StudioUI/createPluginWidget.lua b/BuiltInPlugins/LocalizationTools/Packages/Framework/StudioUI/createPluginWidget.lua index c022693c9a..b9dd6d3759 100644 --- a/BuiltInPlugins/LocalizationTools/Packages/Framework/StudioUI/createPluginWidget.lua +++ b/BuiltInPlugins/LocalizationTools/Packages/Framework/StudioUI/createPluginWidget.lua @@ -9,8 +9,12 @@ ]] game:DefineFastFlag("FixDevFrameworkDockWidgetRestore", false) +game:DefineFastFlag("DevFrameworkPluginWidgetEnabledEvent2", false) +game:DefineFastFlag("DevFrameworkPluginWidgetUseSiblingZIndex", false) local FFlagFixDevFrameworkDockWidgetRestore = game:GetFastFlag("FixDevFrameworkDockWidgetRestore") +local FFlagDevFrameworkPluginWidgetEnabledEvent2 = game:GetFastFlag("DevFrameworkPluginWidgetEnabledEvent2") +local FFlagDevFrameworkPluginWidgetUseSiblingZIndex = game:GetFastFlag("DevFrameworkPluginWidgetUseSiblingZIndex") local Framework = script.Parent.Parent local Roact = require(Framework.Parent.Roact) @@ -29,7 +33,11 @@ local function createPluginWidget(componentName, createWidgetFunc) local widget = createWidgetFunc(props) widget.Name = title or "" - widget.ZIndexBehavior = props.ZIndexBehavior or Enum.ZIndexBehavior.Global + if FFlagDevFrameworkPluginWidgetUseSiblingZIndex then + widget.ZIndexBehavior = props.ZIndexBehavior or Enum.ZIndexBehavior.Sibling + else + widget.ZIndexBehavior = props.ZIndexBehavior or Enum.ZIndexBehavior.Global + end if widget:IsA("PluginGui") then widget:BindToClose(onClose) @@ -65,6 +73,17 @@ local function createPluginWidget(componentName, createWidgetFunc) end end + if FFlagDevFrameworkPluginWidgetEnabledEvent2 then + -- Connect to enabled changing *after* restore + -- Otherwise users of this will get 2 enabled changes: one from the onRestore, and the same from Roact.Change.Enabled + self.widgetEnabledChangedConnection = widget:GetPropertyChangedSignal("Enabled"):Connect(function() + local callback = self.props[Roact.Change.Enabled] + if callback and self.widget and self.widget.Enabled ~= self.props.Enabled then + callback(self.widget) + end + end) + end + self.focus = Focus.new(widget) self.widget = widget end @@ -105,6 +124,11 @@ local function createPluginWidget(componentName, createWidgetFunc) end function PluginWidget:willUnmount() + if self.widgetEnabledChangedConnection then + self.widgetEnabledChangedConnection:Disconnect() + self.widgetEnabledChangedConnection = nil + end + if self.windowFocusReleasedConnection then self.windowFocusReleasedConnection:Disconnect() self.windowFocusReleasedConnection = nil @@ -117,6 +141,7 @@ local function createPluginWidget(componentName, createWidgetFunc) if self.widget then self.widget:Destroy() + self.widget = nil end end diff --git a/BuiltInPlugins/LocalizationTools/Packages/Framework/Style.lua b/BuiltInPlugins/LocalizationTools/Packages/Framework/Style.lua new file mode 100644 index 0000000000..53914d2cc3 --- /dev/null +++ b/BuiltInPlugins/LocalizationTools/Packages/Framework/Style.lua @@ -0,0 +1,17 @@ +local strict = require(script.Parent.Util.strict) + +return strict({ + Colors = require(script.Colors), + ComponentSymbols = require(script.ComponentSymbols), + createDefaultTheme = require(script.createDefaultTheme), + getRawComponentStyle = require(script.getRawComponentStyle), + StyleKey = require(script.StyleKey), + Stylizer = require(script.Stylizer), + + Themes = strict({ + BaseTheme = require(script.Themes.BaseTheme), + DarkTheme = require(script.Themes.DarkTheme), + LightTheme = require(script.Themes.LightTheme), + StudioTheme = require(script.Themes.StudioTheme), + }) +}) \ No newline at end of file diff --git a/BuiltInPlugins/LocalizationTools/Packages/Framework/Style/Colors.lua b/BuiltInPlugins/LocalizationTools/Packages/Framework/Style/Colors.lua new file mode 100644 index 0000000000..6bc14c4ba2 --- /dev/null +++ b/BuiltInPlugins/LocalizationTools/Packages/Framework/Style/Colors.lua @@ -0,0 +1,22 @@ +return { + Gray_Light = Color3.fromRGB(204, 204, 204), + Gray = Color3.fromRGB(60, 60, 60), + Slate = Color3.fromRGB(46, 46, 46), + Carbon = Color3.fromRGB(34, 34, 34), + Blue = Color3.fromRGB(0, 162, 255), + Blue_Dark = Color3.fromRGB(0, 117, 189), + Blue_Light = Color3.fromRGB(53, 181, 255), + + Red = Color3.fromRGB(255, 0, 0), + White = Color3.fromRGB(255, 255, 255), + Black = Color3.fromRGB(0, 0, 0), + + -- TODO: DEVTOOLS-4869 - If we add lighter/darker functions to Color3, then refactor this to use that. + lighter = function(color3, alpha) + return color3:lerp(Color3.new(1, 1, 1), alpha) + end, + + darker = function(color3, alpha) + return color3:lerp(Color3.new(0, 0, 0), alpha) + end, +} \ No newline at end of file diff --git a/BuiltInPlugins/LocalizationTools/Packages/Framework/Style/ComponentSymbols.lua b/BuiltInPlugins/LocalizationTools/Packages/Framework/Style/ComponentSymbols.lua new file mode 100644 index 0000000000..aae77a0281 --- /dev/null +++ b/BuiltInPlugins/LocalizationTools/Packages/Framework/Style/ComponentSymbols.lua @@ -0,0 +1,26 @@ +--[[ + Returns a table of unique values keyed on name for each component. + This file also creates Symbols for each DevFramework component. + + add(key) + Adds a value into the ComponentSymbols table for use in Stylizer. +]] + +local Framework = script.Parent.Parent +local tableCache = require(Framework.Util.tableCache) + +local UIFolderData = require(Framework.UI.UIFolderData) +local StudioUIFolderData = require(Framework.StudioUI.StudioUIFolderData) + +local ComponentSymbols = tableCache("ComponentSymbols") + +local function createSymbolsForFolder(folder) + for _, component in pairs(folder) do + ComponentSymbols:add(component.name, require) + end +end + +createSymbolsForFolder(UIFolderData) +createSymbolsForFolder(StudioUIFolderData) + +return ComponentSymbols \ No newline at end of file diff --git a/BuiltInPlugins/LocalizationTools/Packages/Framework/Style/ComponentSymbols.spec.lua b/BuiltInPlugins/LocalizationTools/Packages/Framework/Style/ComponentSymbols.spec.lua new file mode 100644 index 0000000000..dd14b8bdd9 --- /dev/null +++ b/BuiltInPlugins/LocalizationTools/Packages/Framework/Style/ComponentSymbols.spec.lua @@ -0,0 +1,43 @@ +return function() + local ComponentSymbols = require(script.Parent.ComponentSymbols) + + local function cleanUpSymbols(symbolName) + for k,_ in pairs(ComponentSymbols) do + if typeof(k) == "table" then + ComponentSymbols[k] = nil + end + end + end + + describe("add", function() + afterEach(function() + cleanUpSymbols() + end) + + it("should coerce to the given name", function() + local symbol = ComponentSymbols:add("foo") + expect((tostring(symbol):find("foo"))).to.be.ok() + end) + + it("should not have duplicate entries", function() + local testA = ComponentSymbols:add("abc") + local testB = ComponentSymbols:add("abc") + expect(testA).to.equal(testB) + end) + + it("should get the same entry for the same lookup", function() + ComponentSymbols:add("abc") + ComponentSymbols:add("abc") + local testA = ComponentSymbols["abc"] + local testB = ComponentSymbols["abc"] + expect(testA).to.equal(testB) + end) + + it("should have ComponentSymbols as a metavalue", function() + ComponentSymbols:add("abc") + local testA = ComponentSymbols["abc"] + local mt = getmetatable(testA) + expect(mt).to.equal(ComponentSymbols) + end) + end) +end diff --git a/BuiltInPlugins/LocalizationTools/Packages/Framework/Style/StyleKey.lua b/BuiltInPlugins/LocalizationTools/Packages/Framework/Style/StyleKey.lua new file mode 100644 index 0000000000..3c6f027661 --- /dev/null +++ b/BuiltInPlugins/LocalizationTools/Packages/Framework/Style/StyleKey.lua @@ -0,0 +1,19 @@ +--[[ + Returns a table of unique values which are given a StyleKey metatable to differentiate it as a StyleKey value. + The Stylizer refers to StyleKey and replaces them with the correct color value. +]] + +local Framework = script.Parent.Parent +local tableCache = require(Framework.Util.tableCache) + +local StyleKey = tableCache("StyleKey") + +setmetatable(StyleKey, { + __index = function(t, name) + local newStyleKey = StyleKey:add(name) + t[name] = newStyleKey + return newStyleKey + end +}) + +return StyleKey \ No newline at end of file diff --git a/BuiltInPlugins/LocalizationTools/Packages/Framework/Style/StyleKey.spec.lua b/BuiltInPlugins/LocalizationTools/Packages/Framework/Style/StyleKey.spec.lua new file mode 100644 index 0000000000..442c10e8ef --- /dev/null +++ b/BuiltInPlugins/LocalizationTools/Packages/Framework/Style/StyleKey.spec.lua @@ -0,0 +1,34 @@ +return function() + local StyleKey = require(script.Parent.StyleKey) + + describe("add", function() + it("should coerce to the given name", function() + local symbol = StyleKey:add("foo") + + expect((tostring(symbol):find("foo"))).to.be.ok() + end) + + it("should create a new entry when there is no current entry found", function() + local testA = StyleKey["abc"] + expect(testA).never.to.equal(nil) + end) + + it("should not have duplicate entries", function() + local testA = StyleKey:add("abc") + local testB = StyleKey:add("abc") + expect(testA).to.equal(testB) + end) + + it("should get the same entry for the same lookup", function() + local testA = StyleKey["abc"] + local testB = StyleKey["abc"] + expect(testA).to.equal(testB) + end) + + it("should have StyleKey as a metavalue", function() + local testA = StyleKey["abc"] + local mt = getmetatable(testA) + expect(mt).to.equal(StyleKey) + end) + end) +end diff --git a/BuiltInPlugins/LocalizationTools/Packages/Framework/Style/Stylizer.lua b/BuiltInPlugins/LocalizationTools/Packages/Framework/Style/Stylizer.lua new file mode 100644 index 0000000000..228c4b165a --- /dev/null +++ b/BuiltInPlugins/LocalizationTools/Packages/Framework/Style/Stylizer.lua @@ -0,0 +1,323 @@ +--!nocheck + +--[[ + Wraps theme styles and update logic into a ContextItem. + + Stylizer.new(t, themeProps) + Constructs a new Stylizer. + + Params: + table initialStyles: + The initial style table. + table themeProps: + A table of properties needed to change the theme. It should contain the following properties: + function getThemeName: Required. Returns the current theme name. + table themeChangedConnection: Optional. Signals when the theme changes. It should + contain a function :Connect(). Requires themeProps to contain `themesList`. + table themesList: Optional. Table of themes to use keyed on theme name. Is required + when themeChangedConnection is set. + + Stylizer:getStyleKeysTable(t) + Gets all StyleKey values in the first layer of the table t. + + Stylizer:convertStyleKeys(t, name, parent, styleKeysTable) + Converts t's (which is a Stylizer table)'s StyleKey values into corresponding values in the styleKeysTable parameter + and calculates each component/style's path string. + + Stylizer:getPathString() + Gets the path of the Stylizer in relation to the overall theme table. + + Example usage: + -- MakeTheme.lua + local Stylizer = ContextHelpers.Stylizer + local BaseTheme = Theme.BaseTheme + + local styleRoot = Stylizer.new(BaseTheme, themeProps) + styleRoot:extend({ + TextBox = { + Default = { + TextColor = Color3.new(0,0,0), + Font = Enum.Font.SourceSans, + }, + }, + }) + return styleRoot + + -- TextBox.lua + local TextBox = Roact.PureComponent:extend("TextBox") + + function TextBox:render() + local props = self.props + local style = props.Stylizer + + return Roact.createElement("TextBox", { + TextColor3 = style.TextColor, + Font = style.Font, + }) + end + + ContextServices.mapToProps(TextBox, { + Stylizer = ContextServices.Stylizer, + }) + + return TextBox +]] + +local Framework = script.Parent.Parent +local Roact = require(Framework.Parent.Roact) +local Cryo = require(Framework.Util.Cryo) + +local ComponentSymbols = require(Framework.Style.ComponentSymbols) +local StyleKey = require(Framework.Style.StyleKey) + +local Provider = require(Framework.ContextServices.Provider) +local ContextItem = require(Framework.ContextServices.ContextItem) + +local Util = require(Framework.Util) +local deepCopy = Util.deepCopy +local Signal = Util.Signal + +local Stylizer = ContextItem:extend("Stylizer") + +local function assertIfNotNil(value, condition, message) + if value ~= nil then + assert(condition, message) + end +end + +function Stylizer:getStyleKeysTable(t) + local styleKeysTable = {} + for k,v in pairs(t) do + if type(k) == "table" and getmetatable(k) == StyleKey and type(v) ~= "table" then + -- NOTE: StyleKeys need to be stringified + styleKeysTable[tostring(k)] = v + end + end + return styleKeysTable +end + +--[[ + Gets the classStyle (in form of ["&ClassName"]) within a component. +--]] +function Stylizer:__getClassStyle(className, currentStyle, componentSymbol) + local result = currentStyle[className] + + if not result then + -- Get class style that is embedded (as an ampersandedClassName) in the component's style table + local ampersandedClassName = "&"..tostring(className) + local componentStyle = currentStyle and currentStyle[componentSymbol] + result = componentStyle and componentStyle[ampersandedClassName] + if result and type(result) == "table" then + local componentStyleWithoutEmbeddedClass = Cryo.Dictionary.join(componentStyle, { + [ampersandedClassName] = Cryo.None, + }) + result = Cryo.Dictionary.join(componentStyleWithoutEmbeddedClass, result) + local mt = getmetatable(componentStyle[ampersandedClassName]) + result = setmetatable(result, mt) + end + end + + assert(result, + ("Stylizer:__getClassStyle copuld not find a Style named '%s' for component `%s`") + :format(className, tostring(componentSymbol)) + ) + + return result +end + +function Stylizer:__recalculateTheme(themeProps) + assert(type(themeProps.themesList) == "table", + "Stylizer.__recalculateTheme expects themeProps to contain a table `themesList` when themeChangedConnection is enabled") + + local themeName = themeProps.getThemeName() + if themeName == self.themeName then + return + end + + self.themeName = themeName + + if themeProps and themeProps.themesList then + self:extend(themeProps.themesList[themeName]) + self.valuesChanged:Fire(self) + end +end + +function Stylizer.new(initialStyles, themeProps) + assert(type(initialStyles) == "table", "Stylizer.new expects initialStyles parameter to be a table") + assert(type(themeProps) == "table", "Stylizer.new expects themeProps parameter to be a table") + assert(type(themeProps.getThemeName) == "function", + "Stylizer.new expects themeProps to contain a function `getThemeName`") + + local styleKeysTable = Stylizer:getStyleKeysTable(initialStyles) + local selfCopy = deepCopy(initialStyles) + selfCopy = Stylizer:convertStyleKeys(selfCopy, nil, nil, styleKeysTable) + + local self = { + __calculatedStyle = selfCopy, + __rawStyle = initialStyles, + valuesChanged = Signal.new(), + themeName = themeProps.getThemeName(), + themeChangedConnection = nil, + } + setmetatable(self, Stylizer) + + if themeProps.themeChangedConnection then + self.themeChangedConnection = themeProps.themeChangedConnection:Connect(function() + self:__recalculateTheme(themeProps) + end) + end + + return self +end + +function Stylizer:extend(...) + for _, v in ipairs({...}) do + local vCopy = deepCopy(v) + local joinedStyles = Cryo.Dictionary.join(self.__rawStyle, vCopy) + local styleKeysTable = self:getStyleKeysTable(joinedStyles) + + self.__rawStyle = deepCopy(joinedStyles) + self.__calculatedStyle = Stylizer:convertStyleKeys(joinedStyles, nil, nil, styleKeysTable) + end + return self +end + +function Stylizer:createProvider(root) + return Roact.createElement(Provider, { + ContextItem = self, + UpdateSignal = self.valuesChanged, + }, {root}) +end + +function Stylizer:destroy() + if self.themeChangedConnection then + self.themeChangedConnection:Disconnect() + end +end + +function Stylizer:convertStyleKeys(t, name, parent, styleKeysTable) + assert(t, "Style:convertStyleKeys expects 't' parameter") + assertIfNotNil(parent, (typeof(parent) == "table"), ("Style:convertStyleKeys expects 'parent' parameter to be a table, but got a %s"):format(typeof(table))) + assertIfNotNil(styleKeysTable, (typeof(styleKeysTable) == "table"), ("Style:convertStyleKeys expects 'styleKeysTable' parameter to be a table, but got a %s"):format(typeof(styleKeysTable))) + + local mt + if parent then + mt = { + __index = parent, + __styleName = name or '[unnamed style]', + } + else + mt = { + __index = Stylizer, + __styleName = name or "[Root Style]", + } + end + + -- Link input and parent styles + local this = setmetatable(t, mt) + + -- Process properties and create nested styles + for propName, v in pairs(t) do + local override + if type(v) == "table" then + if getmetatable(v) == StyleKey then + -- NOTE: StyleKeys need to be stringified + override = (parent and parent[v]) or (styleKeysTable and styleKeysTable[tostring(v)]) + or error(("StyleKey %s defines no value @ key %s"):format(v.name, propName)) + + elseif type(v.render) ~= "function" then + override = self:convertStyleKeys(v, propName, this, styleKeysTable) + end + + elseif type(v) == "function" then + local generated = v(this) or {} + if type(generated) == "table" then + override = self:convertStyleKeys(generated, propName, this, styleKeysTable) + end + end + + if override then + rawset(this, propName, override) + end + end + + return this +end + +function Stylizer:getPathString() + local path + local m = getmetatable(self) + while m and m.__styleName do + if path then + path = tostring(m.__styleName) .. "-->" .. tostring(path) + else + path = m.__styleName + end + m = getmetatable(m.__index) + end + return path or "" +end + +function Stylizer:getConsumerItem(target) + local style = target.props.Style + local currentStyle = self.__calculatedStyle + local componentSymbol = ComponentSymbols[target.__componentName] + + if not currentStyle then + assert(false, "Style:getConsumerItem() is unable to find the Style in _context of", target.__componentName) + return self + end + + local result + if style then + if type(style) == "table" then + result = setmetatable(style, { + __index = Stylizer + }) + + elseif type(style) == "string" then + result = self:__getClassStyle(style, currentStyle, componentSymbol) + end + end + + result = result or currentStyle[componentSymbol] or currentStyle or self + + local modifier = target.props.StyleModifier or target.state.StyleModifier + local modStyle = result[modifier] + if modifier and modStyle then + setmetatable(modStyle, { + __index = result, + }) + + for k, v in pairs(modStyle) do + if type(v) == "table" and result[k] then + setmetatable(v, { + __index = result[k], + }) + end + end + + result = modStyle + end + + if self.getUILibraryTheme then + result.getUILibraryTheme = self.getUILibraryTheme + end + + return result +end + +function Stylizer.mock(t, themeProps, callback) + local self = Stylizer.new(t, themeProps) + + if themeProps.themeChangedConnection then + self.themeChangedConnection:Disconnect() + self.themeChangedConnection = themeProps.themeChangedConnection:Connect(function() + callback() + self:__recalculateTheme(themeProps) + end) + end + + return self +end +return Stylizer diff --git a/BuiltInPlugins/LocalizationTools/Packages/Framework/Style/Stylizer.spec.lua b/BuiltInPlugins/LocalizationTools/Packages/Framework/Style/Stylizer.spec.lua new file mode 100644 index 0000000000..999039a5b1 --- /dev/null +++ b/BuiltInPlugins/LocalizationTools/Packages/Framework/Style/Stylizer.spec.lua @@ -0,0 +1,539 @@ +return function() + local Stylizer = require(script.Parent.Stylizer) + local Framework = script.Parent.Parent + local Roact = require(Framework.Parent.Roact) + local provide = require(Framework.ContextServices.provide) + local mapToProps = require(Framework.ContextServices.mapToProps) + + local FrameworkStyle = require(Framework.Style) + local ui = FrameworkStyle.ComponentSymbols + local StyleKey = require(script.Parent.StyleKey) + + local Util = require(Framework.Util) + local Signal = Util.Signal + local StyleModifier = Util.StyleModifier + + local testSymbols = {} + + local function createTestThemedComponent(render) + local TestThemedComponent = Roact.PureComponent:extend("TestThemedComponent") + + function TestThemedComponent:render() + local style = self.props.Theme + if render then + render(style) + end + end + + mapToProps(TestThemedComponent, { + Theme = Stylizer, + }) + + return TestThemedComponent + end + + local function addSymbol(symbolName) + local symbol = ui:add(symbolName) + table.insert(testSymbols, symbol) + return symbol + end + + local function cleanUpSymbols() + for _,v in pairs(testSymbols) do + ui[v] = nil + end + end + + local function createDefaultStylizer(initialTable) + initialTable = initialTable or {} + return Stylizer.new(initialTable, { + getThemeName = function() end, + }) + end + + describe("new", function() + afterEach(function() + cleanUpSymbols() + end) + + it("should expect an initial table as a parameter", function() + expect(function() + Stylizer.new(nil, {}) + end).to.throw() + end) + + it("should expect an inial themeProps as a parameter", function() + expect(function() + Stylizer.new({}, nil) + end).to.throw() + end) + + it("should return a new Stylizer", function() + local stylizer = createDefaultStylizer() + expect(stylizer).to.be.ok() + stylizer:destroy() + end) + end) + + describe("extend", function() + it("should return a new Stylizer", function() + local stylizer = createDefaultStylizer() + stylizer = stylizer:extend({}) + expect(stylizer).to.be.ok() + stylizer:destroy() + end) + + it("should merge the table with existing values", function() + local oldValue = "old" + local addedValue = "add" + local overrideValue = "world" + local stylizer = createDefaultStylizer({ + old = oldValue, + override = "old", + }) + stylizer = stylizer:extend({ + added = addedValue, + override = overrideValue, + }) + + local result = stylizer.__calculatedStyle + expect(result.old).to.equal(oldValue) + expect(result.override).to.equal(overrideValue) + expect(result.added).to.equal(addedValue) + stylizer:destroy() + end) + end) + + it("should be providable as a ContextItem", function() + local stylizer = createDefaultStylizer() + local element = provide({stylizer}, { + Frame = Roact.createElement("Frame"), + }) + local instance = Roact.mount(element) + Roact.unmount(instance) + stylizer:destroy() + end) + + describe("destroy", function() + it("should disconnect from the theme changed signal", function() + local themeChanged = Signal.new() + local calls = 0 + local callback = function(theme, getColors) + calls = calls + 1 + return {} + end + local stylizer = Stylizer.mock({}, { + getThemeName = function() return "Light" end, + themesList = { ["Light"] = {} }, + themeChangedConnection = themeChanged, + }, callback) + stylizer:destroy() + themeChanged:Fire() + expect(calls).to.equal(0) + end) + end) + + describe("getStyleKeysTable", function() + it("should get a list of all StyleKey values in the first layer of the passed in table", function() + local styleKeyValue = "world" + local styleKeysTable = { + [StyleKey.hello] = styleKeyValue, + notAStyleKey = "no", + } + local result = Stylizer:getStyleKeysTable(styleKeysTable) + expect(result.notAStyleKey).to.never.be.ok() + expect(result[tostring(StyleKey.hello)]).to.equal(styleKeyValue) + end) + end) + + describe("convertStyleKeys", function() + it("should replace all StyleKey values with the correct value", function() + local redValue = "Mario" + local styleKeysTable = { + [StyleKey.Red] = redValue, + } + styleKeysTable = Stylizer:getStyleKeysTable(styleKeysTable) + local tableToConvert = { + itsAme = StyleKey.Red, + its = { + aMe = StyleKey.Red, + } + } + local result = Stylizer:convertStyleKeys(tableToConvert, nil, nil, styleKeysTable) + expect(result.itsAme).to.equal(redValue) + expect(result.its.aMe).to.equal(redValue) + end) + end) + + describe("getPathString", function() + afterEach(function() + cleanUpSymbols() + end) + + it("should get the correct path value for root styles", function() + local receivedValue + local themedComponent = createTestThemedComponent(function(style) + receivedValue = style:getPathString() + end) + addSymbol("TestThemedComponent") + local theme = createDefaultStylizer({}) + local element = provide({theme}, { + ThemedComponent = Roact.createElement(themedComponent, {}), + }) + local instance = Roact.mount(element) + Roact.unmount(instance) + expect(receivedValue).to.be.ok() + expect(receivedValue).to.equal("[Root Style]") + theme:destroy() + end) + + it("should get the correct path value for a component", function() + local receivedValue + local themedComponent = createTestThemedComponent(function(style) + receivedValue = style:getPathString() + end) + addSymbol("TestThemedComponent") + local theme = createDefaultStylizer({ + [ui.TestThemedComponent] = { + hello = "world", + }, + }) + local element = provide({theme}, { + ThemedComponent = Roact.createElement(themedComponent), + }) + local instance = Roact.mount(element) + Roact.unmount(instance) + expect(receivedValue).to.be.ok() + expect(receivedValue).to.equal("[Root Style]-->ComponentSymbols(TestThemedComponent)") + theme:destroy() + end) + + it("should get the correct path value for a Style in the Root", function() + local receivedValue + local themedComponent = createTestThemedComponent(function(style) + receivedValue = style:getPathString() + end) + addSymbol("TestThemedComponent") + local theme = createDefaultStylizer({ + [ui.TestThemedComponent] = { + hello = "world", + }, + override = { + test = "test", + } + }) + local element = provide({theme}, { + ThemedComponent = Roact.createElement(themedComponent, { + Style = "override", + }), + }) + local instance = Roact.mount(element) + Roact.unmount(instance) + expect(receivedValue).to.be.ok() + expect(receivedValue).to.equal("[Root Style]-->override") + theme:destroy() + end) + + it("should get the correct path value for an ampersand Style", function() + local receivedValue + local themedComponent = createTestThemedComponent(function(style) + receivedValue = style:getPathString() + end) + addSymbol("TestThemedComponent") + local theme = createDefaultStylizer({ + [ui.TestThemedComponent] = { + hello = "world", + ["&override"] = { + test = "test", + } + }, + }) + local element = provide({theme}, { + ThemedComponent = Roact.createElement(themedComponent, { + Style = "override", + }), + }) + local instance = Roact.mount(element) + Roact.unmount(instance) + expect(receivedValue).to.be.ok() + expect(receivedValue).to.equal("[Root Style]-->ComponentSymbols(TestThemedComponent)-->&override") + theme:destroy() + end) + end) + + describe("getConsumerItem", function() + afterEach(function() + cleanUpSymbols() + end) + + it("should get the corresponding component's style values", function() + local receivedValue + local themedComponent = createTestThemedComponent(function(style) + receivedValue = style + end) + local componentValue = "world" + addSymbol("TestThemedComponent") + addSymbol("doNotGet") + local theme = createDefaultStylizer({ + [ui.TestThemedComponent] = { + hello = componentValue, + }, + [ui.doNotGet] = { + ohNo = "no", + }, + }) + local element = provide({theme}, { + ThemedComponent = Roact.createElement(themedComponent), + }) + local instance = Roact.mount(element) + Roact.unmount(instance) + expect(receivedValue).to.be.ok() + expect(receivedValue.hello).to.equal(componentValue) + expect(receivedValue.ohNo).to.never.be.ok() + theme:destroy() + end) + + it("should replace StyleKey values with the correct value", function() + local receivedValue + local themedComponent = createTestThemedComponent(function(style) + receivedValue = style + end) + local blueValue = Color3.new(1, 1, 1) + local redValue = Color3.new(1, 0, 0) + addSymbol("TestThemedComponent") + local theme = createDefaultStylizer({ + [StyleKey.Blue] = blueValue, + + [ui.TestThemedComponent] = { + theSkyIs = StyleKey.Blue, + ["&override"] = { + theSkyIs = redValue, + } + } + }) + local element = provide({theme}, { + ThemedComponent = Roact.createElement(themedComponent, {}), + }) + local instance = Roact.mount(element) + Roact.unmount(instance) + expect(receivedValue.theSkyIs).to.equal(blueValue) + theme:destroy() + end) + + it("should be overridden with the correct ampersand when a string is passed into Style prop", function() + local receivedValue + local themedComponent = createTestThemedComponent(function(style) + receivedValue = style + end) + local blueValue = Color3.new(1, 1, 1) + local redValue = Color3.new(1, 0, 0) + addSymbol("TestThemedComponent") + local theme = createDefaultStylizer({ + [ui.TestThemedComponent] = { + theSkyIs = blueValue, + ["&override"] = { + theSkyIs = redValue, + } + } + }) + local element = provide({theme}, { + ThemedComponent = Roact.createElement(themedComponent, { + Style = "override", + }), + }) + local instance = Roact.mount(element) + Roact.unmount(instance) + expect(receivedValue.theSkyIs).to.equal(redValue) + theme:destroy() + end) + + it("should get the correct value for a Style in the Root", function() + local receivedValue + local themedComponent = createTestThemedComponent(function(style) + receivedValue = style + end) + addSymbol("TestThemedComponent") + local blueValue = Color3.new(1, 1, 1) + local theme = createDefaultStylizer({ + [ui.TestThemedComponent] = { + hello = "world", + }, + override = { + hello = blueValue, + } + }) + local element = provide({theme}, { + ThemedComponent = Roact.createElement(themedComponent, { + Style = "override", + }), + }) + local instance = Roact.mount(element) + Roact.unmount(instance) + expect(receivedValue).to.be.ok() + expect(receivedValue.hello).to.equal(blueValue) + theme:destroy() + end) + + it("should be overridden with the table passed into Style prop", function() + local receivedValue + local themedComponent = createTestThemedComponent(function(style) + receivedValue = style + end) + local blueValue = Color3.new(1, 1, 1) + local redValue = Color3.new(1, 0, 0) + local blackValue = Color3.new(0, 0, 0) + addSymbol("TestThemedComponent") + local theme = createDefaultStylizer({ + [ui.TestThemedComponent] = { + theSkyIs = blueValue, + ["&override"] = { + theSkyIs = redValue, + } + } + }) + local element = provide({theme}, { + ThemedComponent = Roact.createElement(themedComponent, { + Style = { + theSkyIs = blackValue + }, + }), + }) + local instance = Roact.mount(element) + Roact.unmount(instance) + expect(receivedValue.theSkyIs).to.equal(blackValue) + theme:destroy() + end) + + it("should be called each time the theme updates", function() + local themeChanged = Signal.new() + local calls = 0 + local callback = function(theme, getColors) + calls = calls + 1 + return {} + end + local stylizer = Stylizer.mock({}, { + getThemeName = function() return "Light" end, + themesList = { ["Light"] = {} }, + themeChangedConnection = themeChanged, + }, callback) + themeChanged:Fire() + expect(calls).to.equal(1) + themeChanged:Fire() + expect(calls).to.equal(2) + + stylizer:destroy() + end) + + describe("using StyleModifier prop", function() + afterEach(function() + cleanUpSymbols() + end) + + it("should take values from the current StyleModifier", function() + local receivedValue + local themedComponent = createTestThemedComponent(function(style) + receivedValue = style + end) + addSymbol("TestThemedComponent") + local theme = createDefaultStylizer({ + [ui.TestThemedComponent] = { + Value = "Test", + [StyleModifier.Hover] = { + Value = "HoverTest", + }, + } + }) + local element = provide({theme}, { + ThemedComponent = Roact.createElement(themedComponent, { + StyleModifier = StyleModifier.Hover, + }), + }) + local instance = Roact.mount(element) + Roact.unmount(instance) + expect(receivedValue.Value).to.equal("HoverTest") + theme:destroy() + end) + + it("should take values from ampersand Style and StyleModifier combined", function() + local receivedValue + local themedComponent = createTestThemedComponent(function(style) + receivedValue = style + end) + addSymbol("TestThemedComponent") + local theme = createDefaultStylizer({ + [ui.TestThemedComponent] = { + Value = "Test", + ["&Styled"] = { + [StyleModifier.Hover] = { + Value = "HoverTest", + }, + } + }, + }) + local element = provide({theme}, { + ThemedComponent = Roact.createElement(themedComponent, { + Style = "Styled", + StyleModifier = StyleModifier.Hover, + }), + }) + local instance = Roact.mount(element) + Roact.unmount(instance) + expect(receivedValue.Value).to.equal("HoverTest") + theme:destroy() + end) + + it("should take values from passed-in Style and StyleModifier combined", function() + local receivedValue + local themedComponent = createTestThemedComponent(function(style) + receivedValue = style + end) + addSymbol("TestThemedComponent") + local theme = createDefaultStylizer({ + [ui.TestThemedComponent] = { + Value = "Test", + }, + }) + local element = provide({theme}, { + ThemedComponent = Roact.createElement(themedComponent, { + Style = { + [StyleModifier.Hover] = { + Value = "HoverTest", + }, + }, + StyleModifier = StyleModifier.Hover, + }), + }) + local instance = Roact.mount(element) + Roact.unmount(instance) + expect(receivedValue.Value).to.equal("HoverTest") + theme:destroy() + end) + + it("should fall back to the style if no modified value is found", function() + local receivedValue + local themedComponent = createTestThemedComponent(function(style) + receivedValue = style + end) + local theme = createDefaultStylizer({ + [ui.TestThemedComponent] = { + Value = "Test", + OtherValue = "Test", + [StyleModifier.Hover] = { + Value = "HoverTest", + }, + }, + }) + local element = provide({theme}, { + ThemedComponent = Roact.createElement(themedComponent, { + StyleModifier = StyleModifier.Hover, + }), + }) + local instance = Roact.mount(element) + Roact.unmount(instance) + expect(receivedValue.Value).to.equal("HoverTest") + expect(receivedValue.OtherValue).to.equal("Test") + theme:destroy() + end) + end) + end) +end \ No newline at end of file diff --git a/BuiltInPlugins/LocalizationTools/Packages/Framework/Style/Themes/BaseTheme.lua b/BuiltInPlugins/LocalizationTools/Packages/Framework/Style/Themes/BaseTheme.lua new file mode 100644 index 0000000000..f94e37f1ff --- /dev/null +++ b/BuiltInPlugins/LocalizationTools/Packages/Framework/Style/Themes/BaseTheme.lua @@ -0,0 +1,48 @@ +--[[ + Combines all style.lua tables in each component. + Note that component styles expect defined StyleKeys when used in + Stylizer. Combine this file with DarkTheme, LightTheme, or your own + theme color using Stylizer.new(DarkTheme) and Stylizer:extend(BaseTheme). +]] + +local Framework = script.Parent.Parent.Parent +local StudioUIFolderData = require(Framework.StudioUI.StudioUIFolderData) +local UIFolderData = require(Framework.UI.UIFolderData) + +local ComponentSymbols = require(Framework.Style.ComponentSymbols) +local StyleKey = require(Framework.Style.StyleKey) + +local StudioFrameworkStyles = Framework.StudioUI.StudioFrameworkStyles +local Common = require(StudioFrameworkStyles.Common) + +local Util = Framework.Util +local Cryo = require(Util.Cryo) +local FlagsList = require(Util.Flags).new({ + FFlagRefactorDevFrameworkTheme = {"RefactorDevFrameworkTheme"}, +}) + + +if (not FlagsList:get("FFlagRefactorDevFrameworkTheme")) then + return {} +end + +local styles = Cryo.Dictionary.join(Common.MainText, { + -- Common styles + Color = StyleKey.MainBackground, + BorderColor = StyleKey.Border, +}) + +local function createComponentStyles(folderData) + for _,folder in pairs(folderData) do + if folder.style then + assert(ComponentSymbols[folder.name] ~= nil, ("No Symbol was found for the component %s"):format(folder.name)) + local componentStyleFile = require(folder.style) + styles[ComponentSymbols[folder.name]] = componentStyleFile + end + end +end + +createComponentStyles(UIFolderData) +createComponentStyles(StudioUIFolderData) + +return styles \ No newline at end of file diff --git a/BuiltInPlugins/LocalizationTools/Packages/Framework/Style/Themes/DarkTheme.lua b/BuiltInPlugins/LocalizationTools/Packages/Framework/Style/Themes/DarkTheme.lua new file mode 100644 index 0000000000..81d39841bd --- /dev/null +++ b/BuiltInPlugins/LocalizationTools/Packages/Framework/Style/Themes/DarkTheme.lua @@ -0,0 +1,54 @@ +local Framework = script.Parent.Parent.Parent +local Colors = require(Framework.Style.Colors) +local StyleKey = require(Framework.Style.StyleKey) + +return { + [StyleKey.Border] = Colors.Carbon, + [StyleKey.BrightText] = Colors.White, + [StyleKey.Button] = Colors.Gray, + [StyleKey.ButtonText] = Colors.Gray_Light, + [StyleKey.ButtonHover] = Colors.Gray, + [StyleKey.ButtonDisabled] = Colors.lighter(Colors.Black, 0.26), + [StyleKey.ButtonPressed] = Colors.lighter(Colors.Black, 0.16), + + [StyleKey.CategoryItem] = Color3.fromRGB(53, 53, 53), + + [StyleKey.DialogMainButton] = Colors.Blue, + [StyleKey.DialogMainButtonDisabled] = Colors.Blue, + [StyleKey.DialogMainButtonHover] = Colors.Blue, + [StyleKey.DialogMainButtonSelected] = Colors.Blue_Dark, + [StyleKey.DialogMainButtonText] = Colors.White, + [StyleKey.DialogMainButtonTextDisabled] = Color3.fromRGB(102, 102, 102), + [StyleKey.DimmedText] = Colors.lighter(Colors.Black, 0.4), + + [StyleKey.ErrorText] = Color3.fromRGB(255, 68, 68), + + [StyleKey.InputFieldBackground] = Color3.fromRGB(37, 37, 37), + + [StyleKey.LinkText] = Color3.fromRGB(60, 180, 255), + + [StyleKey.MainBackground] = Colors.Slate, + [StyleKey.MainButton] = Colors.Blue, + [StyleKey.MainText] = Colors.Gray_Light, + [StyleKey.MainTextDisabled] = Color3.fromRGB(85, 85, 85), + [StyleKey.Mid] = Color3.fromRGB(34, 34, 34), + + [StyleKey.RibbonTab] = Color3.fromRGB(37, 37, 37), + + [StyleKey.ScrollBarBackground] = Color3.fromRGB(41, 41, 41), + [StyleKey.ScrollBar] = Colors.lighter(Colors.Black, 0.22), + [StyleKey.SliderKnobColor] = Color3.fromRGB(85, 85, 85), + [StyleKey.SliderKnobImage] = "rbxasset://textures/DeveloperFramework/slider_knob.png", + [StyleKey.SliderBackground] = Color3.fromRGB(37, 37, 37), + [StyleKey.SubText] = Color3.fromRGB(170, 170, 170), + + [StyleKey.TitlebarText] = Color3.fromRGB(204, 204, 204), + [StyleKey.ToggleOnImage] = "rbxasset://textures/RoactStudioWidgets/toggle_on_dark.png", + [StyleKey.ToggleOffImage] = "rbxasset://textures/RoactStudioWidgets/toggle_off_dark.png", + [StyleKey.ToggleDisabledImage] = "rbxasset://textures/RoactStudioWidgets/toggle_disable_dark.png", + + [StyleKey.WarningText] = Color3.fromRGB(255, 141, 60), + + Font = Enum.Font.SourceSans, + TextSize = 18, +} \ No newline at end of file diff --git a/BuiltInPlugins/LocalizationTools/Packages/Framework/Style/Themes/LightTheme.lua b/BuiltInPlugins/LocalizationTools/Packages/Framework/Style/Themes/LightTheme.lua new file mode 100644 index 0000000000..93d7586ba8 --- /dev/null +++ b/BuiltInPlugins/LocalizationTools/Packages/Framework/Style/Themes/LightTheme.lua @@ -0,0 +1,54 @@ +local Framework = script.Parent.Parent.Parent +local StyleKey = require(Framework.Style.StyleKey) +local Colors = require(Framework.Style.Colors) + +return { + [StyleKey.Border] = Color3.fromRGB(182, 182, 182), + [StyleKey.BrightText] = Colors.Black, + [StyleKey.Button] = Colors.White, + [StyleKey.ButtonText] = Colors.Black, + [StyleKey.ButtonHover] = Color3.fromRGB(228, 238, 254), + [StyleKey.ButtonDisabled] = Colors.White, + [StyleKey.ButtonPressed] = Color3.fromRGB(219, 219, 219), + + [StyleKey.CategoryItem] = Color3.fromRGB(233, 233, 233), + + [StyleKey.DialogMainButton] = Colors.Blue, + [StyleKey.DialogMainButtonDisabled] = Color3.fromRGB(153, 218, 255), + [StyleKey.DialogMainButtonHover] = Colors.Blue_Light, + [StyleKey.DialogMainButtonSelected] = Colors.Blue_Dark, + [StyleKey.DialogMainButtonText] = Colors.White, + [StyleKey.DialogMainButtonTextDisabled] = Color3.fromRGB(102, 102, 102), + [StyleKey.DimmedText] = Color3.fromRGB(136, 136, 136), + + [StyleKey.ErrorText] = Colors.Red, + + [StyleKey.InputFieldBackground] = Colors.White, + + [StyleKey.LinkText] = Colors.Blue_Light, + + [StyleKey.MainBackground] = Colors.White, + [StyleKey.MainButton] = Color3.fromRGB(228, 238, 254), + [StyleKey.MainText] = Colors.Black, + [StyleKey.MainTextDisabled] = Color3.fromRGB(120, 120, 120), + [StyleKey.Mid] = Color3.fromRGB(238, 238, 238), + + [StyleKey.RibbonTab] = Color3.fromRGB(243, 243, 243), + + [StyleKey.ScrollBarBackground] = Color3.fromRGB(238, 238, 238), + [StyleKey.ScrollBar] = Colors.White, + [StyleKey.SliderKnobColor] = Colors.White, + [StyleKey.SliderKnobImage] = "rbxasset://textures/DeveloperFramework/slider_knob_light.png", + [StyleKey.SliderBackground] = Color3.fromRGB(204, 204, 204), + [StyleKey.SubText] = Color3.fromRGB(170, 170, 170), + + [StyleKey.TitlebarText] = Colors.Black, + [StyleKey.ToggleOnImage] = "rbxasset://textures/RoactStudioWidgets/toggle_on_light.png", + [StyleKey.ToggleOffImage] = "rbxasset://textures/RoactStudioWidgets/toggle_off_light.png", + [StyleKey.ToggleDisabledImage] = "rbxasset://textures/RoactStudioWidgets/toggle_disable_light.png", + + [StyleKey.WarningText] = Color3.fromRGB(255, 128, 0), + + Font = Enum.Font.SourceSans, + TextSize = 18, +} \ No newline at end of file diff --git a/BuiltInPlugins/LocalizationTools/Packages/Framework/Style/Themes/StudioTheme.lua b/BuiltInPlugins/LocalizationTools/Packages/Framework/Style/Themes/StudioTheme.lua new file mode 100644 index 0000000000..9936f07430 --- /dev/null +++ b/BuiltInPlugins/LocalizationTools/Packages/Framework/Style/Themes/StudioTheme.lua @@ -0,0 +1,49 @@ +--[[ + The Default theme for Studio. +]] + +local Framework = script.Parent.Parent.Parent +local DarkTheme = require(Framework.Style.Themes.DarkTheme) +local LightTheme = require(Framework.Style.Themes.LightTheme) +local createDefaultTheme = require(Framework.Style.createDefaultTheme) +local Cryo = require(Framework.Util).Cryo + +local getThemeName = function() + return settings().Studio.Theme.Name +end + +local StudioTheme = {} + +function StudioTheme.new(darkThemeOverride, lightThemeOverride) + local darkTheme = DarkTheme + if darkThemeOverride then + darkTheme = Cryo.Dictionary.join(DarkTheme, darkThemeOverride) + end + + local lightTheme = LightTheme + if lightThemeOverride then + lightTheme = Cryo.Dictionary.join(LightTheme, lightThemeOverride) + end + + local themeProps = { + getThemeName = getThemeName, + themesList = { + ["Dark"] = darkTheme, + ["Light"] = lightTheme, + }, + themeChangedConnection = settings().Studio.ThemeChanged, + } + return createDefaultTheme(themeProps) +end + +function StudioTheme.mock() + local themeProps = { + getThemeName = function() return "Dark" end, + themesList = { + Dark = DarkTheme, + }, + } + return createDefaultTheme(themeProps) +end + +return StudioTheme \ No newline at end of file diff --git a/BuiltInPlugins/LocalizationTools/Packages/Framework/Style/Themes/StudioTheme.spec.lua b/BuiltInPlugins/LocalizationTools/Packages/Framework/Style/Themes/StudioTheme.spec.lua new file mode 100644 index 0000000000..0392df2733 --- /dev/null +++ b/BuiltInPlugins/LocalizationTools/Packages/Framework/Style/Themes/StudioTheme.spec.lua @@ -0,0 +1,8 @@ +return function() + local StudioTheme = require(script.Parent.StudioTheme) + + it("should create a base theme without issue", function() + local result = StudioTheme.mock() + expect(result).to.be.ok() + end) +end \ No newline at end of file diff --git a/BuiltInPlugins/LocalizationTools/Packages/Framework/Style/createDefaultTheme.lua b/BuiltInPlugins/LocalizationTools/Packages/Framework/Style/createDefaultTheme.lua new file mode 100644 index 0000000000..923e1ec9f7 --- /dev/null +++ b/BuiltInPlugins/LocalizationTools/Packages/Framework/Style/createDefaultTheme.lua @@ -0,0 +1,17 @@ +--[[ + Creates a default theme with given props. +]] + +local Style = script.Parent + +local BaseTheme = require(Style.Themes.BaseTheme) +local Stylizer = require(Style.Stylizer) + +return function(themeProps) + assert(typeof(themeProps) == "table", "createDefaultTheme expects themeProps parameter to be a table") + assert(typeof(themeProps.themesList) == "table", "createDefaultTheme expects themeProps to contain a table `themesList`") + assert(typeof(themeProps.getThemeName) == "function", "createDefaultTheme expects themeProps to contain a function `getThemeName`") + + local style = Stylizer.new(themeProps.themesList[themeProps.getThemeName()], themeProps) + return style:extend(BaseTheme) +end \ No newline at end of file diff --git a/BuiltInPlugins/LocalizationTools/Packages/Framework/Style/createDefaultTheme.spec.lua b/BuiltInPlugins/LocalizationTools/Packages/Framework/Style/createDefaultTheme.spec.lua new file mode 100644 index 0000000000..8fbbba0cc0 --- /dev/null +++ b/BuiltInPlugins/LocalizationTools/Packages/Framework/Style/createDefaultTheme.spec.lua @@ -0,0 +1,30 @@ +return function() + local createDefaultTheme = require(script.Parent.createDefaultTheme) + local DarkTheme = require(script.Parent.Themes.DarkTheme) + + local function callWithProps() + local themeProps = { + getThemeName = function() return "Dark" end, + themesList = { + Dark = DarkTheme, + }, + } + + return createDefaultTheme(themeProps) + end + + it("should create a base theme without issue", function() + local result = callWithProps() + expect(result).to.be.ok() + end) + + it("should be extendable", function() + local result = callWithProps() + local success, _ = pcall(function() + return result:extend({ + hello = "world", + }) + end) + expect(success).to.equal(true) + end) +end \ No newline at end of file diff --git a/BuiltInPlugins/LocalizationTools/Packages/Framework/Style/getRawComponentStyle.lua b/BuiltInPlugins/LocalizationTools/Packages/Framework/Style/getRawComponentStyle.lua new file mode 100644 index 0000000000..5c76de757c --- /dev/null +++ b/BuiltInPlugins/LocalizationTools/Packages/Framework/Style/getRawComponentStyle.lua @@ -0,0 +1,17 @@ +--[[ + Gets the original raw, un-calcuated UI or StudioUI style table for a given component. +]] + +local Framework = script.Parent.Parent +local StudioUIFolderData = require(Framework.StudioUI.StudioUIFolderData) +local UIFolderData = require(Framework.UI.UIFolderData) + +return function(componentName) + local componentData = UIFolderData[componentName] or StudioUIFolderData[componentName] + local result + + if componentData.style then + result = require(componentData.style) + end + return result +end \ No newline at end of file diff --git a/BuiltInPlugins/LocalizationTools/Packages/Framework/Style/getRawComponentStyle.spec.lua b/BuiltInPlugins/LocalizationTools/Packages/Framework/Style/getRawComponentStyle.spec.lua new file mode 100644 index 0000000000..c8d9020535 --- /dev/null +++ b/BuiltInPlugins/LocalizationTools/Packages/Framework/Style/getRawComponentStyle.spec.lua @@ -0,0 +1,22 @@ +return function() + local Framework = script.Parent.Parent + local getRawComponentStyle = require(script.Parent.getRawComponentStyle) + + it("should get the style table for the correct UI component", function() + local result = getRawComponentStyle("Button") + + local styleFile = Framework.UI.Button:FindFirstChild("style") + local styleTable = require(styleFile) + + expect(styleTable).to.equal(result) + end) + + it("should get the style table for the correct StudioUI component", function() + local result = getRawComponentStyle("SearchBar") + + local styleFile = Framework.StudioUI.SearchBar:FindFirstChild("style") + local styleTable = require(styleFile) + + expect(styleTable).to.equal(result) + end) +end \ No newline at end of file diff --git a/BuiltInPlugins/LocalizationTools/Packages/Framework/TestHelpers.lua b/BuiltInPlugins/LocalizationTools/Packages/Framework/TestHelpers.lua index 075bd0fb21..4ae630e7b4 100644 --- a/BuiltInPlugins/LocalizationTools/Packages/Framework/TestHelpers.lua +++ b/BuiltInPlugins/LocalizationTools/Packages/Framework/TestHelpers.lua @@ -1,6 +1,11 @@ local strict = require(script.Parent.Util.strict) return strict({ + Instances = require(script.Instances), + + makeSettableValue = require(script.makeSettableValue), provideMockContext = require(script.provideMockContext), runFrameworkTests = require(script.runFrameworkTests), -}) \ No newline at end of file + setEquals = require(script.setEquals), + testImmutability = require(script.testImmutability), +}) diff --git a/BuiltInPlugins/LocalizationTools/Packages/Framework/TestHelpers/Instances.lua b/BuiltInPlugins/LocalizationTools/Packages/Framework/TestHelpers/Instances.lua new file mode 100644 index 0000000000..c005f97da5 --- /dev/null +++ b/BuiltInPlugins/LocalizationTools/Packages/Framework/TestHelpers/Instances.lua @@ -0,0 +1,10 @@ +local strict = require(script.Parent.Parent.Util.strict) + +return strict({ + MockAnalyticsService = require(script.MockAnalyticsService), + MockMouse = require(script.MockMouse), + MockPlugin = require(script.MockPlugin), + MockPluginToolbar = require(script.MockPluginToolbar), + MockPluginToolbarButton = require(script.MockPluginToolbarButton), + MockSelectionService = require(script.MockSelectionService), +}) diff --git a/BuiltInPlugins/LocalizationTools/Packages/Framework/TestHelpers/Instances/MockAnalyticsService.lua b/BuiltInPlugins/LocalizationTools/Packages/Framework/TestHelpers/Instances/MockAnalyticsService.lua new file mode 100644 index 0000000000..7da446b5cc --- /dev/null +++ b/BuiltInPlugins/LocalizationTools/Packages/Framework/TestHelpers/Instances/MockAnalyticsService.lua @@ -0,0 +1,41 @@ +local MockAnalyticsService = {} +MockAnalyticsService.__index = MockAnalyticsService + +function MockAnalyticsService.new() + local self = setmetatable({ + eventCount = 0, + lastEvent = nil, + + _sessionId = "", + }, MockAnalyticsService) + + return self +end + +function MockAnalyticsService:Destroy() +end + +function MockAnalyticsService:GetSessionId() + return self._sessionId +end + +function MockAnalyticsService:SendEventDeferred(target, context, evt, argsTable) + local event = { + target = target, + ctx = context, + evt = evt, + } + + assert(type(argsTable) == "table", "expected table, argsTable was " .. type(argsTable)) + for k, v in pairs(argsTable) do + if event[k] ~= nil then + warn("Overriding base keyword " .. k .. " in via argsTable in SendEventDeferred()." ) + end + event[k] = v + end + + self.lastEvent = event + self.eventCount = self.eventCount + 1 +end + +return MockAnalyticsService diff --git a/BuiltInPlugins/LocalizationTools/Packages/Framework/TestHelpers/Instances/MockMouse.lua b/BuiltInPlugins/LocalizationTools/Packages/Framework/TestHelpers/Instances/MockMouse.lua new file mode 100644 index 0000000000..c3178b260d --- /dev/null +++ b/BuiltInPlugins/LocalizationTools/Packages/Framework/TestHelpers/Instances/MockMouse.lua @@ -0,0 +1,23 @@ +local DevFrameworkRoot = script.Parent.Parent.Parent +local Signal = require(DevFrameworkRoot.Util.Signal) + +local MockMouse = {} +MockMouse.__index = MockMouse + +function MockMouse.new() + return setmetatable({ + Icon = "rbxasset://SystemCursors/Arrow", + + Origin = CFrame.new(), + UnitRay = Ray.new(Vector3.new(), Vector3.new()), + Target = nil, + + WheelForward = Signal.new(), + WheelBackward = Signal.new(), + }, MockMouse) +end + +function MockMouse:Destroy() +end + +return MockMouse diff --git a/BuiltInPlugins/LocalizationTools/Packages/Framework/TestHelpers/Instances/MockPlugin.lua b/BuiltInPlugins/LocalizationTools/Packages/Framework/TestHelpers/Instances/MockPlugin.lua new file mode 100644 index 0000000000..38b2fcabac --- /dev/null +++ b/BuiltInPlugins/LocalizationTools/Packages/Framework/TestHelpers/Instances/MockPlugin.lua @@ -0,0 +1,133 @@ +local DevFrameworkRoot = script.Parent.Parent.Parent +local Signal = require(DevFrameworkRoot.Util.Signal) + +local MockMouse = require(script.Parent.MockMouse) +local MockPluginToolbar = require(script.Parent.MockPluginToolbar) + +local MockPlugin = {} +MockPlugin.__index = MockPlugin + +local function createScreenGui() + local screen = Instance.new("ScreenGui", game.CoreGui) + screen.Name = "PluginMockGui" + screen.ZIndexBehavior = Enum.ZIndexBehavior.Sibling + return screen +end + +--[[ + id : string? + mockedPlugins : {[MockPlugin] : boolean}? + Optional set for interfacing with other plugins created in a unit test. + E.g. when calling MockPlugin:Activate(), all other plugins in the mockedPlugins set get deactivated + For tests that don't need this functionality, leave mockedPlugins as nil +]] +function MockPlugin.new(id, mockedPlugins) + local self = setmetatable({ + _id = id or "", + Name = id or "MockPlugin", + + Deactivation = Signal.new(), + Unloading = Signal.new(), + + _activated = false, + _activatedWithExclusiveMouse = false, + + _mouse = MockMouse.new(), + + _toolbars = {}, + + subWindows = {}, + }, MockPlugin) + + if mockedPlugins then + self._mockedPlugins = mockedPlugins + self._mockedPlugins[self] = true + end + + return self +end + +function MockPlugin:Destroy() + for _, toolbar in pairs(self._toolbars) do + toolbar:Destroy() + end + self._toolbars = {} + + if self._mouse then + self._mouse:Destroy() + self._mouse = nil + end + + if self._mockedPlugins then + self._mockedPlugins[self] = nil + self._mockedPlugins = nil + end +end + +function MockPlugin:CreateToolbar(id) + if self._toolbars[id] then + return self._toolbars[id] + end + + local toolbar = MockPluginToolbar.new(self, id) + self._toolbars[id] = toolbar + return toolbar +end + +function MockPlugin:IsActivated() + return self._activated +end + +function MockPlugin:IsActivatedWithExclusiveMouse() + return self._activatedWithExclusiveMouse +end + +function MockPlugin:Activate(exclusiveMouse) + if self._mockedPlugins then + for mockedPlugin, _ in pairs(self._mockedPlugins) do + if mockedPlugin._activated then + mockedPlugin:Deactivate() + end + end + end + + self._activated = true + self._activatedWithExclusiveMouse = exclusiveMouse and true or false +end + +function MockPlugin:Deactivate() + if not self._activated then + return + end + self._activated = false + self._activatedWithExclusiveMouse = false + self.Deactivation:Fire() +end + +function MockPlugin:GetMouse() + return self._mouse +end + +function MockPlugin:GetSubWindow(index) + local now = tick() + local timeout = now + 1 + while not self.subWindows[index] do + wait() + if tick() > now + timeout then + error("Sub-window has not been created") + end + end + return self.subWindows[index] +end + +function MockPlugin:CreateDockWidgetPluginGui(_, ...) + local gui = createScreenGui() + table.insert(self.subWindows, gui) + return gui +end + +function MockPlugin:CreateQWidgetPluginGui(title, ...) + return self:CreateDockWidgetPluginGui(title, ...) +end + +return MockPlugin diff --git a/BuiltInPlugins/LocalizationTools/Packages/Framework/TestHelpers/Instances/MockPlugin.spec.lua b/BuiltInPlugins/LocalizationTools/Packages/Framework/TestHelpers/Instances/MockPlugin.spec.lua new file mode 100644 index 0000000000..7123f1382d --- /dev/null +++ b/BuiltInPlugins/LocalizationTools/Packages/Framework/TestHelpers/Instances/MockPlugin.spec.lua @@ -0,0 +1,78 @@ +local MockPlugin = require(script.Parent.MockPlugin) + +return function() + it("should have a mouse", function() + local plugin = MockPlugin.new() + expect(plugin:GetMouse()).to.be.ok() + end) + + it("should create toolbars", function() + local plugin = MockPlugin.new() + local toolbar = plugin:CreateToolbar("Foo") + + expect(toolbar).to.be.ok() + expect(toolbar._plugin).to.equal(plugin) + expect(toolbar._id).to.equal("Foo") + expect(toolbar.Text).to.equal("Foo") + + toolbar:Destroy() + plugin:Destroy() + end) + + describe("activation", function() + it("should work", function() + local plugin = MockPlugin.new() + expect(plugin:IsActivated()).to.equal(false) + expect(plugin:IsActivatedWithExclusiveMouse()).to.equal(false) + + plugin:Activate() + expect(plugin:IsActivated()).to.equal(true) + expect(plugin:IsActivatedWithExclusiveMouse()).to.equal(false) + + plugin:Activate(true) + expect(plugin:IsActivated()).to.equal(true) + expect(plugin:IsActivatedWithExclusiveMouse()).to.equal(true) + + plugin:Deactivate() + expect(plugin:IsActivated()).to.equal(false) + expect(plugin:IsActivatedWithExclusiveMouse()).to.equal(false) + end) + + it("should fire a signal when deactivated", function() + local signalFired = false + + local plugin = MockPlugin.new() + local con = plugin.Deactivation:Connect(function() + signalFired = true + end) + + -- Plugin starts deactivated so this should do nothing + plugin:Deactivate() + expect(signalFired).to.equal(false) + + plugin:Activate() + plugin:Deactivate() + expect(signalFired).to.equal(true) + + con:Disconnect() + end) + + it("should deactivate other plugins if mockedPlugins set is given", function() + local mockedPlugins = {} + + local p1 = MockPlugin.new("", mockedPlugins) + local p2 = MockPlugin.new("", mockedPlugins) + + expect(p1:IsActivated()).to.equal(false) + expect(p2:IsActivated()).to.equal(false) + + p1:Activate() + expect(p1:IsActivated()).to.equal(true) + expect(p2:IsActivated()).to.equal(false) + + p2:Activate() + expect(p1:IsActivated()).to.equal(false) + expect(p2:IsActivated()).to.equal(true) + end) + end) +end diff --git a/BuiltInPlugins/LocalizationTools/Packages/Framework/TestHelpers/Instances/MockPluginToolbar.lua b/BuiltInPlugins/LocalizationTools/Packages/Framework/TestHelpers/Instances/MockPluginToolbar.lua new file mode 100644 index 0000000000..6548cea079 --- /dev/null +++ b/BuiltInPlugins/LocalizationTools/Packages/Framework/TestHelpers/Instances/MockPluginToolbar.lua @@ -0,0 +1,63 @@ +local MockPluginToolbarButton = require(script.Parent.MockPluginToolbarButton) + +local MockPluginToolbar = {} +MockPluginToolbar.__index = MockPluginToolbar + +function MockPluginToolbar.new(plugin, id) + local self = { + _plugin = plugin, + _id = id, + + Name = id, + Text = id, + + _buttons = {}, + } + + setmetatable(self, MockPluginToolbar) + return self +end + +function MockPluginToolbar:Destroy() + self._plugin = nil + for _, button in pairs(self._buttons) do + button:Destroy() + end + self._buttons = {} +end + +function MockPluginToolbar:CreateButton(id, tooltip, icon, text) + local hasId = id and #id > 0 + local hasTooltip = tooltip and #tooltip > 0 + local hasIcon = icon and #icon > 0 + local hasText = text and #text > 0 + + local useLegacyBehavior = hasTooltip or hasIcon or hasText + + local finalId + local finalText + + if useLegacyBehavior then + finalId = hasId and id or tooltip + finalText = hasText and text or (hasId and id or tooltip) + else + finalId = id + finalText = id + + assert(#finalId > 0, ("Toolbar %s tried to create a button with empty id"):format(self._id)) + end + + assert(not self._buttons[finalId], ("Toolbar %s already has a button with id %s"):format(self._id, finalId)) + local button = MockPluginToolbarButton.new(self._plugin, self, finalId) + self._buttons[finalId] = button + + button.Text = finalText + if useLegacyBehavior then + button.Tooltip = tooltip or "" + button.Icon = icon or "" + end + + return button +end + +return MockPluginToolbar diff --git a/BuiltInPlugins/LocalizationTools/Packages/Framework/TestHelpers/Instances/MockPluginToolbar.spec.lua b/BuiltInPlugins/LocalizationTools/Packages/Framework/TestHelpers/Instances/MockPluginToolbar.spec.lua new file mode 100644 index 0000000000..94d7e3c353 --- /dev/null +++ b/BuiltInPlugins/LocalizationTools/Packages/Framework/TestHelpers/Instances/MockPluginToolbar.spec.lua @@ -0,0 +1,38 @@ +local MockPluginToolbar = require(script.Parent.MockPluginToolbar) + +return function() + describe("CreateButton", function() + it("should support legacy API", function() + local toolbar = MockPluginToolbar.new(nil, "") + + local button = toolbar:CreateButton("id", "tooltip", "icon", "text") + expect(button._toolbar).to.equal(toolbar) + expect(button._id).to.equal("id") + expect(button.Tooltip).to.equal("tooltip") + expect(button.Icon).to.equal("icon") + expect(button.Text).to.equal("text") + button:Destroy() + + button = toolbar:CreateButton("", "foo") + expect(button._id).to.equal("foo") + expect(button.Tooltip).to.equal("foo") + expect(button.Icon).to.equal("") + expect(button.Text).to.equal("foo") + button:Destroy() + + toolbar:Destroy() + end) + + it("should support new API", function() + local toolbar = MockPluginToolbar.new(nil, "") + local button = toolbar:CreateButton("foo_id") + expect(button._toolbar).to.equal(toolbar) + expect(button._id).to.equal("foo_id") + expect(button.Tooltip).to.equal("") + expect(button.Icon).to.equal("") + expect(button.Text).to.equal("foo_id") + button:Destroy() + toolbar:Destroy() + end) + end) +end diff --git a/BuiltInPlugins/LocalizationTools/Packages/Framework/TestHelpers/Instances/MockPluginToolbarButton.lua b/BuiltInPlugins/LocalizationTools/Packages/Framework/TestHelpers/Instances/MockPluginToolbarButton.lua new file mode 100644 index 0000000000..29c148490a --- /dev/null +++ b/BuiltInPlugins/LocalizationTools/Packages/Framework/TestHelpers/Instances/MockPluginToolbarButton.lua @@ -0,0 +1,42 @@ +local DevFrameworkRoot = script.Parent.Parent.Parent +local Signal = require(DevFrameworkRoot.Util.Signal) + +local MockPluginToolbarButton = {} +MockPluginToolbarButton.__index = MockPluginToolbarButton + +function MockPluginToolbarButton.new(plugin, toolbar, id) + local self = { + _plugin = plugin, + _toolbar = toolbar, + _id = id, + + Name = id, + Tooltip = "", + Icon = "", + Text = "", + Enabled = true, + Active = false, + ClickableWhenViewportHidden = true, + + Click = Signal.new(), + } + setmetatable(self, MockPluginToolbarButton) + + return self +end + +function MockPluginToolbarButton:Destroy() + self._toolbar = nil + self._plugin = nil +end + +function MockPluginToolbarButton:SetActive(newActive) + if self._plugin and self.Active and not newActive then + self._plugin:Deactivate() + end + + self.Active = newActive +end + +return MockPluginToolbarButton + diff --git a/BuiltInPlugins/LocalizationTools/Packages/Framework/TestHelpers/Instances/MockPluginToolbarButton.spec.lua b/BuiltInPlugins/LocalizationTools/Packages/Framework/TestHelpers/Instances/MockPluginToolbarButton.spec.lua new file mode 100644 index 0000000000..806bd9ab06 --- /dev/null +++ b/BuiltInPlugins/LocalizationTools/Packages/Framework/TestHelpers/Instances/MockPluginToolbarButton.spec.lua @@ -0,0 +1,22 @@ +local MockPlugin = require(script.Parent.MockPlugin) + +return function() + it("should deactivate the parent plugin when deactivating", function() + local plugin = MockPlugin.new() + local toolbar = plugin:CreateToolbar("") + local button = toolbar:CreateButton("foo") + + plugin:Activate() + -- Only catch when going from active -> not active + button:SetActive(false) + expect(plugin:IsActivated()).to.equal(true) + + button:SetActive(true) + expect(plugin:IsActivated()).to.equal(true) + + button:SetActive(false) + expect(plugin:IsActivated()).to.equal(false) + + plugin:Destroy() + end) +end diff --git a/BuiltInPlugins/LocalizationTools/Packages/Framework/TestHelpers/Instances/MockSelectionService.lua b/BuiltInPlugins/LocalizationTools/Packages/Framework/TestHelpers/Instances/MockSelectionService.lua new file mode 100644 index 0000000000..94aafdb058 --- /dev/null +++ b/BuiltInPlugins/LocalizationTools/Packages/Framework/TestHelpers/Instances/MockSelectionService.lua @@ -0,0 +1,28 @@ +local DevFrameworkRoot = script.Parent.Parent.Parent +local Signal = require(DevFrameworkRoot.Util.Signal) + +local MockSelectionService = {} +MockSelectionService.__index = MockSelectionService + +function MockSelectionService.new() + local self = setmetatable({ + _selection = {}, + SelectionChanged = Signal.new(), + }, MockSelectionService) + + return self +end + +function MockSelectionService:Destroy() +end + +function MockSelectionService:Get() + return self._selection +end + +function MockSelectionService:Set(selection) + self._selection = selection + self.SelectionChanged:Fire() +end + +return MockSelectionService diff --git a/BuiltInPlugins/LocalizationTools/Packages/Framework/TestHelpers/Instances/MockStudioService.lua b/BuiltInPlugins/LocalizationTools/Packages/Framework/TestHelpers/Instances/MockStudioService.lua new file mode 100644 index 0000000000..1c0b6ae981 --- /dev/null +++ b/BuiltInPlugins/LocalizationTools/Packages/Framework/TestHelpers/Instances/MockStudioService.lua @@ -0,0 +1,19 @@ +local MockStudioService = {} +MockStudioService.__index = MockStudioService + +function MockStudioService.new() + local self = setmetatable({ + _localUserId = 0, + }, MockStudioService) + + return self +end + +function MockStudioService:Destroy() +end + +function MockStudioService:GetUserId() + return self._localUserId +end + +return MockStudioService diff --git a/BuiltInPlugins/LocalizationTools/Packages/Framework/TestHelpers/makeSettableValue.lua b/BuiltInPlugins/LocalizationTools/Packages/Framework/TestHelpers/makeSettableValue.lua new file mode 100644 index 0000000000..cb5210e281 --- /dev/null +++ b/BuiltInPlugins/LocalizationTools/Packages/Framework/TestHelpers/makeSettableValue.lua @@ -0,0 +1,21 @@ +local DevFrameworkRoot = script.Parent.Parent +local Signal = require(DevFrameworkRoot.Util.Signal) + +--[[ +Wrapper to encapsulate a value with get/set methods and a changed signal +]] +return function(initialValue) + local value = initialValue + local changed = Signal.new() + + return { + get = function() + return value + end, + set = function(newValue) + value = newValue + changed:Fire() + end, + changed = changed, + } +end diff --git a/BuiltInPlugins/LocalizationTools/Packages/Framework/TestHelpers/makeSettableValue.spec.lua b/BuiltInPlugins/LocalizationTools/Packages/Framework/TestHelpers/makeSettableValue.spec.lua new file mode 100644 index 0000000000..4db840c64b --- /dev/null +++ b/BuiltInPlugins/LocalizationTools/Packages/Framework/TestHelpers/makeSettableValue.spec.lua @@ -0,0 +1,40 @@ +local makeSettableValue = require(script.Parent.makeSettableValue) + +return function() + it("should store the initial value", function() + local v = makeSettableValue("foo") + expect(v.get()).to.equal("foo") + + v = makeSettableValue(12345) + expect(v.get()).to.equal(12345) + end) + + it("should update the stored value", function() + local v = makeSettableValue("foo") + + v.set("bar") + expect(v.get()).to.equal("bar") + + v.set(13579) + expect(v.get()).to.equal(13579) + end) + + it("should fire a changed signal", function() + local signalFired = false + + local v = makeSettableValue("foo") + local con = v.changed:Connect(function() + signalFired = true + end) + + expect(signalFired).to.equal(false) + v.set("bar") + expect(signalFired).to.equal(true) + + signalFired = false + v.set("baz") + expect(signalFired).to.equal(true) + + con:Disconnect() + end) +end diff --git a/BuiltInPlugins/LocalizationTools/Packages/Framework/TestHelpers/provideMockContext.lua b/BuiltInPlugins/LocalizationTools/Packages/Framework/TestHelpers/provideMockContext.lua index 25a3151d5e..10633a0dc7 100644 --- a/BuiltInPlugins/LocalizationTools/Packages/Framework/TestHelpers/provideMockContext.lua +++ b/BuiltInPlugins/LocalizationTools/Packages/Framework/TestHelpers/provideMockContext.lua @@ -27,20 +27,28 @@ local DevFrameworkRoot = script.Parent.Parent local ContextServices = require(DevFrameworkRoot.ContextServices) local StudioFrameworkStyles = require(DevFrameworkRoot.StudioUI).StudioFrameworkStyles -local mockPlugin = require(DevFrameworkRoot.TestHelpers.Services.mockPlugin) +local MockPlugin = require(DevFrameworkRoot.TestHelpers.Instances.MockPlugin) local Rodux = require(DevFrameworkRoot.Parent.Rodux) - +local StudioTheme = require(DevFrameworkRoot.Style.Themes.StudioTheme) +local Util = require(DevFrameworkRoot.Util) +local FlagsList = Util.Flags.new({ + FFlagRefactorDevFrameworkTheme = {"RefactorDevFrameworkTheme"}, +}) -- contextItemsList : (table, optional) a list of ContextItems to include in the stack. Will override any duplicates. -- children : (table, required) a map of children like you would pass into Roact.createElement's children return function(contextItemsList, children) if contextItemsList then assert(type(contextItemsList) == "table", "Expected contextItemsList to be a table.") - assert(type(next(contextItemsList)) == "number" or type(next(contextItemsList)) == "nil", + assert(type((next(contextItemsList))) == "number" or type((next(contextItemsList))) == "nil", "Expected contextItemsList to be an array.") end assert(type(children) == "table", "Expected children to be a table.") - assert(type(next(children)) == "string", "Expected children to be a map of components.") + assert(type((next(children))) == "string", "Expected children to be a map of components.") + + -- Multiple items use the plugin in some way + -- Create 1 mock plugin and use it in each + local mockPlugin = MockPlugin.new() -- create a list of default mocks local contextItems = {} @@ -55,9 +63,7 @@ return function(contextItemsList, children) table.insert(contextItems, localization) -- Mouse - local mouse = ContextServices.Mouse.new({ - Icon = "rbxasset://SystemCursors/Arrow", - }) + local mouse = ContextServices.Mouse.new(mockPlugin:GetMouse()) table.insert(contextItems, mouse) -- Navigation @@ -69,11 +75,11 @@ return function(contextItemsList, children) table.insert(contextItems, analytics) -- Plugin - local plugin = ContextServices.Plugin.new(mockPlugin.new()) + local plugin = ContextServices.Plugin.new(mockPlugin) table.insert(contextItems, plugin) -- PluginActions - local pluginActions = ContextServices.PluginActions.new(mockPlugin.new(), {}) + local pluginActions = ContextServices.PluginActions.new(mockPlugin, {}) table.insert(contextItems, pluginActions) -- Store @@ -83,19 +89,24 @@ return function(contextItemsList, children) table.insert(contextItems, store) -- Theme - local theme = ContextServices.Theme.mock(function(theme, getColor) - return { - Framework = StudioFrameworkStyles.new(theme, getColor), - } - end, function() - return { - Name = Enum.UITheme.Light.Name, - - GetColor = function(_, _) - return Color3.new() - end, - } - end) + local theme + if FlagsList:get("FFlagRefactorDevFrameworkTheme") then + theme = StudioTheme.mock() + else + theme = ContextServices.Theme.mock(function(theme, getColor) + return { + Framework = StudioFrameworkStyles.new(theme, getColor), + } + end, function() + return { + Name = "Light", + + GetColor = function(_, _) + return Color3.new() + end, + } + end) + end table.insert(contextItems, theme) @@ -109,4 +120,4 @@ return function(contextItemsList, children) -- render the components inside the provided context stack return ContextServices.provide(contextItems, children) -end \ No newline at end of file +end diff --git a/BuiltInPlugins/LocalizationTools/Packages/Framework/TestHelpers/provideMockContext.spec.lua b/BuiltInPlugins/LocalizationTools/Packages/Framework/TestHelpers/provideMockContext.spec.lua index af4c975042..d2500676da 100644 --- a/BuiltInPlugins/LocalizationTools/Packages/Framework/TestHelpers/provideMockContext.spec.lua +++ b/BuiltInPlugins/LocalizationTools/Packages/Framework/TestHelpers/provideMockContext.spec.lua @@ -4,7 +4,10 @@ local provideMockContext = require(script.Parent.provideMockContext) local ContextServices = require(Framework.ContextServices) local Provider = require(Framework.ContextServices.Provider) local ContextItem = require(Framework.ContextServices.ContextItem) - +local Util = require(Framework.Util) +local FlagsList = Util.Flags.new({ + FFlagRefactorDevFrameworkTheme = {"RefactorDevFrameworkTheme"}, +}) return function() it("should work without arguments", function() local element = provideMockContext({}, { @@ -37,10 +40,15 @@ return function() function testComponent:render() wasRendered = true - local theme = self.props.Theme:get("Framework") local localization = self.props.Localization local plugin = self.props.Plugin:get() local mouse = self.props.Mouse:get() + local theme + if FlagsList:get("FFlagRefactorDevFrameworkTheme") then + theme = self.props.Stylizer + else + theme = self.props.Theme:get("Framework") + end expect(theme).to.never.equal(nil) expect(localization).to.never.equal(nil) @@ -51,7 +59,8 @@ return function() end ContextServices.mapToProps(testComponent, { - Theme = ContextServices.Theme, + Stylizer = FlagsList:get("FFlagRefactorDevFrameworkTheme") and ContextServices.Stylizer or nil, + Theme = (not FlagsList:get("FFlagRefactorDevFrameworkTheme")) and ContextServices.Theme or nil, Localization = ContextServices.Localization, Mouse = ContextServices.Mouse, Plugin = ContextServices.Plugin, diff --git a/BuiltInPlugins/LocalizationTools/Packages/Framework/TestHelpers/setEquals.lua b/BuiltInPlugins/LocalizationTools/Packages/Framework/TestHelpers/setEquals.lua new file mode 100644 index 0000000000..31e25dbc20 --- /dev/null +++ b/BuiltInPlugins/LocalizationTools/Packages/Framework/TestHelpers/setEquals.lua @@ -0,0 +1,16 @@ +-- Helper to verify that 2 sets have exactly the same keys +return function(result, expected) + for k in pairs(result) do + if not expected[k] then + return false + end + end + + for k in pairs(expected) do + if not result[k] then + return false + end + end + + return true +end diff --git a/BuiltInPlugins/LocalizationTools/Packages/Framework/TestHelpers/testImmutability.lua b/BuiltInPlugins/LocalizationTools/Packages/Framework/TestHelpers/testImmutability.lua new file mode 100644 index 0000000000..c56fb0bf24 --- /dev/null +++ b/BuiltInPlugins/LocalizationTools/Packages/Framework/TestHelpers/testImmutability.lua @@ -0,0 +1,74 @@ +--[[ + A function that every reducer should use to check that valid actions do not mutate the previous state. + + This function takes a snapshot of the state before and after an action is applied. + Then it checks if any fields from the original data have been mutated. + + The final output of the reducer is ultimately irrelevant for this test. + All that matters is that the original data is preserved and unmodified. +]] +local DevFrameworkRoot = script.Parent.Parent +local deepJoin = require(DevFrameworkRoot.Util.deepJoin) + +local function allFieldsAreUnchanged(tableA, tableB) + -- if there's some mistake, escape + if type(tableA) ~= "table" or type(tableB) ~= "table" then + if not tableA then + tableA = "nil" + end + if not tableB then + tableB = "nil" + end + error(string.format("Expected to compare two tables, got %s and %s", tostring(tableA), tostring(tableB))) + end + + -- count all the keys in B + local expectedNumKeysB = 0 + for _, _ in pairs(tableB) do + expectedNumKeysB = expectedNumKeysB + 1 + end + + -- check that all keys are equal in type and value + local expectedNumKeysA = 0 + for key, vA in pairs(tableA) do + expectedNumKeysA = expectedNumKeysA + 1 + local vB = tableB[key] + + if type(vA) == "table" then + -- if there's a child, verify that all its values are unmutated + allFieldsAreUnchanged(vA, vB) + else + if vA ~= vB then + error(string.format("the field \"%s\" no longer matches", key)) + end + end + end + + -- make sure that no keys haven't gone missing + if expectedNumKeysA ~= expectedNumKeysB then + return error(string.format("Number of keys mismatch : %d vs %d", expectedNumKeysA, expectedNumKeysB)) + end + + -- if we've made it here, these tables are separate yet equal matches + return true +end + + +return function(reducer, action, previousState) + assert(type(reducer) == "function", "Expected a reducer to test") + assert(type(action) == "table", "Expected an action to test") + if previousState ~= nil then + assert(type(previousState) == "table", "Expected previousState to be a table") + end + + -- copy the originalState + local originalState = reducer(previousState, { type = "__nil__" }) + local originalStateCopy = deepJoin(originalState, {}) + assert(allFieldsAreUnchanged(originalState, originalStateCopy), "deepJoin mutates fields") + + -- run the state through a reducer, disregard the output + reducer(originalState, action) + + -- check that originalState still matches the originalStateCopy + return allFieldsAreUnchanged(originalState, originalStateCopy) +end diff --git a/BuiltInPlugins/LocalizationTools/Packages/Framework/TestHelpers/testImmutability.spec.lua b/BuiltInPlugins/LocalizationTools/Packages/Framework/TestHelpers/testImmutability.spec.lua new file mode 100644 index 0000000000..e3eee1ea0e --- /dev/null +++ b/BuiltInPlugins/LocalizationTools/Packages/Framework/TestHelpers/testImmutability.spec.lua @@ -0,0 +1,169 @@ +local DevFrameworkRoot = script.Parent.Parent + +local Rodux = require(DevFrameworkRoot.Parent.Rodux) +local Cryo = require(DevFrameworkRoot.Util.Cryo) + +local Action = require(DevFrameworkRoot.Util.Action) + +local testImmutability = require(script.Parent.testImmutability) + +return function() + it("should error on invalid input", function() + -- expected fields + -- reducer : (Rodux reducer) + -- action : (Action) + + local function createTestReducer() + local defaultState = { + foo = "bar", + } + local testReducer = Rodux.createReducer(defaultState, { + emptyAction = function(state, _) + return Cryo.Dictionary.join(state, {}) + end, + }) + + return testReducer + end + local emptyAction = Action("emptyAction", function() + return {} + end) + + -- if everything is fine, return true + expect(testImmutability(createTestReducer(), emptyAction)).to.equal(true) + + -- invalid reducer + expect(function() + testImmutability("", emptyAction) + end).to.throw() + + -- invalid action + expect(function() + testImmutability(createTestReducer(), "hello") + end).to.throw() + end) + + it("should return true if the originalState is unchanged by the supplied action", function() + -- create a proper reducer + local r = Rodux.createReducer({ value = "foo" }, { + setValue = function(state, action) + local newValue = action.value + return Cryo.Dictionary.join(state, { + value = newValue + }) + end + }) + local setValueAction = Action("setValue", function(v) + return { value = v } + end) + local emptyAction = Action("emptyAction", function() + return {} + end) + + -- get the default state + local defaultState = r(nil, emptyAction) + expect(defaultState.value).to.equal("foo") + + -- show that this action can modify this state + local newState = r(nil, setValueAction("bar")) + expect(newState.value).to.equal("bar") + + -- even if the state can be modified by the action, + -- the original state table should not reflect those changes. + expect(testImmutability(r, setValueAction("test"))).to.equal(true) + end) + + it("should throw an error if the originalState has been modified in any way", function() + local r = Rodux.createReducer({ value = "foo" }, { + setValue = function(state, action) + -- erroneously modify the old state + local newValue = action.value + state.value = newValue + return state + end + }) + local setValueAction = Action("setValue", function(v) + return { value = v } + end) + + expect(function() + testImmutability(r, setValueAction("test")) + end).to.throw() + end) + + it("should catch changes in nested tables", function() + local r = Rodux.createReducer({ + value = { + children = { "foo", "bar", "cat" }, + }, + }, { + setChildren = function(state, action) + -- reuse the table from the old state + local newValue = {} + newValue.value = {} + newValue.value.children = state.value.children + + -- erroneously mutate the old data + newValue.value.children[1] = "fooo" + return newValue + end + }) + local setChildrenAction = Action("setChildren", function() + return {} + end) + + expect(function() + testImmutability(r, setChildrenAction("test")) + end).to.throw() + end) + + it("should return true when the result is two empty tables", function() + local emptyAction = Action("emptyAction", function() return {} end) + local testReducer = Rodux.createReducer({ + tA = {}, + tB = {}, + }, { + emptyAction = function(state, action) + return Cryo.Dictionary.join(state, {}) + end, + }) + + expect(testImmutability(testReducer, emptyAction)).to.equal(true) + end) + + it("should allow you pass a table in as a previous state", function() + -- create a state that holds non-default information + local previousState = { + tA = { + name = "test" + } + } + + -- create a reducer whose default state lacks enough information + -- to work on more advanced actions. Let's assume another action sets + -- the field 'tA', and changeNameAction works with its contents. + local testReducer = Rodux.createReducer({}, { + changeNameAction = function(state, action) + local target = action.target + local newName = action.name + + return Cryo.Dictionary.join(state, { + [target] = Cryo.Dictionary.join(state[target], { + name = newName + }), + }) + end, + }) + + -- create an action that extends work done by other actions + local changeNameAction = Action("changeNameAction", function(target, name) + return { + target = target, + name = name + } + end) + + expect(testImmutability(testReducer, changeNameAction, + previousState)).to.equal(true) + end) +end diff --git a/BuiltInPlugins/LocalizationTools/Packages/Framework/UI/Box.lua b/BuiltInPlugins/LocalizationTools/Packages/Framework/UI/Box.lua index e609f28176..1e13b22d73 100644 --- a/BuiltInPlugins/LocalizationTools/Packages/Framework/UI/Box.lua +++ b/BuiltInPlugins/LocalizationTools/Packages/Framework/UI/Box.lua @@ -1,10 +1,9 @@ --[[ A simple, solid color Decoration. - Required Props: - Theme Theme: A Theme ContextItem, which is provided via mapToProps. - Optional Props: + Theme Theme: A Theme ContextItem, which is provided via mapToProps. + Stylizer Stylizer: A Stylizer ContextItem, which is provided via mapToProps. Style Style: The style with which to render this component. StyleModifier StyleModifier: The StyleModifier index into Style. @@ -14,11 +13,15 @@ Color3 BorderColor: The color of the border. number BorderSize: the size of the border. ]] - local Framework = script.Parent.Parent local Roact = require(Framework.Parent.Roact) local ContextServices = require(Framework.ContextServices) -local Typecheck = require(Framework.Util).Typecheck +local Util = require(Framework.Util) +local Typecheck = Util.Typecheck + +local FlagsList = Util.Flags.new({ + FFlagRefactorDevFrameworkTheme = {"RefactorDevFrameworkTheme"}, +}) local Box = Roact.PureComponent:extend("Box") Typecheck.wrap(Box, script) @@ -26,7 +29,12 @@ Typecheck.wrap(Box, script) function Box:render() local props = self.props local theme = props.Theme - local style = theme:getStyle("Framework", self) + local style + if FlagsList:get("FFlagRefactorDevFrameworkTheme") then + style = props.Stylizer + else + style = theme:getStyle("Framework", self) + end local color = style.Color local transparency = style.Transparency @@ -43,7 +51,8 @@ function Box:render() end ContextServices.mapToProps(Box, { - Theme = ContextServices.Theme, + Stylizer = FlagsList:get("FFlagRefactorDevFrameworkTheme") and ContextServices.Stylizer or nil, + Theme = (not FlagsList:get("FFlagRefactorDevFrameworkTheme")) and ContextServices.Theme or nil, }) return Box diff --git a/BuiltInPlugins/LocalizationTools/Packages/Framework/UI/Box/style.lua b/BuiltInPlugins/LocalizationTools/Packages/Framework/UI/Box/style.lua index 295019b26c..2f3b83232e 100644 --- a/BuiltInPlugins/LocalizationTools/Packages/Framework/UI/Box/style.lua +++ b/BuiltInPlugins/LocalizationTools/Packages/Framework/UI/Box/style.lua @@ -1,20 +1,33 @@ local Framework = script.Parent.Parent.Parent -local Util = require(Framework.Util) -local Style = Util.Style +local StyleKey = require(Framework.Style.StyleKey) local StudioFrameworkStyles = Framework.StudioUI.StudioFrameworkStyles local Common = require(StudioFrameworkStyles.Common) -return function(theme, getColor) - local common = Common(theme, getColor) +local Util = require(Framework.Util) +local Style = Util.Style +local FlagsList = Util.Flags.new({ + FFlagRefactorDevFrameworkTheme = {"RefactorDevFrameworkTheme"}, +}) - local Default = Style.extend(common.Background, { +if FlagsList:get("FFlagRefactorDevFrameworkTheme") then + return { + Color = StyleKey.MainBackground, Transparency = 0, BorderSize = 0, - }) - - return { - Default = Default, } -end +else + return function(theme, getColor) + local common = Common(theme, getColor) + + local Default = Style.extend(common.Background, { + Transparency = 0, + BorderSize = 0, + }) + + return { + Default = Default, + } + end +end \ No newline at end of file diff --git a/BuiltInPlugins/LocalizationTools/Packages/Framework/UI/Box/test.spec.lua b/BuiltInPlugins/LocalizationTools/Packages/Framework/UI/Box/test.spec.lua index cbcaf15f51..edc3b7c984 100644 --- a/BuiltInPlugins/LocalizationTools/Packages/Framework/UI/Box/test.spec.lua +++ b/BuiltInPlugins/LocalizationTools/Packages/Framework/UI/Box/test.spec.lua @@ -6,16 +6,35 @@ return function() local provide = ContextServices.provide local FrameworkStyles = require(Framework.UI.FrameworkStyles) local Box = require(script.Parent) + local TestHelpers = require(Framework.TestHelpers) + + local StudioTheme = require(Framework.Style.Themes.StudioTheme) + + local Util = require(Framework.Util) + local FlagsList = Util.Flags.new({ + FFlagRefactorDevFrameworkTheme = {"RefactorDevFrameworkTheme"}, + }) local function createTestBoxDecoration() - local theme = Theme.new(function() - return { - Framework = FrameworkStyles.new(), - } - end) - return provide({theme}, { - BoxDecoration = Roact.createElement(Box), - }) + if FlagsList:get("FFlagRefactorDevFrameworkTheme") then + return TestHelpers.provideMockContext(nil, { + BoxDecoration = Roact.createElement(Box), + }) + else + local theme + if FlagsList:get("FFlagRefactorDevFrameworkTheme") then + theme = StudioTheme.mock() + else + theme = Theme.new(function() + return { + Framework = FrameworkStyles.new(), + } + end) + end + return provide({theme}, { + BoxDecoration = Roact.createElement(Box), + }) + end end it("should create and destroy without errors", function() diff --git a/BuiltInPlugins/LocalizationTools/Packages/Framework/UI/BulletList.lua b/BuiltInPlugins/LocalizationTools/Packages/Framework/UI/BulletList.lua index 41fca31eb2..72cb7b973e 100644 --- a/BuiltInPlugins/LocalizationTools/Packages/Framework/UI/BulletList.lua +++ b/BuiltInPlugins/LocalizationTools/Packages/Framework/UI/BulletList.lua @@ -2,16 +2,16 @@ An array of strings and/or elements displayed as a bulleted list. Required Props: - Theme Theme: A Theme ContextItem, which is provided via mapToProps. array[any] Items: The item to display after each bullet point. Should be an array of strings and/or elements. Strings will be measured to determine the item size. Elements must specify their own size. Optional Props: + Theme Theme: A Theme ContextItem, which is provided via mapToProps. number LayoutOrder: Order in which the element is placed. - Style Style: The style with which to render this component. StyleModifier StyleModifier: The StyleModifier index into Style. boolean TextWrapped: Sets text wrapped. boolean TextTruncate: Sets text truncated. + Stylizer Stylizer: A Stylizer ContextItem, which is provided via mapToProps. Style Values: Enum.Font Font: The font used to render the text. @@ -22,15 +22,19 @@ Color3 TextColor: The color of the text. number TextSize: The size of the text. ]] - local TextService = game:GetService("TextService") local Framework = script.Parent.Parent local Roact = require(Framework.Parent.Roact) local Cryo = require(Framework.Parent.Cryo) local ContextServices = require(Framework.ContextServices) +local Util = require(Framework.Util) local t = require(Framework.Util.Typecheck.t) -local Typecheck = require(Framework.Util).Typecheck +local Typecheck = Util.Typecheck + +local FlagsList = Util.Flags.new({ + FFlagRefactorDevFrameworkTheme = {"RefactorDevFrameworkTheme"}, +}) local BulletList = Roact.PureComponent:extend("BulletList") Typecheck.wrap(BulletList, script) @@ -48,7 +52,13 @@ function BulletList:init() local items = props.Items local theme = props.Theme - local style = theme:getStyle("Framework", self) + local style + if FlagsList:get("FFlagRefactorDevFrameworkTheme") then + style = props.Stylizer + else + style = theme:getStyle("Framework", self) + end + local textSize = style.TextSize local font = style.Font local padding = style.Padding @@ -98,8 +108,12 @@ function BulletList:didUpdate() end function BulletList:calculateItemOffset() - local theme = self.props.Theme - local style = theme:getStyle("Framework", self) + local style + if FlagsList:get("FFlagRefactorDevFrameworkTheme") then + style = self.props.Stylizer + else + style = self.props.Theme:getStyle("Framework", self) + end local itemOffset = style.ItemOffset local markerSize = style.MarkerSize @@ -115,7 +129,12 @@ function BulletList:render() local textTruncate = props.TextTruncate local theme = props.Theme - local style = theme:getStyle("Framework", self) + local style + if FlagsList:get("FFlagRefactorDevFrameworkTheme") then + style = props.Stylizer + else + style = theme:getStyle("Framework", self) + end local font = style.Font local markerImage = style.MarkerImage local markerSize = style.MarkerSize @@ -209,7 +228,8 @@ function BulletList:render() end ContextServices.mapToProps(BulletList, { - Theme = ContextServices.Theme, + Stylizer = FlagsList:get("FFlagRefactorDevFrameworkTheme") and ContextServices.Stylizer or nil, + Theme = (not FlagsList:get("FFlagRefactorDevFrameworkTheme")) and ContextServices.Theme or nil, }) return BulletList diff --git a/BuiltInPlugins/LocalizationTools/Packages/Framework/UI/BulletList/style.lua b/BuiltInPlugins/LocalizationTools/Packages/Framework/UI/BulletList/style.lua index 931ce726e0..c1b02cd2cb 100644 --- a/BuiltInPlugins/LocalizationTools/Packages/Framework/UI/BulletList/style.lua +++ b/BuiltInPlugins/LocalizationTools/Packages/Framework/UI/BulletList/style.lua @@ -6,17 +6,30 @@ local Style = Util.Style local StudioFrameworkStyles = Framework.StudioUI.StudioFrameworkStyles local Common = require(StudioFrameworkStyles.Common) -return function(theme, getColor) - local common = Common(theme, getColor) +local FlagsList = Util.Flags.new({ + FFlagRefactorDevFrameworkTheme = {"RefactorDevFrameworkTheme"}, +}) - local Default = Style.extend(common.MainText, { +if FlagsList:get("FFlagRefactorDevFrameworkTheme") then + return { ItemOffset = 12, MarkerImage = "rbxasset://textures/StudioSharedUI/dot.png", MarkerSize = 4, Padding = 6, - }) - - return { - Default = Default, } +else + return function(theme, getColor) + local common = Common(theme, getColor) + + local Default = Style.extend(common.MainText, { + ItemOffset = 12, + MarkerImage = "rbxasset://textures/StudioSharedUI/dot.png", + MarkerSize = 4, + Padding = 6, + }) + + return { + Default = Default, + } + end end diff --git a/BuiltInPlugins/LocalizationTools/Packages/Framework/UI/BulletList/test.spec.lua b/BuiltInPlugins/LocalizationTools/Packages/Framework/UI/BulletList/test.spec.lua index 9e8c93c941..c8d912bf3a 100644 --- a/BuiltInPlugins/LocalizationTools/Packages/Framework/UI/BulletList/test.spec.lua +++ b/BuiltInPlugins/LocalizationTools/Packages/Framework/UI/BulletList/test.spec.lua @@ -31,7 +31,7 @@ return function() Roact.update(instance, createTestBulletList({ Items = {"one", "two", "three", "four"} - }, container)) + })) local size2 = container:FindFirstChild("BulletList", true).Size @@ -51,7 +51,7 @@ return function() Roact.update(instance, createTestBulletList({ Items = {"one", "two"} - }, container)) + })) local size2 = container:FindFirstChild("BulletList", true).Size @@ -86,4 +86,4 @@ return function() Roact.unmount(instance) end) end) -end \ No newline at end of file +end diff --git a/BuiltInPlugins/LocalizationTools/Packages/Framework/UI/Button.lua b/BuiltInPlugins/LocalizationTools/Packages/Framework/UI/Button.lua index c99a7396cc..13a5c5fcf2 100644 --- a/BuiltInPlugins/LocalizationTools/Packages/Framework/UI/Button.lua +++ b/BuiltInPlugins/LocalizationTools/Packages/Framework/UI/Button.lua @@ -4,7 +4,6 @@ Required Props: callback OnClick: A callback for when the user clicks this button. - Theme Theme: A Theme ContextItem, which is provided via mapToProps. Optional Props: string Text: The text to display in this button. @@ -15,6 +14,8 @@ Vector2 AnchorPoint: The pivot point of this component's Position prop. number ZIndex: The render index of this component. number LayoutOrder: The layout order of this component in a list. + Stylizer Stylizer: A Stylizer ContextItem, which is provided via mapToProps. + Theme Theme: A Theme ContextItem, which is provided via mapToProps. Style Values: Component Background: The background to render for this Button. @@ -39,6 +40,10 @@ local Util = require(Framework.Util) local StyleModifier = Util.StyleModifier local Typecheck = Util.Typecheck +local FlagsList = Util.Flags.new({ + FFlagRefactorDevFrameworkTheme = {"RefactorDevFrameworkTheme"}, +}) + local Button = Roact.PureComponent:extend("Button") Typecheck.wrap(Button, script) @@ -66,7 +71,12 @@ function Button:render() local theme = props.Theme local styleModifier = props.StyleModifier or state.StyleModifier - local style = theme:getStyle("Framework", self) + local style + if FlagsList:get("FFlagRefactorDevFrameworkTheme") then + style = props.Stylizer + else + style = theme:getStyle("Framework", self) + end local background = style.Background local backgroundStyle = style.BackgroundStyle local foreground = style.Foreground @@ -124,7 +134,8 @@ function Button:render() end ContextServices.mapToProps(Button, { - Theme = ContextServices.Theme, + Stylizer = FlagsList:get("FFlagRefactorDevFrameworkTheme") and ContextServices.Stylizer or nil, + Theme = (not FlagsList:get("FFlagRefactorDevFrameworkTheme")) and ContextServices.Theme or nil, }) return Button diff --git a/BuiltInPlugins/LocalizationTools/Packages/Framework/UI/Button/example.lua b/BuiltInPlugins/LocalizationTools/Packages/Framework/UI/Button/example.lua index a59c7667c8..e077e09ef3 100644 --- a/BuiltInPlugins/LocalizationTools/Packages/Framework/UI/Button/example.lua +++ b/BuiltInPlugins/LocalizationTools/Packages/Framework/UI/Button/example.lua @@ -17,58 +17,99 @@ return function(plugin) local HoverArea = UI.HoverArea local Util = require(Framework.Util) + local Cryo = Util.Cryo local StyleTable = Util.StyleTable local Style = Util.Style local StyleModifier = Util.StyleModifier + local FlagsList = Util.Flags.new({ + FFlagRefactorDevFrameworkTheme = {"RefactorDevFrameworkTheme"}, + }) + + local FrameworkStyle = Framework.Style + local ui = require(FrameworkStyle).ComponentSymbols + local StudioTheme = require(FrameworkStyle.Themes.StudioTheme) + local BaseTheme = require(FrameworkStyle.Themes.BaseTheme) + local StyleKey = require(FrameworkStyle.StyleKey) local pluginItem = Plugin.new(plugin) local mouse = Mouse.new(plugin:GetMouse()) - local theme = Theme.new(function(theme, getColor) - local studioStyles = StudioFrameworkStyles.new(theme, getColor) + local theme + if FlagsList:get("FFlagRefactorDevFrameworkTheme") then + theme = StudioTheme.new() + theme:extend({ + [ui.Button] = Cryo.Dictionary.join(BaseTheme[ui.Button], { + ["&Close"] = { + Foreground = Decoration.Image, + ForegroundStyle = { + Image = "rbxasset://textures/ui/CloseButton.png", + }, + [StyleModifier.Hover] = { + ForegroundStyle = { + Image = "rbxasset://textures/ui/CloseButton_dn.png", + }, + }, + }, + }), + + [ui.Image] = Cryo.Dictionary.join(BaseTheme[ui.Image], { + ["&Settings"] = { + Image = "rbxasset://textures/AnimationEditor/btn_manage.png", + Color = StyleKey.MainText, + }, - local button = StyleTable.new("Button", function() - -- Defining a new button style that uses images - local Close = Style.new({ - Foreground = Decoration.Image, - ForegroundStyle = { - Image = "rbxasset://textures/ui/CloseButton.png", + ["&SettingsPrimary"] = { + Color = StyleKey.DialogMainButtonText, }, - [StyleModifier.Hover] = { + }), + }) + else + theme = Theme.new(function(theme, getColor) + local studioStyles = StudioFrameworkStyles.new(theme, getColor) + + local button = StyleTable.new("Button", function() + -- Defining a new button style that uses images + local Close = Style.new({ + Foreground = Decoration.Image, ForegroundStyle = { - Image = "rbxasset://textures/ui/CloseButton_dn.png", + Image = "rbxasset://textures/ui/CloseButton.png", }, - }, - }) + [StyleModifier.Hover] = { + ForegroundStyle = { + Image = "rbxasset://textures/ui/CloseButton_dn.png", + }, + }, + }) - return { - Close = Close, - } - end) + return { + Close = Close, + } + end) - local image = StyleTable.new("Image", function() - local Settings = Style.extend(studioStyles.Image.Default, { - Image = "rbxasset://textures/AnimationEditor/btn_manage.png", - Color = theme:GetColor("MainText"), - }) + local image = StyleTable.new("Image", function() + local Settings = Style.extend(studioStyles.Image.Default, { + Image = "rbxasset://textures/AnimationEditor/btn_manage.png", + Color = theme:GetColor("MainText"), + }) - local SettingsPrimary = Style.extend(Settings, { - Color = theme:GetColor("DialogMainButtonText"), - }) + local SettingsPrimary = Style.extend(Settings, { + Color = theme:GetColor("DialogMainButtonText"), + }) + + return { + Settings = Settings, + SettingsPrimary = SettingsPrimary, + } + end) return { - Settings = Settings, - SettingsPrimary = SettingsPrimary, + Framework = StyleTable.extend(studioStyles, { + Button = button, + Image = image, + }) } end) - - return { - Framework = StyleTable.extend(studioStyles, { - Button = button, - Image = image, - }) - } - end) + end -- Mount and display a dialog local ExampleButtons = Roact.PureComponent:extend("ExampleButtons") diff --git a/BuiltInPlugins/LocalizationTools/Packages/Framework/UI/Button/style.lua b/BuiltInPlugins/LocalizationTools/Packages/Framework/UI/Button/style.lua index d957ec285b..b371b39e99 100644 --- a/BuiltInPlugins/LocalizationTools/Packages/Framework/UI/Button/style.lua +++ b/BuiltInPlugins/LocalizationTools/Packages/Framework/UI/Button/style.lua @@ -1,74 +1,136 @@ local Framework = script.Parent.Parent.Parent local Util = require(Framework.Util) +local Cryo = Util.Cryo +local deepCopy = Util.deepCopy local Style = Util.Style local StyleModifier = Util.StyleModifier +local StyleKey = require(Framework.Style.StyleKey) + local UI = require(Framework.UI) local Decoration = UI.Decoration +local FlagsList = Util.Flags.new({ + FFlagRefactorDevFrameworkTheme = {"RefactorDevFrameworkTheme"}, +}) + local StudioFrameworkStyles = Framework.StudioUI.StudioFrameworkStyles local Common = require(StudioFrameworkStyles.Common) local UIFolderData = require(Framework.UI.UIFolderData) local RoundBox = require(UIFolderData.RoundBox.style) -return function(theme, getColor) - local common = Common(theme, getColor) - local roundBox = RoundBox(theme, getColor) - - local Default = Style.extend(common.MainText, { +if FlagsList:get("FFlagRefactorDevFrameworkTheme") then + local roundBox = deepCopy(RoundBox) + return { Padding = 0, TextXAlignment = Enum.TextXAlignment.Center, TextYAlignment = Enum.TextYAlignment.Center, - TextColor = theme:GetColor("ButtonText"), + TextColor = StyleKey.ButtonText, Background = Decoration.Box, - BackgroundStyle = Style.extend(roundBox.Default, { - Color = theme:GetColor("Button"), + BackgroundStyle = Cryo.Dictionary.join(roundBox, { + Color = StyleKey.Button, }), + [StyleModifier.Hover] = { - BackgroundStyle = { - Color = theme:GetColor("Button", "Hover"), - }, + BackgroundStyle = Cryo.Dictionary.join(roundBox, { + Color = StyleKey.ButtonHover, + }), }, [StyleModifier.Disabled] = { BackgroundStyle = { - Color = theme:GetColor("Button", "Disabled"), + Color = StyleKey.ButtonDisabled, }, - TextColor = theme:GetColor("ButtonText", "Disabled"), + TextColor = StyleKey.ButtonDisabled, }, [StyleModifier.Pressed] = { BackgroundStyle = { - Color = theme:GetColor("Button", "Pressed"), + Color = StyleKey.ButtonHover, }, }, - }) - local Round = Style.extend(Default, { - Background = Decoration.RoundBox, - }) + ["&Round"] = { + Background = Decoration.RoundBox, + }, - local RoundPrimary = Style.extend(Round, { - TextColor = theme:GetColor("DialogMainButtonText"), - BackgroundStyle = Style.extend(roundBox.Default, { - Color = theme:GetColor("DialogMainButton"), - }), - [StyleModifier.Hover] = { - BackgroundStyle = { - Color = theme:GetColor("DialogMainButton", "Hover"), + ["&RoundPrimary"] = { + Background = Decoration.RoundBox, + BackgroundStyle = Cryo.Dictionary.join(roundBox, { + Color = StyleKey.DialogMainButton, + }), + TextColor = StyleKey.DialogMainButtonText, + [StyleModifier.Hover] = { + BackgroundStyle = { + Color = StyleKey.DialogMainButtonHover, + }, }, - }, - [StyleModifier.Disabled] = { - BackgroundStyle = { - Color = theme:GetColor("DialogMainButton", "Disabled"), + [StyleModifier.Disabled] = { + BackgroundStyle = { + Color = StyleKey.DialogMainButtonDisabled, + }, + TextColor = StyleKey.DialogMainButtonTextDisabled, }, - TextColor = theme:GetColor("DialogMainButtonText", "Disabled"), }, - }) - - return { - Default = Default, - Round = Round, - RoundPrimary = RoundPrimary, } -end +else + return function(theme, getColor) + local common = Common(theme, getColor) + local roundBox = RoundBox(theme, getColor) + + local Default = Style.extend(common.MainText, { + Padding = 0, + TextXAlignment = Enum.TextXAlignment.Center, + TextYAlignment = Enum.TextYAlignment.Center, + TextColor = theme:GetColor("ButtonText"), + Background = Decoration.Box, + BackgroundStyle = Style.extend(roundBox.Default, { + Color = theme:GetColor("Button"), + }), + [StyleModifier.Hover] = { + BackgroundStyle = { + Color = theme:GetColor("Button", "Hover"), + }, + }, + [StyleModifier.Disabled] = { + BackgroundStyle = { + Color = theme:GetColor("Button", "Disabled"), + }, + TextColor = theme:GetColor("ButtonText", "Disabled"), + }, + [StyleModifier.Pressed] = { + BackgroundStyle = { + Color = theme:GetColor("Button", "Pressed"), + }, + }, + }) + + local Round = Style.extend(Default, { + Background = Decoration.RoundBox, + }) + + local RoundPrimary = Style.extend(Round, { + TextColor = theme:GetColor("DialogMainButtonText"), + BackgroundStyle = Style.extend(roundBox.Default, { + Color = theme:GetColor("DialogMainButton"), + }), + [StyleModifier.Hover] = { + BackgroundStyle = { + Color = theme:GetColor("DialogMainButton", "Hover"), + }, + }, + [StyleModifier.Disabled] = { + BackgroundStyle = { + Color = theme:GetColor("DialogMainButton", "Disabled"), + }, + TextColor = theme:GetColor("DialogMainButtonText", "Disabled"), + }, + }) + + return { + Default = Default, + Round = Round, + RoundPrimary = RoundPrimary, + } + end +end \ No newline at end of file diff --git a/BuiltInPlugins/LocalizationTools/Packages/Framework/UI/Button/test.spec.lua b/BuiltInPlugins/LocalizationTools/Packages/Framework/UI/Button/test.spec.lua index a44d2547c4..cafabfaa40 100644 --- a/BuiltInPlugins/LocalizationTools/Packages/Framework/UI/Button/test.spec.lua +++ b/BuiltInPlugins/LocalizationTools/Packages/Framework/UI/Button/test.spec.lua @@ -8,12 +8,24 @@ return function() local Button = require(script.Parent) local Box = require(Framework.UI.Box) + local StudioTheme = require(Framework.Style.Themes.StudioTheme) + + local Util = require(Framework.Util) + local FlagsList = Util.Flags.new({ + FFlagRefactorDevFrameworkTheme = {"RefactorDevFrameworkTheme"}, + }) + local function createTestButton(props, children) - local theme = Theme.new(function() - return { - Framework = FrameworkStyles.new(), - } - end) + local theme + if FlagsList:get("FFlagRefactorDevFrameworkTheme") then + theme = StudioTheme.mock() + else + theme = Theme.new(function() + return { + Framework = FrameworkStyles.new(), + } + end) + end return provide({theme}, { Button = Roact.createElement(Button, props, children), }) diff --git a/BuiltInPlugins/LocalizationTools/Packages/Framework/UI/Container/example.lua b/BuiltInPlugins/LocalizationTools/Packages/Framework/UI/Container/example.lua index 6ffd2d3ef8..6741545ce5 100644 --- a/BuiltInPlugins/LocalizationTools/Packages/Framework/UI/Container/example.lua +++ b/BuiltInPlugins/LocalizationTools/Packages/Framework/UI/Container/example.lua @@ -11,47 +11,81 @@ return function(plugin) local Container = UI.Container local Decoration = UI.Decoration + local FrameworkStyle = Framework.Style + local ui = require(FrameworkStyle).ComponentSymbols + local StudioTheme = require(FrameworkStyle.Themes.StudioTheme) + local BaseTheme = require(FrameworkStyle.Themes.BaseTheme) + local Util = require(Framework.Util) + local Cryo = Util.Cryo local StyleTable = Util.StyleTable local Style = Util.Style + local FlagsList = Util.Flags.new({ + FFlagRefactorDevFrameworkTheme = {"RefactorDevFrameworkTheme"}, + }) local pluginItem = Plugin.new(plugin) - local theme = Theme.new(function(theme, getColor) - local box = StyleTable.new("Box", function() - local Black = Style.new({ - Color = Color3.new(0, 0, 0), + local theme + if FlagsList:get("FFlagRefactorDevFrameworkTheme") then + theme = StudioTheme.new() + theme:extend({ + [ui.Box] = Cryo.Dictionary.join(BaseTheme[ui.Box], { Transparency = 0, BorderSize = 0, - }) - local Red = Style.extend(Black, { - Color = Color3.new(0.3, 0, 0), - }) + ["&Black"] = { + Color = Color3.new(0, 0, 0), + }, - return { - Black = Black, - Red = Red, - } - end) + ["&Red"] = { + Color = Color3.new(0.3, 0, 0), + }, + }), - local image = StyleTable.new("Image", function() - local WarningIcon = Style.new({ - Image = "rbxasset://textures/ui/ErrorIcon.png", - }) + [ui.Image] = Cryo.Dictionary.join(BaseTheme[ui.Image], { + ["&WarningIcon"] = { + Image = "rbxasset://textures/ui/ErrorIcon.png", + }, + }), + }) + else + theme = Theme.new(function(theme, getColor) + local box = StyleTable.new("Box", function() + local Black = Style.new({ + Color = Color3.new(0, 0, 0), + Transparency = 0, + BorderSize = 0, + }) + + local Red = Style.extend(Black, { + Color = Color3.new(0.3, 0, 0), + }) + + return { + Black = Black, + Red = Red, + } + end) + + local image = StyleTable.new("Image", function() + local WarningIcon = Style.new({ + Image = "rbxasset://textures/ui/ErrorIcon.png", + }) + + return { + WarningIcon = WarningIcon, + } + end) return { - WarningIcon = WarningIcon, + Framework = StyleTable.extend(FrameworkStyles.new(), { + Box = box, + Image = image, + }) } end) - - return { - Framework = StyleTable.extend(FrameworkStyles.new(), { - Box = box, - Image = image, - }) - } - end) + end -- Mount and display a dialog local ExampleContainer = Roact.PureComponent:extend("ExampleContainer") diff --git a/BuiltInPlugins/LocalizationTools/Packages/Framework/UI/Container/test.spec.lua b/BuiltInPlugins/LocalizationTools/Packages/Framework/UI/Container/test.spec.lua index 77f5d1aacc..1417100db0 100644 --- a/BuiltInPlugins/LocalizationTools/Packages/Framework/UI/Container/test.spec.lua +++ b/BuiltInPlugins/LocalizationTools/Packages/Framework/UI/Container/test.spec.lua @@ -8,12 +8,24 @@ return function() local Container = require(script.Parent) local Box = require(Framework.UI.Box) + local StudioTheme = require(Framework.Style.Themes.StudioTheme) + + local Util = require(Framework.Util) + local FlagsList = Util.Flags.new({ + FFlagRefactorDevFrameworkTheme = {"RefactorDevFrameworkTheme"}, + }) + local function createTestContainer(props, children) - local theme = Theme.new(function() - return { - Framework = FrameworkStyles.new(), - } - end) + local theme + if FlagsList:get("FFlagRefactorDevFrameworkTheme") then + theme = StudioTheme.mock() + else + theme = Theme.new(function() + return { + Framework = FrameworkStyles.new(), + } + end) + end return provide({theme}, { Container = Roact.createElement(Container, props, children), }) diff --git a/BuiltInPlugins/LocalizationTools/Packages/Framework/UI/DropShadow.lua b/BuiltInPlugins/LocalizationTools/Packages/Framework/UI/DropShadow.lua index e5e9ec09e8..d9815b84a1 100644 --- a/BuiltInPlugins/LocalizationTools/Packages/Framework/UI/DropShadow.lua +++ b/BuiltInPlugins/LocalizationTools/Packages/Framework/UI/DropShadow.lua @@ -2,13 +2,12 @@ A rectangular drop shadow that appears at the edges of an element. The children of the DropShadow appear within padding equal to the value of Radius. - Required Props: - Theme Theme: A Theme ContextItem, which is provided via mapToProps. - Optional Props: Style Style: The style with which to render this component. StyleModifier StyleModifier: The StyleModifier index into Style. number ZIndex: The render index of the shadow - should be behind the element it shadows. + Stylizer Stylizer: A Stylizer ContextItem, which is provided via mapToProps. + Theme Theme: A Theme ContextItem, which is provided via mapToProps. Style Values: Color3 Color: The color of the shadow. @@ -19,12 +18,16 @@ number Radius: The radius of the shadow, in pixels. number Transparency: The transparency of the shadow (ranges from 0 to 1). ]] - local Framework = script.Parent.Parent local Roact = require(Framework.Parent.Roact) local ContextServices = require(Framework.ContextServices) +local Util = require(Framework.Util) local t = require(Framework.Util.Typecheck.t) -local Typecheck = require(Framework.Util).Typecheck +local Typecheck = Util.Typecheck + +local FlagsList = Util.Flags.new({ + FFlagRefactorDevFrameworkTheme = {"RefactorDevFrameworkTheme"}, +}) local DropShadow = Roact.PureComponent:extend("DropShadow") Typecheck.wrap(DropShadow, script) @@ -34,7 +37,12 @@ function DropShadow:render() local zIndex = props.ZIndex local theme = props.Theme - local style = theme:getStyle("Framework", self) + local style + if FlagsList:get("FFlagRefactorDevFrameworkTheme") then + style = props.Stylizer + else + style = theme:getStyle("Framework", self) + end local color = style.Color local offset = style.Offset or Vector2.new() @@ -80,7 +88,8 @@ function DropShadow:render() end ContextServices.mapToProps(DropShadow, { - Theme = ContextServices.Theme, + Stylizer = FlagsList:get("FFlagRefactorDevFrameworkTheme") and ContextServices.Stylizer or nil, + Theme = (not FlagsList:get("FFlagRefactorDevFrameworkTheme")) and ContextServices.Theme or nil, }) return DropShadow diff --git a/BuiltInPlugins/LocalizationTools/Packages/Framework/UI/DropShadow/style.lua b/BuiltInPlugins/LocalizationTools/Packages/Framework/UI/DropShadow/style.lua index 4280b47efd..0caf0a4aad 100644 --- a/BuiltInPlugins/LocalizationTools/Packages/Framework/UI/DropShadow/style.lua +++ b/BuiltInPlugins/LocalizationTools/Packages/Framework/UI/DropShadow/style.lua @@ -3,18 +3,35 @@ local Framework = script.Parent.Parent.Parent local Util = require(Framework.Util) local Style = Util.Style -return function(theme, getColor) +local StyleKey = require(Framework.Style.StyleKey) - local Default = Style.new({ - Color = theme:GetColor("Border"), +local FlagsList = Util.Flags.new({ + FFlagRefactorDevFrameworkTheme = {"RefactorDevFrameworkTheme"}, +}) + +if FlagsList:get("FFlagRefactorDevFrameworkTheme") then + return { + Color = StyleKey.Border, Image = "rbxasset://textures/StudioSharedUI/dropShadow.png", ImageSize = 16, Offset = Vector2.new(), Radius = 6, - Transparency = 0 - }) - - return { - Default = Default, + Transparency = 0, } -end +else + return function(theme, getColor) + + local Default = Style.new({ + Color = theme:GetColor("Border"), + Image = "rbxasset://textures/StudioSharedUI/dropShadow.png", + ImageSize = 16, + Offset = Vector2.new(), + Radius = 6, + Transparency = 0 + }) + + return { + Default = Default, + } + end +end \ No newline at end of file diff --git a/BuiltInPlugins/LocalizationTools/Packages/Framework/UI/DropdownMenu.lua b/BuiltInPlugins/LocalizationTools/Packages/Framework/UI/DropdownMenu.lua index ce29ef7881..b1e6b3c757 100644 --- a/BuiltInPlugins/LocalizationTools/Packages/Framework/UI/DropdownMenu.lua +++ b/BuiltInPlugins/LocalizationTools/Packages/Framework/UI/DropdownMenu.lua @@ -8,14 +8,15 @@ boolean Hide: Whether the menu is hidden table Items: An array of each item that should appear in the dropdown. callback OnItemActivated: A callback for when the user selects a dropdown entry. - Theme Theme: a Theme object supplied by mapToProps() Focus Focus: a Focus object supplied by mapToProps() Optional Props: + Theme Theme: a Theme object supplied by mapToProps() string PlaceholderText: A placeholder to display if there is no item selected. callback OnRenderItem: A function used to render a dropdown menu item. callback OnFocusLost: A function called when the focus on the menu is lost. number SelectedIndex: The currently selected item index. + Stylizer Stylizer: A Stylizer ContextItem, which is provided via mapToProps. Style Style: The style with which to render this component. Style Values: @@ -25,13 +26,14 @@ number Width: The width of the menu area. number MaxHeight: The maximum height of the menu area. ]] - local Framework = script.Parent.Parent local Roact = require(Framework.Parent.Roact) local ContextServices = require(Framework.ContextServices) + local Util = require(Framework.Util) local prioritize = Util.prioritize local Typecheck = Util.Typecheck + local UI = Framework.UI local Container = require(UI.Container) local CaptureFocus = require(UI.CaptureFocus) @@ -40,6 +42,11 @@ local Button = require(UI.Button) local RoundBox = require(UI.RoundBox) local TextLabel = require(UI.TextLabel) +local FlagsList = Util.Flags.new({ + FFlagRefactorDevFrameworkTheme = {"RefactorDevFrameworkTheme"}, + FFlagRefactorDevFrameworkContextItems = {"RefactorDevFrameworkContextItems"}, +}) + local DropdownMenu = Roact.PureComponent:extend("DropdownMenu") Typecheck.wrap(DropdownMenu, script) @@ -86,7 +93,12 @@ function DropdownMenu:init() -- calculate the size and position of the dropdown local height = state.menuContentSize.Y - local style = props.Theme:getStyle("Framework", self) + local style + if FlagsList:get("FFlagRefactorDevFrameworkTheme") then + style = props.Stylizer + else + style = props.Theme:getStyle("Framework", self) + end local maxHeight = style.MaxHeight local sourcePosition = state.absolutePosition @@ -168,10 +180,14 @@ local function defaultOnRenderItem(item, index, activated) end function DropdownMenu:renderMenu() - local state = self.state local props = self.props - local style = props.Theme:getStyle("Framework", self) + local style + if FlagsList:get("FFlagRefactorDevFrameworkTheme") then + style = props.Stylizer + else + style = props.Theme:getStyle("Framework", self) + end local items = props.Items local onRenderItem = prioritize(props.OnRenderItem, defaultOnRenderItem) @@ -186,7 +202,7 @@ function DropdownMenu:renderMenu() local width = style.Width local offset = prioritize(style.Offset, Vector2.new(0, 0)) - local pluginGui = props.Focus:getTarget() + local pluginGui = FlagsList:get("FFlagRefactorDevFrameworkContextItems") and props.Focus:get() or props.Focus:getTarget() local menuPositionAndSize = self.getPositionAndSize(pluginGui, width, offset) local x = menuPositionAndSize.X @@ -257,7 +273,8 @@ end ContextServices.mapToProps(DropdownMenu, { Focus = ContextServices.Focus, - Theme = ContextServices.Theme, + Stylizer = FlagsList:get("FFlagRefactorDevFrameworkTheme") and ContextServices.Stylizer or nil, + Theme = (not FlagsList:get("FFlagRefactorDevFrameworkTheme")) and ContextServices.Theme or nil, }) return DropdownMenu diff --git a/BuiltInPlugins/LocalizationTools/Packages/Framework/UI/DropdownMenu/style.lua b/BuiltInPlugins/LocalizationTools/Packages/Framework/UI/DropdownMenu/style.lua index 96e8578c53..1b189d6ba8 100644 --- a/BuiltInPlugins/LocalizationTools/Packages/Framework/UI/DropdownMenu/style.lua +++ b/BuiltInPlugins/LocalizationTools/Packages/Framework/UI/DropdownMenu/style.lua @@ -3,24 +3,47 @@ local Framework = script.Parent.Parent.Parent local StudioFrameworkStyles = Framework.StudioUI.StudioFrameworkStyles local Common = require(StudioFrameworkStyles.Common) local UIFolderData = require(Framework.UI.UIFolderData) -local Util = require(Framework.Util) local RoundBox = require(UIFolderData.RoundBox.style) + +local Util = require(Framework.Util) +local deepCopy = Util.deepCopy local Style = Util.Style -return function(theme, getColor) - local common = Common(theme, getColor) - local roundBox = RoundBox(theme, getColor) +local StyleKey = require(Framework.Style.StyleKey) +local FlagsList = Util.Flags.new({ + FFlagRefactorDevFrameworkTheme = {"RefactorDevFrameworkTheme"}, +}) + +if FlagsList:get("FFlagRefactorDevFrameworkTheme") then + local roundBox = deepCopy(RoundBox) return { - Default = Style.extend(common.Border, { - BackgroundStyle = roundBox.Default, - Width = 240, - MaxHeight = 240, - Offset = Vector2.new(0, 0), - Text = Style.extend(common.MainText, { - TextXAlignment = Enum.TextXAlignment.Left, - }) - }) + BackgroundStyle = roundBox, + BorderColor = StyleKey.Border, + Width = 240, + MaxHeight = 240, + Offset = Vector2.new(0, 0), + Text = { + TextSize = 18, + TextXAlignment = Enum.TextXAlignment.Left, + }, } +else + return function(theme, getColor) + local common = Common(theme, getColor) + local roundBox = RoundBox(theme, getColor) + return { + Default = Style.extend(common.Border, { + BackgroundStyle = roundBox.Default, + Width = 240, + MaxHeight = 240, + Offset = Vector2.new(0, 0), + Text = { + TextSize = 18, + TextXAlignment = Enum.TextXAlignment.Left, + }, + }) + } + end end \ No newline at end of file diff --git a/BuiltInPlugins/LocalizationTools/Packages/Framework/UI/FakeLoadingBar/test.spec.lua b/BuiltInPlugins/LocalizationTools/Packages/Framework/UI/FakeLoadingBar/test.spec.lua index bea92510ff..1306596adc 100644 --- a/BuiltInPlugins/LocalizationTools/Packages/Framework/UI/FakeLoadingBar/test.spec.lua +++ b/BuiltInPlugins/LocalizationTools/Packages/Framework/UI/FakeLoadingBar/test.spec.lua @@ -7,12 +7,24 @@ return function() local FrameworkStyles = require(Framework.UI.FrameworkStyles) local FakeLoadingBar = require(script.Parent) + local StudioTheme = require(Framework.Style.Themes.StudioTheme) + + local Util = require(Framework.Util) + local FlagsList = Util.Flags.new({ + FFlagRefactorDevFrameworkTheme = {"RefactorDevFrameworkTheme"}, + }) + local function createTestFakeLoadingBar(props, children) - local theme = Theme.new(function() - return { - Framework = FrameworkStyles.new(), - } - end) + local theme + if FlagsList:get("FFlagRefactorDevFrameworkTheme") then + theme = StudioTheme.mock() + else + theme = Theme.new(function() + return { + Framework = FrameworkStyles.new(), + } + end) + end return provide({theme}, { FakeLoadingBar = Roact.createElement(FakeLoadingBar, props, children), }) diff --git a/BuiltInPlugins/LocalizationTools/Packages/Framework/UI/FrameworkStyles.spec.lua b/BuiltInPlugins/LocalizationTools/Packages/Framework/UI/FrameworkStyles.spec.lua index ed89aed938..7986ecd5a5 100644 --- a/BuiltInPlugins/LocalizationTools/Packages/Framework/UI/FrameworkStyles.spec.lua +++ b/BuiltInPlugins/LocalizationTools/Packages/Framework/UI/FrameworkStyles.spec.lua @@ -9,8 +9,8 @@ return function() for _, entry in pairs(styles) do expect(entry.Default).to.be.ok() - expect(next(entry.Default)).never.to.be.ok() + expect((next(entry.Default))).never.to.be.ok() end end) end) -end \ No newline at end of file +end diff --git a/BuiltInPlugins/LocalizationTools/Packages/Framework/UI/Image.lua b/BuiltInPlugins/LocalizationTools/Packages/Framework/UI/Image.lua index dd0635a462..4a602ce8af 100644 --- a/BuiltInPlugins/LocalizationTools/Packages/Framework/UI/Image.lua +++ b/BuiltInPlugins/LocalizationTools/Packages/Framework/UI/Image.lua @@ -1,12 +1,11 @@ --[[ A Decoration image. - Required Props: - Theme Theme: A Theme ContextItem, which is provided via mapToProps. - Optional Props: + Theme Theme: A Theme ContextItem, which is provided via mapToProps. Style Style: The style with which to render this component. StyleModifier StyleModifier: The StyleModifier index into Style. + Stylizer Stylizer: A Stylizer ContextItem, which is provided via mapToProps. Style Values: Vector2 AnchorPoint: The anchor point of the image. @@ -26,6 +25,11 @@ local Framework = script.Parent.Parent local Roact = require(Framework.Parent.Roact) local ContextServices = require(Framework.ContextServices) local Typecheck = require(Framework.Util).Typecheck +local Util = require(Framework.Util) + +local FlagsList = Util.Flags.new({ + FFlagRefactorDevFrameworkTheme = {"RefactorDevFrameworkTheme"}, +}) local Image = Roact.PureComponent:extend("Image") Typecheck.wrap(Image, script) @@ -33,7 +37,12 @@ Typecheck.wrap(Image, script) function Image:render() local props = self.props local theme = props.Theme - local style = theme:getStyle("Framework", self) + local style + if FlagsList:get("FFlagRefactorDevFrameworkTheme") then + style = props.Stylizer + else + style = theme:getStyle("Framework", self) + end local color = style.Color local transparency = style.Transparency @@ -65,7 +74,8 @@ function Image:render() end ContextServices.mapToProps(Image, { - Theme = ContextServices.Theme, + Stylizer = FlagsList:get("FFlagRefactorDevFrameworkTheme") and ContextServices.Stylizer or nil, + Theme = (not FlagsList:get("FFlagRefactorDevFrameworkTheme")) and ContextServices.Theme or nil, }) return Image diff --git a/BuiltInPlugins/LocalizationTools/Packages/Framework/UI/Image/style.lua b/BuiltInPlugins/LocalizationTools/Packages/Framework/UI/Image/style.lua index 8f8ad3191f..a100295d01 100644 --- a/BuiltInPlugins/LocalizationTools/Packages/Framework/UI/Image/style.lua +++ b/BuiltInPlugins/LocalizationTools/Packages/Framework/UI/Image/style.lua @@ -3,12 +3,22 @@ local Framework = script.Parent.Parent.Parent local Util = require(Framework.Util) local Style = Util.Style -return function(theme, getColor) - local Default = Style.new({ - Color = Color3.new(1, 1, 1), -- Full white so image is uncolored - }) +local FlagsList = Util.Flags.new({ + FFlagRefactorDevFrameworkTheme = {"RefactorDevFrameworkTheme"}, +}) +if FlagsList:get("FFlagRefactorDevFrameworkTheme") then return { - Default = Default, + Color = Color3.new(1, 1, 1), -- Full white so image is uncolored } -end +else + return function(theme, getColor) + local Default = Style.new({ + Color = Color3.new(1, 1, 1), -- Full white so image is uncolored + }) + + return { + Default = Default, + } + end +end \ No newline at end of file diff --git a/BuiltInPlugins/LocalizationTools/Packages/Framework/UI/Image/test.spec.lua b/BuiltInPlugins/LocalizationTools/Packages/Framework/UI/Image/test.spec.lua index a8fd8f1c5f..6860be79dd 100644 --- a/BuiltInPlugins/LocalizationTools/Packages/Framework/UI/Image/test.spec.lua +++ b/BuiltInPlugins/LocalizationTools/Packages/Framework/UI/Image/test.spec.lua @@ -7,12 +7,24 @@ return function() local FrameworkStyles = require(Framework.UI.FrameworkStyles) local Image = require(script.Parent) + local StudioTheme = require(Framework.Style.Themes.StudioTheme) + + local Util = require(Framework.Util) + local FlagsList = Util.Flags.new({ + FFlagRefactorDevFrameworkTheme = {"RefactorDevFrameworkTheme"}, + }) + local function createTestImageDecoration() - local theme = Theme.new(function() - return { - Framework = FrameworkStyles.new(), - } - end) + local theme + if FlagsList:get("FFlagRefactorDevFrameworkTheme") then + theme = StudioTheme.mock() + else + theme = Theme.new(function() + return { + Framework = FrameworkStyles.new(), + } + end) + end return provide({theme}, { Image = Roact.createElement(Image), }) diff --git a/BuiltInPlugins/LocalizationTools/Packages/Framework/UI/InfiniteScrollingFrame.lua b/BuiltInPlugins/LocalizationTools/Packages/Framework/UI/InfiniteScrollingFrame.lua index fa700bd024..8e3a1d96cb 100644 --- a/BuiltInPlugins/LocalizationTools/Packages/Framework/UI/InfiniteScrollingFrame.lua +++ b/BuiltInPlugins/LocalizationTools/Packages/Framework/UI/InfiniteScrollingFrame.lua @@ -3,13 +3,14 @@ A thin wrapper around the infinite-scroller library with DeveloperFramework theming and naming conventions. Required Props: - Theme Theme: the theme supplied from mapToProps() array[any] Items: The items to scroll through. callback RenderItem: Callback to render each item that should be visible. The items should have LayoutOrder set. Optional Props: + Theme Theme: the theme supplied from mapToProps() Style Style: a style table supplied from props and theme:getStyle() + Stylizer Stylizer: A Stylizer ContextItem, which is provided via mapToProps. UDim2 Position: The position of the scrolling frame. UDim2 Size: The size of the scrolling frame. integer LayoutOrder: The order this component will display in a UILayout. @@ -35,11 +36,11 @@ integer ScrollBarThickness: The horizontal width of the scrollbar. boolean ScrollingEnabled: Whether scrolling in this frame will change the CanvasPosition. ]] - local Framework = script.Parent.Parent local Util = require(Framework.Util) local FlagsList = Util.Flags.new({ FFlagStudioDevFrameworkPackage = {"StudioDevFrameworkPackage"}, + FFlagRefactorDevFrameworkTheme = {"RefactorDevFrameworkTheme"}, }) local Roact = require(Framework.Parent.Roact) @@ -95,9 +96,17 @@ function InfiniteScrollingFrame:init() self.getInfiniteScrollingFrameProps = function(props, style) -- After filtering out parent's props and DeveloperFramework-specific props (such as Style and Theme), -- what is left are infinite-scroller props + local updatedProps + if FlagsList:get("FFlagRefactorDevFrameworkTheme") then + updatedProps = Cryo.Dictionary.join(props, { + Stylizer = Cryo.None + }) + else + updatedProps = props + end return Cryo.Dictionary.join( style, - props, + updatedProps, self.propFilters.containerProps, self.propFilters.wrapperProps, { @@ -128,7 +137,13 @@ end function InfiniteScrollingFrame:render() local props = self.props local theme = props.Theme - local style = theme:getStyle("Framework", self) + + local style + if FlagsList:get("FFlagRefactorDevFrameworkTheme") then + style = props.Stylizer + else + style = theme:getStyle("Framework", self) + end local position = props.Position local size = props.Size @@ -147,7 +162,8 @@ function InfiniteScrollingFrame:render() end ContextServices.mapToProps(InfiniteScrollingFrame, { - Theme = ContextServices.Theme, + Stylizer = FlagsList:get("FFlagRefactorDevFrameworkTheme") and ContextServices.Stylizer or nil, + Theme = (not FlagsList:get("FFlagRefactorDevFrameworkTheme")) and ContextServices.Theme or nil, }) return InfiniteScrollingFrame \ No newline at end of file diff --git a/BuiltInPlugins/LocalizationTools/Packages/Framework/UI/InfiniteScrollingFrame/style.lua b/BuiltInPlugins/LocalizationTools/Packages/Framework/UI/InfiniteScrollingFrame/style.lua index 2ce6539f13..0d8c13bdd1 100644 --- a/BuiltInPlugins/LocalizationTools/Packages/Framework/UI/InfiniteScrollingFrame/style.lua +++ b/BuiltInPlugins/LocalizationTools/Packages/Framework/UI/InfiniteScrollingFrame/style.lua @@ -3,12 +3,38 @@ local Framework = script.Parent.Parent.Parent local StudioFrameworkStyles = Framework.StudioUI.StudioFrameworkStyles local Common = require(StudioFrameworkStyles.Common) -return function(theme, getColor) - local common = Common(theme, getColor) +local StyleKey = require(Framework.Style.StyleKey) - local Default = common.Scroller +local Util = require(Framework.Util) +local FlagsList = Util.Flags.new({ + FFlagRefactorDevFrameworkTheme = {"RefactorDevFrameworkTheme"}, +}) +if FlagsList:get("FFlagRefactorDevFrameworkTheme") then return { - Default = Default, + BackgroundTransparency = 1, + BorderSizePixel = 0, + BackgroundColor3 = StyleKey.MainBackground, + + TopImage = "rbxasset://textures/StudioToolbox/ScrollBarTop.png", + MidImage = "rbxasset://textures/StudioToolbox/ScrollBarMiddle.png", + BottomImage = "rbxasset://textures/StudioToolbox/ScrollBarBottom.png", + + ScrollingEnabled = true, + ScrollingDirection = Enum.ScrollingDirection.Y, + ScrollBarThickness = 8, + ScrollBarImageTransparency = 0.5, + ScrollBarImageColor3 = StyleKey.ScrollBar, + VerticalScrollBarInset = Enum.ScrollBarInset.Always } +else + return function(theme, getColor) + local common = Common(theme, getColor) + + local Default = common.Scroller + + return { + Default = Default, + } + end end diff --git a/BuiltInPlugins/LocalizationTools/Packages/Framework/UI/InstanceTreeView.lua b/BuiltInPlugins/LocalizationTools/Packages/Framework/UI/InstanceTreeView.lua index e2582b8bad..0d6c9d37a0 100644 --- a/BuiltInPlugins/LocalizationTools/Packages/Framework/UI/InstanceTreeView.lua +++ b/BuiltInPlugins/LocalizationTools/Packages/Framework/UI/InstanceTreeView.lua @@ -2,7 +2,6 @@ Displays the hierarchy of an instance. Required Props: - Theme Theme: The theme supplied from mapToProps() UDim2 Size: The size of the component table Instances: The instance which this tree should display at root table Expansion: Which items should be expanded - Set @@ -11,8 +10,10 @@ callback OnSelectionChange: Called when a node is selected or not - (newSelection: Set) => void Optional Props: + Theme Theme: The theme supplied from mapToProps() callback SortChildren: A comparator function to sort two items in the tree - SortChildren(left: Item, right: Item) => boolean Style Style: a style table supplied from props and theme:getStyle() + Stylizer Stylizer: A Stylizer ContextItem, which is provided via mapToProps. Style Values: table TreeView: Style values for the underlying tree view. @@ -30,6 +31,11 @@ local Cryo = require(Framework.Parent.Cryo) local UI = Framework.UI local TreeView = require(UI.TreeView) local InstanceTreeRow = require(script.InstanceTreeRow) +local Util = require(Framework.Util) + +local FlagsList = Util.Flags.new({ + FFlagRefactorDevFrameworkTheme = {"RefactorDevFrameworkTheme"}, +}) local InstanceTreeView = Roact.PureComponent:extend("InstanceTreeView") Typecheck.wrap(InstanceTreeView, script) @@ -55,7 +61,12 @@ function InstanceTreeView:init() self.renderRow = function(row) local props = self.props local theme = props.Theme - local style = theme:getStyle("Framework", self) + local style + if FlagsList:get("FFlagRefactorDevFrameworkTheme") then + style = props.Stylizer + else + style = theme:getStyle("Framework", self) + end local isSelected = props.Selection[row.item] local isExpanded = props.Expansion[row.item] @@ -81,7 +92,12 @@ end function InstanceTreeView:render() local props = self.props local theme = props.Theme - local style = theme:getStyle("Framework", self) + local style + if FlagsList:get("FFlagRefactorDevFrameworkTheme") then + style = props.Stylizer + else + style = theme:getStyle("Framework", self) + end return Roact.createElement(TreeView, { RootItems = props.Instances, @@ -95,7 +111,8 @@ function InstanceTreeView:render() end ContextServices.mapToProps(InstanceTreeView, { - Theme = ContextServices.Theme, + Stylizer = FlagsList:get("FFlagRefactorDevFrameworkTheme") and ContextServices.Stylizer or nil, + Theme = (not FlagsList:get("FFlagRefactorDevFrameworkTheme")) and ContextServices.Theme or nil, }) return InstanceTreeView \ No newline at end of file diff --git a/BuiltInPlugins/LocalizationTools/Packages/Framework/UI/InstanceTreeView/InstanceTreeRow.lua b/BuiltInPlugins/LocalizationTools/Packages/Framework/UI/InstanceTreeView/InstanceTreeRow.lua index 8000e57785..6110490a54 100644 --- a/BuiltInPlugins/LocalizationTools/Packages/Framework/UI/InstanceTreeView/InstanceTreeRow.lua +++ b/BuiltInPlugins/LocalizationTools/Packages/Framework/UI/InstanceTreeView/InstanceTreeRow.lua @@ -25,7 +25,7 @@ local TextLabel = require(UI.TextLabel) Typecheck.wrap(InstanceTreeRow, script) local function getClassIcon(instance) - local StudioService = game:GetService("StudioService") + local StudioService = game:GetService("StudioService") local className = instance.ClassName if instance.IsA then if instance:IsA("JointInstance") and className == "ManualWeld" or className == "ManualGlue" then @@ -73,9 +73,10 @@ function InstanceTreeRow:render() local arrowSize = style.Arrow.Size local padding = style.IconPadding local iconInfo = getClassIcon(item) - local iconSize = iconInfo.ImageRectSize.X + -- Default iconSize to (0, 0) as ImageRectSize is unavailable in Roblox CLI + local iconSize = iconInfo.ImageRectSize or Vector2.new() local labelOffset = indent + arrowSize + 2 * padding - local textOffset = iconSize + 3 * padding + local textOffset = iconSize.X + 3 * padding return Roact.createElement(Container, { Size = UDim2.new(1, -indent, 0, style.RowHeight), @@ -105,10 +106,10 @@ function InstanceTreeRow:render() Size = UDim2.new(1, -arrowSize, 1, 0), }, { Icon = Roact.createElement("ImageLabel", { - Size = UDim2.fromOffset(iconSize, iconInfo.ImageRectSize.Y), + Size = UDim2.fromOffset(iconSize.X, iconSize.Y), BackgroundTransparency = 1, Image = iconInfo.Image, - ImageRectSize = iconInfo.ImageRectSize, + ImageRectSize = iconSize, ImageRectOffset = iconInfo.ImageRectOffset, Position = UDim2.new(0, padding, 0.5, 0), AnchorPoint = Vector2.new(0, 0.5) diff --git a/BuiltInPlugins/LocalizationTools/Packages/Framework/UI/InstanceTreeView/style.lua b/BuiltInPlugins/LocalizationTools/Packages/Framework/UI/InstanceTreeView/style.lua index bff5e0d59f..582d52069b 100644 --- a/BuiltInPlugins/LocalizationTools/Packages/Framework/UI/InstanceTreeView/style.lua +++ b/BuiltInPlugins/LocalizationTools/Packages/Framework/UI/InstanceTreeView/style.lua @@ -1,20 +1,27 @@ local Framework = script.Parent.Parent.Parent local Util = require(Framework.Util) +local Cryo = Util.Cryo +local deepCopy = Util.deepCopy local Style = Util.Style + +local StyleKey = require(Framework.Style.StyleKey) + local UIFolderData = require(Framework.UI.UIFolderData) local TreeView = require(UIFolderData.TreeView.style) local StudioFrameworkStyles = Framework.StudioUI.StudioFrameworkStyles local Common = require(StudioFrameworkStyles.Common) -return function(theme, getColor) - local common = Common(theme, getColor) - local treeView = TreeView(theme, getColor) +local FlagsList = Util.Flags.new({ + FFlagRefactorDevFrameworkTheme = {"RefactorDevFrameworkTheme"}, +}) - local Default = Style.new({ - Text = Style.extend(common.MainText, {}), - TreeView = Style.extend(treeView.Default, {}), +if FlagsList:get("FFlagRefactorDevFrameworkTheme") then + local treeView = deepCopy(TreeView) + return { + Text = Common.MainText, + TreeView = treeView, Indent = 20, RowHeight = 24, Arrow = { @@ -22,25 +29,57 @@ return function(theme, getColor) Size = 12, ExpandedOffset = Vector2.new(24, 0), CollapsedOffset = Vector2.new(12, 0), - Color = theme:GetColor("MainText") + Color = StyleKey.MainText, }, IconPadding = 5, - HoverColor = theme:GetColor("Button", "Hover"), - SelectedColor = theme:GetColor("DialogMainButton"), - SelectedTextColor = theme:GetColor("DialogMainButtonText") - }) - - local Compact = Style.extend(Default, { - Text = Style.extend(common.MainText, { - TextSize = 14 - }), - IconPadding = 3, - RowHeight = 20, - Indent = 16 - }) + HoverColor = StyleKey.ButtonHover, + SelectedColor = StyleKey.DialogMainButton, + SelectedTextColor = StyleKey.DialogMainButtonText, - return { - Default = Default, - Compact = Compact + ["&Compact"] = { + Text = Cryo.Dictionary.join(Common.MainText, { + TextSize = 14, + }), + IconPadding = 3, + RowHeight = 20, + Indent = 16 + } } -end +else + return function(theme, getColor) + local common = Common(theme, getColor) + local treeView = TreeView(theme, getColor) + + local Default = Style.new({ + Text = Style.extend(common.MainText, {}), + TreeView = Style.extend(treeView.Default, {}), + Indent = 20, + RowHeight = 24, + Arrow = { + Image = "rbxasset://textures/StudioSharedUI/arrowSpritesheet.png", + Size = 12, + ExpandedOffset = Vector2.new(24, 0), + CollapsedOffset = Vector2.new(12, 0), + Color = theme:GetColor("MainText") + }, + IconPadding = 5, + HoverColor = theme:GetColor("Button", "Hover"), + SelectedColor = theme:GetColor("DialogMainButton"), + SelectedTextColor = theme:GetColor("DialogMainButtonText") + }) + + local Compact = Style.extend(Default, { + Text = Style.extend(common.MainText, { + TextSize = 14 + }), + IconPadding = 3, + RowHeight = 20, + Indent = 16 + }) + + return { + Default = Default, + Compact = Compact + } + end +end \ No newline at end of file diff --git a/BuiltInPlugins/LocalizationTools/Packages/Framework/UI/LinkText.lua b/BuiltInPlugins/LocalizationTools/Packages/Framework/UI/LinkText.lua index bd62ad201f..aa7ce4d917 100644 --- a/BuiltInPlugins/LocalizationTools/Packages/Framework/UI/LinkText.lua +++ b/BuiltInPlugins/LocalizationTools/Packages/Framework/UI/LinkText.lua @@ -4,9 +4,10 @@ Required Props: callback OnClick: A callback for when the user clicks this link. - Theme Theme: A Theme ContextItem, which is provided via mapToProps. Optional Props: + Theme Theme: A Theme ContextItem, which is provided via mapToProps. + Stylizer Stylizer: A Stylizer ContextItem, which is provided via mapToProps. string Text: The text to display in this link. Style Style: The style with which to render this component. StyleModifier StyleModifier: The StyleModifier index into Style. @@ -30,9 +31,13 @@ local Framework = script.Parent.Parent local Roact = require(Framework.Parent.Roact) local ContextServices = require(Framework.ContextServices) local HoverArea = require(Framework.UI.HoverArea) + local Util = require(Framework.Util) local StyleModifier = Util.StyleModifier local Typecheck = Util.Typecheck +local FlagsList = Util.Flags.new({ + FFlagRefactorDevFrameworkTheme = {"RefactorDevFrameworkTheme"}, +}) local Button = require(Framework.UI.Button) @@ -73,7 +78,12 @@ function LinkText:render() local state = self.state local theme = props.Theme local styleModifier = state.StyleModifier - local style = theme:getStyle("Framework", self) + local style + if FlagsList:get("FFlagRefactorDevFrameworkTheme") then + style = props.Stylizer + else + style = theme:getStyle("Framework", self) + end local size = props.Size @@ -166,7 +176,8 @@ function LinkText:render() end ContextServices.mapToProps(LinkText, { - Theme = ContextServices.Theme, + Stylizer = FlagsList:get("FFlagRefactorDevFrameworkTheme") and ContextServices.Stylizer or nil, + Theme = (not FlagsList:get("FFlagRefactorDevFrameworkTheme")) and ContextServices.Theme or nil, }) return LinkText diff --git a/BuiltInPlugins/LocalizationTools/Packages/Framework/UI/LinkText/example.lua b/BuiltInPlugins/LocalizationTools/Packages/Framework/UI/LinkText/example.lua index 4f03c023cc..2f8bc4927c 100644 --- a/BuiltInPlugins/LocalizationTools/Packages/Framework/UI/LinkText/example.lua +++ b/BuiltInPlugins/LocalizationTools/Packages/Framework/UI/LinkText/example.lua @@ -10,20 +10,32 @@ return function(plugin) local Dialog = StudioUI.Dialog local StudioFrameworkStyles = StudioUI.StudioFrameworkStyles + local StudioTheme = require(Framework.Style.Themes.StudioTheme) + local UI = require(Framework.UI) local Container = UI.Container local LinkText = UI.LinkText local Decoration = UI.Decoration + local Util = require(Framework.Util) + local FlagsList = Util.Flags.new({ + FFlagRefactorDevFrameworkTheme = {"RefactorDevFrameworkTheme"}, + }) + local pluginItem = Plugin.new(plugin) local mouse = Mouse.new(plugin:GetMouse()) - local theme = Theme.new(function(theme, getColor) - local studioStyles = StudioFrameworkStyles.new(theme, getColor) - return { - Framework = studioStyles, - } - end) + local theme + if FlagsList:get("FFlagRefactorDevFrameworkTheme") then + theme = StudioTheme.new() + else + theme = Theme.new(function(theme, getColor) + local studioStyles = StudioFrameworkStyles.new(theme, getColor) + return { + Framework = studioStyles, + } + end) + end -- Mount and display a dialog local ExampleLinkText = Roact.PureComponent:extend("ExampleLinkText") diff --git a/BuiltInPlugins/LocalizationTools/Packages/Framework/UI/LinkText/style.lua b/BuiltInPlugins/LocalizationTools/Packages/Framework/UI/LinkText/style.lua index 7cd57e28ca..da9dac1c20 100644 --- a/BuiltInPlugins/LocalizationTools/Packages/Framework/UI/LinkText/style.lua +++ b/BuiltInPlugins/LocalizationTools/Packages/Framework/UI/LinkText/style.lua @@ -1,19 +1,30 @@ local Framework = script.Parent.Parent.Parent +local StyleKey = require(Framework.Style.StyleKey) + local Util = require(Framework.Util) local Style = Util.Style +local FlagsList = Util.Flags.new({ + FFlagRefactorDevFrameworkTheme = {"RefactorDevFrameworkTheme"}, +}) local StudioFrameworkStyles = Framework.StudioUI.StudioFrameworkStyles local Common = require(StudioFrameworkStyles.Common) -return function(theme, getColor) - local common = Common(theme, getColor) - - local Default = Style.extend(common.MainText, { - TextColor = theme:GetColor("LinkText"), - }) - +if FlagsList:get("FFlagRefactorDevFrameworkTheme") then return { - Default = Default, + TextColor = StyleKey.LinkText, } -end +else + return function(theme, getColor) + local common = Common(theme, getColor) + + local Default = Style.extend(common.MainText, { + TextColor = theme:GetColor("LinkText"), + }) + + return { + Default = Default, + } + end +end \ No newline at end of file diff --git a/BuiltInPlugins/LocalizationTools/Packages/Framework/UI/LinkText/test.spec.lua b/BuiltInPlugins/LocalizationTools/Packages/Framework/UI/LinkText/test.spec.lua index ba7d3b4e2b..37cecf30b1 100644 --- a/BuiltInPlugins/LocalizationTools/Packages/Framework/UI/LinkText/test.spec.lua +++ b/BuiltInPlugins/LocalizationTools/Packages/Framework/UI/LinkText/test.spec.lua @@ -8,13 +8,25 @@ return function() local FrameworkStyles = require(Framework.UI.FrameworkStyles) local LinkText = require(script.Parent) + local StudioTheme = require(Framework.Style.Themes.StudioTheme) + + local Util = require(Framework.Util) + local FlagsList = Util.Flags.new({ + FFlagRefactorDevFrameworkTheme = {"RefactorDevFrameworkTheme"}, + }) + local function createTestLinkText(props) local mouse = Mouse.new({}) - local theme = Theme.new(function() - return { - Framework = FrameworkStyles.new(), - } - end) + local theme + if FlagsList:get("FFlagRefactorDevFrameworkTheme") then + theme = StudioTheme.mock() + else + theme = Theme.new(function() + return { + Framework = FrameworkStyles.new(), + } + end) + end return provide({theme, mouse}, { LinkText = Roact.createElement(LinkText, props), }) diff --git a/BuiltInPlugins/LocalizationTools/Packages/Framework/UI/LoadingBar.lua b/BuiltInPlugins/LocalizationTools/Packages/Framework/UI/LoadingBar.lua index 6f964d35d9..837d5a034b 100644 --- a/BuiltInPlugins/LocalizationTools/Packages/Framework/UI/LoadingBar.lua +++ b/BuiltInPlugins/LocalizationTools/Packages/Framework/UI/LoadingBar.lua @@ -5,12 +5,13 @@ Required Props: number Progress: The progress of the load, between 0 and 1. - Theme Theme: A Theme ContextItem, which is provided via mapToProps. Optional Props: - Style Style: The style with which to render this component. + Theme Theme: A Theme ContextItem, which is provided via mapToProps. + Stylizer Stylizer: A Stylizer ContextItem, which is provided via mapToProps. StyleModifier StyleModifier: The StyleModifier index into Style. UDim2 Size: The size of this component. + Style Style: The style with which to render this component. UDim2 Position: The position of this component. Vector2 AnchorPoint: The pivot point of this component's Position prop. number ZIndex: The render index of this component. @@ -27,7 +28,12 @@ local Framework = script.Parent.Parent local Roact = require(Framework.Parent.Roact) local ContextServices = require(Framework.ContextServices) local Container = require(Framework.UI.Container) -local Typecheck = require(Framework.Util).Typecheck + +local Util = require(Framework.Util) +local Typecheck = Util.Typecheck +local FlagsList = Util.Flags.new({ + FFlagRefactorDevFrameworkTheme = {"RefactorDevFrameworkTheme"}, +}) local LoadingBar = Roact.PureComponent:extend("LoadingBar") Typecheck.wrap(LoadingBar, script) @@ -39,7 +45,12 @@ end function LoadingBar:render() local props = self.props local theme = props.Theme - local style = theme:getStyle("Framework", self) + local style + if FlagsList:get("FFlagRefactorDevFrameworkTheme") then + style = props.Stylizer + else + style = theme:getStyle("Framework", self) + end local progress = props.Progress progress = math.clamp(progress, 0, 1) @@ -73,7 +84,8 @@ function LoadingBar:render() end ContextServices.mapToProps(LoadingBar, { - Theme = ContextServices.Theme, + Stylizer = FlagsList:get("FFlagRefactorDevFrameworkTheme") and ContextServices.Stylizer or nil, + Theme = (not FlagsList:get("FFlagRefactorDevFrameworkTheme")) and ContextServices.Theme or nil, }) return LoadingBar diff --git a/BuiltInPlugins/LocalizationTools/Packages/Framework/UI/LoadingBar/example.lua b/BuiltInPlugins/LocalizationTools/Packages/Framework/UI/LoadingBar/example.lua index a01c4950f7..2fde53e3ce 100644 --- a/BuiltInPlugins/LocalizationTools/Packages/Framework/UI/LoadingBar/example.lua +++ b/BuiltInPlugins/LocalizationTools/Packages/Framework/UI/LoadingBar/example.lua @@ -15,14 +15,26 @@ return function(plugin) local Decoration = UI.Decoration local Button = UI.Button + local StudioTheme = require(Framework.Style.Themes.StudioTheme) + + local Util = require(Framework.Util) + local FlagsList = Util.Flags.new({ + FFlagRefactorDevFrameworkTheme = {"RefactorDevFrameworkTheme"}, + }) + local pluginItem = Plugin.new(plugin) - local theme = Theme.new(function(theme, getColor) - local studioStyles = StudioFrameworkStyles.new(theme, getColor) - return { - Framework = studioStyles, - } - end) + local theme + if FlagsList:get("FFlagRefactorDevFrameworkTheme") then + theme = StudioTheme.new() + else + theme = Theme.new(function(theme, getColor) + local studioStyles = StudioFrameworkStyles.new(theme, getColor) + return { + Framework = studioStyles, + } + end) + end -- Mount and display a dialog local ExampleLoadingBar = Roact.PureComponent:extend("ExampleLoadingBar") @@ -64,6 +76,11 @@ return function(plugin) local progressText = ("%i%%"):format(progress * 100) + local textColor + if (not FlagsList:get("FFlagRefactorDevFrameworkTheme")) then + textColor = theme:get("Framework").Button.Default.TextColor + end + return ContextServices.provide({pluginItem, theme}, { Main = Roact.createElement(Dialog, { Enabled = enabled, @@ -99,7 +116,7 @@ return function(plugin) Size = UDim2.fromOffset(120, 16), BackgroundTransparency = 1, Text = progressText, - TextColor3 = theme:get("Framework").Button.Default.TextColor, + TextColor3 = textColor, Font = Enum.Font.SourceSans, TextSize = 16, }), diff --git a/BuiltInPlugins/LocalizationTools/Packages/Framework/UI/LoadingBar/style.lua b/BuiltInPlugins/LocalizationTools/Packages/Framework/UI/LoadingBar/style.lua index 9159121f37..38307c9f06 100644 --- a/BuiltInPlugins/LocalizationTools/Packages/Framework/UI/LoadingBar/style.lua +++ b/BuiltInPlugins/LocalizationTools/Packages/Framework/UI/LoadingBar/style.lua @@ -1,7 +1,14 @@ local Framework = script.Parent.Parent.Parent +local StyleKey = require(Framework.Style.StyleKey) + local Util = require(Framework.Util) +local Cryo = Util.Cryo +local deepCopy = Util.deepCopy local Style = Util.Style +local FlagsList = Util.Flags.new({ + FFlagRefactorDevFrameworkTheme = {"RefactorDevFrameworkTheme"}, +}) local UI = require(Framework.UI) local Decoration = UI.Decoration @@ -9,21 +16,35 @@ local Decoration = UI.Decoration local UIFolderData = require(Framework.UI.UIFolderData) local RoundBox = require(UIFolderData.RoundBox.style) -return function(theme, getColor) - local roundBox = RoundBox(theme, getColor) - - local Default = Style.new({ +if FlagsList:get("FFlagRefactorDevFrameworkTheme") then + local roundBox = deepCopy(RoundBox) + return { Background = Decoration.RoundBox, Foreground = Decoration.RoundBox, - BackgroundStyle = Style.extend(roundBox.Default, { - Color = theme:GetColor("Button"), + BackgroundStyle = Cryo.Dictionary.join(roundBox, { + Color = StyleKey.Button, }), - ForegroundStyle = Style.extend(roundBox.Default, { - Color = theme:GetColor("DialogMainButton", "Selected"), + ForegroundStyle = Cryo.Dictionary.join(roundBox, { + Color = StyleKey.DialogMainButtonSelected, }), - }) - - return { - Default = Default, } -end +else + return function(theme, getColor) + local roundBox = RoundBox(theme, getColor) + + local Default = Style.new({ + Background = Decoration.RoundBox, + Foreground = Decoration.RoundBox, + BackgroundStyle = Style.extend(roundBox.Default, { + Color = theme:GetColor("Button"), + }), + ForegroundStyle = Style.extend(roundBox.Default, { + Color = theme:GetColor("DialogMainButton", "Selected"), + }), + }) + + return { + Default = Default, + } + end +end \ No newline at end of file diff --git a/BuiltInPlugins/LocalizationTools/Packages/Framework/UI/LoadingBar/test.spec.lua b/BuiltInPlugins/LocalizationTools/Packages/Framework/UI/LoadingBar/test.spec.lua index b2e7fed4ab..c898cde0f4 100644 --- a/BuiltInPlugins/LocalizationTools/Packages/Framework/UI/LoadingBar/test.spec.lua +++ b/BuiltInPlugins/LocalizationTools/Packages/Framework/UI/LoadingBar/test.spec.lua @@ -8,12 +8,24 @@ return function() local LoadingBar = require(script.Parent) local Box = require(Framework.UI.Box) + local StudioTheme = require(Framework.Style.Themes.StudioTheme) + + local Util = require(Framework.Util) + local FlagsList = Util.Flags.new({ + FFlagRefactorDevFrameworkTheme = {"RefactorDevFrameworkTheme"}, + }) + local function createTestLoadingBar(props, children) - local theme = Theme.new(function() - return { - Framework = FrameworkStyles.new(), - } - end) + local theme + if FlagsList:get("FFlagRefactorDevFrameworkTheme") then + theme = StudioTheme.mock() + else + theme = Theme.new(function() + return { + Framework = FrameworkStyles.new(), + } + end) + end return provide({theme}, { LoadingBar = Roact.createElement(LoadingBar, props, children), }) diff --git a/BuiltInPlugins/LocalizationTools/Packages/Framework/UI/LoadingIndicator.lua b/BuiltInPlugins/LocalizationTools/Packages/Framework/UI/LoadingIndicator.lua index 38135ca2fb..61d4c063d9 100644 --- a/BuiltInPlugins/LocalizationTools/Packages/Framework/UI/LoadingIndicator.lua +++ b/BuiltInPlugins/LocalizationTools/Packages/Framework/UI/LoadingIndicator.lua @@ -1,10 +1,8 @@ --[[ Loading Indicator of 3 rectangles which cyclically increase and then decrease in height while changing color. - Required Props: - Theme Theme: A Theme ContextItem, which is provided via mapToProps. - Optional Props: + Theme Theme: A Theme ContextItem, which is provided via mapToProps. Vector2 AnchorPoint: an offset for positioning number LayoutOrder: The layout order of this component in a UILayout. UDim2 Position: The position of the component. Defaults to zero. @@ -12,18 +10,23 @@ Style Style: The style with which to render this component. StyleModifier StyleModifier: The StyleModifier index into Style. number ZIndex: The render index of this component. + Stylizer Stylizer: A Stylizer ContextItem, which is provided via mapToProps. Style Values: Color3 StartColor: The starting color of the blocks. Color3 EndColor: The color of the blocks as they are at their maximum height. ]] - local RunService = game:GetService("RunService") local Framework = script.Parent.Parent local Roact = require(Framework.Parent.Roact) local ContextServices = require(Framework.ContextServices) -local Typecheck = require(Framework.Util).Typecheck + +local Util = require(Framework.Util) +local Typecheck = Util.Typecheck +local FlagsList = Util.Flags.new({ + FFlagRefactorDevFrameworkTheme = {"RefactorDevFrameworkTheme"}, +}) local LoadingIndicator = Roact.PureComponent:extend("LoadingIndicator") Typecheck.wrap(LoadingIndicator, script) @@ -95,7 +98,12 @@ function LoadingIndicator:render() local layoutOrder = props.LayoutOrder local theme = props.Theme - local style = theme:getStyle("Framework", self) + local style + if FlagsList:get("FFlagRefactorDevFrameworkTheme") then + style = props.Stylizer + else + style = theme:getStyle("Framework", self) + end local startColor = style.StartColor local endColor = style.EndColor @@ -142,7 +150,8 @@ function LoadingIndicator:render() end ContextServices.mapToProps(LoadingIndicator, { - Theme = ContextServices.Theme, + Stylizer = FlagsList:get("FFlagRefactorDevFrameworkTheme") and ContextServices.Stylizer or nil, + Theme = (not FlagsList:get("FFlagRefactorDevFrameworkTheme")) and ContextServices.Theme or nil, }) return LoadingIndicator diff --git a/BuiltInPlugins/LocalizationTools/Packages/Framework/UI/LoadingIndicator/style.lua b/BuiltInPlugins/LocalizationTools/Packages/Framework/UI/LoadingIndicator/style.lua index feedb15ead..d0ec23c9c3 100644 --- a/BuiltInPlugins/LocalizationTools/Packages/Framework/UI/LoadingIndicator/style.lua +++ b/BuiltInPlugins/LocalizationTools/Packages/Framework/UI/LoadingIndicator/style.lua @@ -1,16 +1,27 @@ local Framework = script.Parent.Parent.Parent +local StyleKey = require(Framework.Style.StyleKey) + local Util = require(Framework.Util) local Style = Util.Style +local FlagsList = Util.Flags.new({ + FFlagRefactorDevFrameworkTheme = {"RefactorDevFrameworkTheme"}, +}) -return function(theme, getColor) - - local Default = Style.new({ - StartColor = theme:GetColor("DimmedText"), - EndColor = theme:GetColor("DialogMainButton", "Selected") - }) - +if FlagsList:get("FFlagRefactorDevFrameworkTheme") then return { - Default = Default, + StartColor = StyleKey.DimmedText, + EndColor = StyleKey.DialogMainButtonSelected, } -end +else + return function(theme, getColor) + local Default = Style.new({ + StartColor = theme:GetColor("DimmedText"), + EndColor = theme:GetColor("DialogMainButton", "Selected") + }) + + return { + Default = Default, + } + end +end \ No newline at end of file diff --git a/BuiltInPlugins/LocalizationTools/Packages/Framework/UI/RadioButton.lua b/BuiltInPlugins/LocalizationTools/Packages/Framework/UI/RadioButton.lua index 11680f3214..14ccde4cfe 100644 --- a/BuiltInPlugins/LocalizationTools/Packages/Framework/UI/RadioButton.lua +++ b/BuiltInPlugins/LocalizationTools/Packages/Framework/UI/RadioButton.lua @@ -4,15 +4,16 @@ Required Props: string Key: The key that will be sent back to the OnClick function. string Text: The text to display. - Theme Theme: A Theme ContextItem, which is provided via mapToProps. Optional Props: boolean Disabled: Whether or not the radio button is disabled. OnClick will not work when disabled. number LayoutOrder: The layout order of the frame. callback OnClick: paramters(string key). Fires when the button is activated and returns back the Key. boolean Selected: Whether or not the radio button is selected. + Style Style: The style with which to render this component. + Theme Theme: A Theme ContextItem, which is provided via mapToProps. + Stylizer Stylizer: A Stylizer ContextItem, which is provided via mapToProps. ]] - local TextService = game:GetService("TextService") local Framework = script.Parent.Parent @@ -25,6 +26,9 @@ local TextLabel = require(Framework.UI.TextLabel) local Util = require(Framework.Util) local Typecheck = Util.Typecheck local StyleModifier = Util.StyleModifier +local FlagsList = Util.Flags.new({ + FFlagRefactorDevFrameworkTheme = {"RefactorDevFrameworkTheme"}, +}) local RadioButton = Roact.PureComponent:extend("RadioButton") Typecheck.wrap(RadioButton, script) @@ -55,7 +59,13 @@ function RadioButton:render() local text = self.props.Text local theme = self.props.Theme - local style = theme:getStyle("Framework", self) + local style + if FlagsList:get("FFlagRefactorDevFrameworkTheme") then + style = self.props.Stylizer + else + style = theme:getStyle("Framework", self) + end + local font = style.Font local textSize = style.TextSize local imageSize = style.ImageSize @@ -116,7 +126,8 @@ function RadioButton:render() end ContextServices.mapToProps(RadioButton, { - Theme = ContextServices.Theme, + Stylizer = FlagsList:get("FFlagRefactorDevFrameworkTheme") and ContextServices.Stylizer or nil, + Theme = (not FlagsList:get("FFlagRefactorDevFrameworkTheme")) and ContextServices.Theme or nil, }) return RadioButton \ No newline at end of file diff --git a/BuiltInPlugins/LocalizationTools/Packages/Framework/UI/RadioButton/example.lua b/BuiltInPlugins/LocalizationTools/Packages/Framework/UI/RadioButton/example.lua index e9ad922971..d9476065bf 100644 --- a/BuiltInPlugins/LocalizationTools/Packages/Framework/UI/RadioButton/example.lua +++ b/BuiltInPlugins/LocalizationTools/Packages/Framework/UI/RadioButton/example.lua @@ -15,14 +15,26 @@ return function(plugin) local StudioUI = require(Framework.StudioUI) local StudioFrameworkStyles = StudioUI.StudioFrameworkStyles + local StudioTheme = require(Framework.Style.Themes.StudioTheme) + + local Util = require(Framework.Util) + local FlagsList = Util.Flags.new({ + FFlagRefactorDevFrameworkTheme = {"RefactorDevFrameworkTheme"}, + }) + local pluginItem = Plugin.new(plugin) local mouse = Mouse.new(plugin:GetMouse()) - local theme = Theme.new(function(theme, getColor) - return { - Framework = StudioFrameworkStyles.new(theme, getColor) - } - end) + local theme + if FlagsList:get("FFlagRefactorDevFrameworkTheme") then + theme = StudioTheme.new() + else + theme = Theme.new(function(theme, getColor) + return { + Framework = StudioFrameworkStyles.new(theme, getColor) + } + end) + end -- Mount and display a dialog local ExampleContainer = Roact.PureComponent:extend("ExampleContainer") diff --git a/BuiltInPlugins/LocalizationTools/Packages/Framework/UI/RadioButton/style.lua b/BuiltInPlugins/LocalizationTools/Packages/Framework/UI/RadioButton/style.lua index c4af3f83f7..e5b964d761 100644 --- a/BuiltInPlugins/LocalizationTools/Packages/Framework/UI/RadioButton/style.lua +++ b/BuiltInPlugins/LocalizationTools/Packages/Framework/UI/RadioButton/style.lua @@ -1,8 +1,13 @@ local Framework = script.Parent.Parent.Parent +local StyleKey = require(Framework.Style.StyleKey) + local Util = require(Framework.Util) local Style = Util.Style local StyleModifier = Util.StyleModifier +local FlagsList = Util.Flags.new({ + FFlagRefactorDevFrameworkTheme = {"RefactorDevFrameworkTheme"}, +}) local UI = require(Framework.UI) local Decoration = UI.Decoration @@ -10,10 +15,8 @@ local Decoration = UI.Decoration local StudioFrameworkStyles = Framework.StudioUI.StudioFrameworkStyles local Common = require(StudioFrameworkStyles.Common) -return function(theme, getColor) - local common = Common(theme, getColor) - - local Default = Style.extend(common.MainText, { +if FlagsList:get("FFlagRefactorDevFrameworkTheme") then + return { Padding = 6, ImageSize = UDim2.new(0, 20, 0, 20), Background = UI.Button, @@ -21,31 +24,67 @@ return function(theme, getColor) Background = Decoration.Image, BackgroundStyle = { Image = "rbxasset://textures/GameSettings/RadioButton.png", - Color = theme:GetColor("MainBackground"), + Color = StyleKey.MainBackground, }, [StyleModifier.Selected] = { BackgroundStyle = { Image = "rbxasset://textures/GameSettings/RadioButton.png", - Color = theme:GetColor("MainBackground"), + Color = StyleKey.MainBackground, }, Foreground = Decoration.Image, ForegroundStyle = { AnchorPoint = Vector2.new(0.5, 0.5), Image = "rbxasset://textures/ui/LuaApp/icons/ic-blue-dot.png", Position = UDim2.new(0.5, 0, 0.5, 0), - Size = UDim2.new(0.4, 0, 0.4, 0) + Size = UDim2.new(0.4, 0, 0.4, 0), }, }, [StyleModifier.Disabled] = { BackgroundStyle = { Image = "rbxasset://textures/GameSettings/RadioButton.png", - Color = theme:GetColor("MainBackground"), + Color = StyleKey.MainBackground, }, }, }, - }) - - return { - Default = Default, } -end +else + return function(theme, getColor) + local common = Common(theme, getColor) + + local Default = Style.extend(common.MainText, { + Padding = 6, + ImageSize = UDim2.new(0, 20, 0, 20), + Background = UI.Button, + BackgroundStyle = { + Background = Decoration.Image, + BackgroundStyle = { + Image = "rbxasset://textures/GameSettings/RadioButton.png", + Color = theme:GetColor("MainBackground"), + }, + [StyleModifier.Selected] = { + BackgroundStyle = { + Image = "rbxasset://textures/GameSettings/RadioButton.png", + Color = theme:GetColor("MainBackground"), + }, + Foreground = Decoration.Image, + ForegroundStyle = { + AnchorPoint = Vector2.new(0.5, 0.5), + Image = "rbxasset://textures/ui/LuaApp/icons/ic-blue-dot.png", + Position = UDim2.new(0.5, 0, 0.5, 0), + Size = UDim2.new(0.4, 0, 0.4, 0) + }, + }, + [StyleModifier.Disabled] = { + BackgroundStyle = { + Image = "rbxasset://textures/GameSettings/RadioButton.png", + Color = theme:GetColor("MainBackground"), + }, + }, + }, + }) + + return { + Default = Default, + } + end +end \ No newline at end of file diff --git a/BuiltInPlugins/LocalizationTools/Packages/Framework/UI/RadioButton/test.spec.lua b/BuiltInPlugins/LocalizationTools/Packages/Framework/UI/RadioButton/test.spec.lua index 9fc369ebcb..4e578229a9 100644 --- a/BuiltInPlugins/LocalizationTools/Packages/Framework/UI/RadioButton/test.spec.lua +++ b/BuiltInPlugins/LocalizationTools/Packages/Framework/UI/RadioButton/test.spec.lua @@ -8,17 +8,29 @@ return function() local RadioButton = require(script.Parent) local Immutable = require(Framework.Util.Immutable) + local StudioTheme = require(Framework.Style.Themes.StudioTheme) + + local Util = require(Framework.Util) + local FlagsList = Util.Flags.new({ + FFlagRefactorDevFrameworkTheme = {"RefactorDevFrameworkTheme"}, + }) + local DEFAULT_PROPS = { Key = "", Text = "", } local function createTestRadioButton(props, children) - local theme = Theme.new(function() - return { - Framework = FrameworkStyles.new(), - } - end) + local theme + if FlagsList:get("FFlagRefactorDevFrameworkTheme") then + theme = StudioTheme.mock() + else + theme = Theme.new(function() + return { + Framework = FrameworkStyles.new(), + } + end) + end local combinedProps if props then combinedProps = Immutable.JoinDictionaries(DEFAULT_PROPS, props) diff --git a/BuiltInPlugins/LocalizationTools/Packages/Framework/UI/RadioButtonList.lua b/BuiltInPlugins/LocalizationTools/Packages/Framework/UI/RadioButtonList.lua index ec577cea84..55a45e7104 100644 --- a/BuiltInPlugins/LocalizationTools/Packages/Framework/UI/RadioButtonList.lua +++ b/BuiltInPlugins/LocalizationTools/Packages/Framework/UI/RadioButtonList.lua @@ -4,21 +4,24 @@ Required Props: table Buttons: A list of buttons to display. Example: { Key = "", Text = "", Disabled = false }. - Theme Theme: A Theme ContextItem, which is provided via mapToProps. Optional Props: string SelectedKey: The initially selected key. number LayoutOrder: The layout order of the frame. Enum.FillDirection FillDirection: The direction in which buttons are filled. callback OnClick: paramters(string key). Fires when the button is activated and returns back the Key. + Theme Theme: A Theme ContextItem, which is provided via mapToProps. + Stylizer Stylizer: A Stylizer ContextItem, which is provided via mapToProps. ]] - local Framework = script.Parent.Parent local Roact = require(Framework.Parent.Roact) local ContextServices = require(Framework.ContextServices) local Util = require(Framework.Util) local Typecheck = Util.Typecheck +local FlagsList = Util.Flags.new({ + FFlagRefactorDevFrameworkTheme = {"RefactorDevFrameworkTheme"}, +}) local RadioButton = require(Framework.UI.RadioButton) @@ -57,7 +60,12 @@ function RadioButtonList:render() local layoutOrder = self.props.LayoutOrder local theme = self.props.Theme - local style = theme:getStyle("Framework", self) + local style + if FlagsList:get("FFlagRefactorDevFrameworkTheme") then + style = self.props.Stylizer + else + style = theme:getStyle("Framework", self) + end local children = {} @@ -82,7 +90,8 @@ function RadioButtonList:render() end ContextServices.mapToProps(RadioButtonList, { - Theme = ContextServices.Theme, + Stylizer = FlagsList:get("FFlagRefactorDevFrameworkTheme") and ContextServices.Stylizer or nil, + Theme = (not FlagsList:get("FFlagRefactorDevFrameworkTheme")) and ContextServices.Theme or nil, }) return RadioButtonList diff --git a/BuiltInPlugins/LocalizationTools/Packages/Framework/UI/RadioButtonList/example.lua b/BuiltInPlugins/LocalizationTools/Packages/Framework/UI/RadioButtonList/example.lua index 39b9d2b71e..45734e7da8 100644 --- a/BuiltInPlugins/LocalizationTools/Packages/Framework/UI/RadioButtonList/example.lua +++ b/BuiltInPlugins/LocalizationTools/Packages/Framework/UI/RadioButtonList/example.lua @@ -15,14 +15,26 @@ return function(plugin) local StudioUI = require(Framework.StudioUI) local StudioFrameworkStyles = StudioUI.StudioFrameworkStyles + local Util = require(Framework.Util) + local FlagsList = Util.Flags.new({ + FFlagRefactorDevFrameworkTheme = {"RefactorDevFrameworkTheme"}, + }) + + local StudioTheme = require(Framework.Style.Themes.StudioTheme) + local pluginItem = Plugin.new(plugin) local mouse = Mouse.new(plugin:GetMouse()) - local theme = Theme.new(function(theme, getColor) - return { - Framework = StudioFrameworkStyles.new(theme, getColor) - } - end) + local theme + if FlagsList:get("FFlagRefactorDevFrameworkTheme") then + theme = StudioTheme.new() + else + theme = Theme.new(function(theme, getColor) + return { + Framework = StudioFrameworkStyles.new(theme, getColor) + } + end) + end -- Mount and display a dialog local ExampleContainer = Roact.PureComponent:extend("ExampleContainer") diff --git a/BuiltInPlugins/LocalizationTools/Packages/Framework/UI/RadioButtonList/style.lua b/BuiltInPlugins/LocalizationTools/Packages/Framework/UI/RadioButtonList/style.lua index eb87ca16ec..88be1c8387 100644 --- a/BuiltInPlugins/LocalizationTools/Packages/Framework/UI/RadioButtonList/style.lua +++ b/BuiltInPlugins/LocalizationTools/Packages/Framework/UI/RadioButtonList/style.lua @@ -2,18 +2,27 @@ local Framework = script.Parent.Parent.Parent local Util = require(Framework.Util) local Style = Util.Style +local FlagsList = Util.Flags.new({ + FFlagRefactorDevFrameworkTheme = {"RefactorDevFrameworkTheme"}, +}) local StudioFrameworkStyles = Framework.StudioUI.StudioFrameworkStyles local Common = require(StudioFrameworkStyles.Common) -return function(theme, getColor) - local common = Common(theme, getColor) - - local Default = Style.extend(common.MainText, { - Padding = 6, - }) - +if FlagsList:get("FFlagRefactorDevFrameworkTheme") then return { - Default = Default, + Padding = 6, } +else + return function(theme, getColor) + local common = Common(theme, getColor) + + local Default = Style.extend(common.MainText, { + Padding = 6, + }) + + return { + Default = Default, + } + end end diff --git a/BuiltInPlugins/LocalizationTools/Packages/Framework/UI/RangeSlider.lua b/BuiltInPlugins/LocalizationTools/Packages/Framework/UI/RangeSlider.lua index ef2d1ba494..cd601a0e07 100644 --- a/BuiltInPlugins/LocalizationTools/Packages/Framework/UI/RangeSlider.lua +++ b/BuiltInPlugins/LocalizationTools/Packages/Framework/UI/RangeSlider.lua @@ -8,7 +8,6 @@ number UpperRangeValue: Current value for the upper range knob callback OnValuesChanged: The callback is called whenever the min or max value changes - OnValuesChanged(minValue: number, maxValue: number) Mouse Mouse: A Mouse ContextItem, which is provided via mapToProps. - Theme Theme: A Theme ContextItem, which is provided via mapToProps. Optional Props: Vector2 AnchorPoint: The anchorPoint of the component @@ -22,8 +21,9 @@ StyleModifier StyleModifier: The StyleModifier index into Style. number SnapIncrement: Incremental points that the slider's knob will snap to. A "0" snap increment means no snapping. number VerticalDragTolerance: A vertical pixel height for allowing a mouse button press to drag knobs on outside the component's size. + Theme Theme: A Theme ContextItem, which is provided via mapToProps. + Stylizer Stylizer: A Stylizer ContextItem, which is provided via mapToProps. ]] - local Framework = script.Parent.Parent local Roact = require(Framework.Parent.Roact) local ContextServices = require(Framework.ContextServices) @@ -31,6 +31,9 @@ local ContextServices = require(Framework.ContextServices) local Util = require(Framework.Util) local Typecheck = Util.Typecheck local StyleModifier = Util.StyleModifier +local FlagsList = Util.Flags.new({ + FFlagRefactorDevFrameworkTheme = {"RefactorDevFrameworkTheme"}, +}) local UI = Framework.UI local Container = require(UI.Container) @@ -159,7 +162,12 @@ end function RangeSlider:render() local theme = self.props.Theme - local style = theme:getStyle("Framework", self) + local style + if FlagsList:get("FFlagRefactorDevFrameworkTheme") then + style = self.props.Stylizer + else + style = theme:getStyle("Framework", self) + end local anchorPoint = self.props.AnchorPoint local isDisabled = self.props.Disabled @@ -252,7 +260,8 @@ end ContextServices.mapToProps(RangeSlider, { Mouse = ContextServices.Mouse, - Theme = ContextServices.Theme, + Stylizer = FlagsList:get("FFlagRefactorDevFrameworkTheme") and ContextServices.Stylizer or nil, + Theme = (not FlagsList:get("FFlagRefactorDevFrameworkTheme")) and ContextServices.Theme or nil, }) return RangeSlider \ No newline at end of file diff --git a/BuiltInPlugins/LocalizationTools/Packages/Framework/UI/RangeSlider/example.lua b/BuiltInPlugins/LocalizationTools/Packages/Framework/UI/RangeSlider/example.lua index 03f35a00e0..061dbb97a2 100644 --- a/BuiltInPlugins/LocalizationTools/Packages/Framework/UI/RangeSlider/example.lua +++ b/BuiltInPlugins/LocalizationTools/Packages/Framework/UI/RangeSlider/example.lua @@ -19,6 +19,13 @@ return function(plugin) local ExampleRangeSlider = Roact.PureComponent:extend("ExampleRangeSlider") + local Util = require(Framework.Util) + local FlagsList = Util.Flags.new({ + FFlagRefactorDevFrameworkTheme = {"RefactorDevFrameworkTheme"}, + }) + + local StudioTheme = require(Framework.Style.Themes.StudioTheme) + local pluginItem = Plugin.new(plugin) local mouse = Mouse.new(plugin:GetMouse()) @@ -38,11 +45,16 @@ return function(plugin) }) end - self.theme = Theme.new(function(theme, getColor) - return { - Framework = StudioFrameworkStyles.new(theme, getColor) - } - end) + self.theme = nil + if FlagsList:get("FFlagRefactorDevFrameworkTheme") then + self.theme = StudioTheme.new() + else + self.theme = Theme.new(function(theme, getColor) + return { + Framework = StudioFrameworkStyles.new(theme, getColor) + } + end) + end self.setValues = function(lowerValue, upperValue) self:setState({ @@ -95,13 +107,14 @@ return function(plugin) UpperRangeValue = 3, Min = MIN_VALUE, Max = MAX_VALUE, + OnValuesChanged = function() end, Position = UDim2.new(0.5, 0, 0.5, 0), Size = UDim2.new(0, 200, 0, 20), }), RangeSliderNoLower = Roact.createElement(RangeSlider, { AnchorPoint = Vector2.new(0.5, 0.5), Disabled = false, - HideLower = true, + HideLowerKnob = true, LowerRangeValue = self.state.currentMin, UpperRangeValue = self.state.currentMax, Min = MIN_VALUE, diff --git a/BuiltInPlugins/LocalizationTools/Packages/Framework/UI/RangeSlider/style.lua b/BuiltInPlugins/LocalizationTools/Packages/Framework/UI/RangeSlider/style.lua index b7167886d3..535d967e6b 100644 --- a/BuiltInPlugins/LocalizationTools/Packages/Framework/UI/RangeSlider/style.lua +++ b/BuiltInPlugins/LocalizationTools/Packages/Framework/UI/RangeSlider/style.lua @@ -1,9 +1,14 @@ local Framework = script.Parent.Parent.Parent +local StyleKey = require(Framework.Style.StyleKey) + local Util = require(Framework.Util) local Style = Util.Style local StyleModifier = Util.StyleModifier local StyleValue = Util.StyleValue +local FlagsList = Util.Flags.new({ + FFlagRefactorDevFrameworkTheme = {"RefactorDevFrameworkTheme"}, +}) local UI = require(Framework.UI) local Decoration = UI.Decoration @@ -15,65 +20,108 @@ local BAR_HEIGHT = 6 local BAR_SLICE_CENTER = Rect.new(3, 0, 4, 6) local SLIDER_HANDLE_SIZE = 18 -return function(theme, getColor) - local common = Common(theme, getColor) - - local BackgroundColor = StyleValue.new("BackgroundColor", { - Light = Color3.fromRGB(204, 204, 204), - Dark = Color3.fromRGB(37, 37, 37), - }) - - local KnobColor = StyleValue.new("KnobColor", { - Light = Color3.fromRGB(255, 255, 255), - Dark = Color3.fromRGB(85, 85, 85), - }) - - local KnobImage = StyleValue.new("KnobImage", { - Light = "rbxasset://textures/DeveloperFramework/slider_knob_light.png", - Dark = "rbxasset://textures/DeveloperFramework/slider_knob.png", - }) +if FlagsList:get("FFlagRefactorDevFrameworkTheme") then local knobStyle = { AnchorPoint = Vector2.new(0.5, 0.5), - Color = KnobColor:get(theme.Name), - Image = KnobImage:get(theme.Name), + Color = StyleKey.SliderKnobColor, + Image = StyleKey.SliderKnobImage, Size = UDim2.new(0, SLIDER_HANDLE_SIZE, 0, SLIDER_HANDLE_SIZE), [StyleModifier.Disabled] = { - Color = theme:GetColor("Button"), + Color = StyleKey.Button, }, } - local Default = Style.extend(common.MainText, { + return { KnobSize = Vector2.new(18, 18), Background = Decoration.Image, BackgroundStyle = { - AnchorPoint = Vector2.new(0, 0.5), - Color = BackgroundColor:get(theme.Name), + AnchorPoint = Vector2.new(0, 0.5), + Color = StyleKey.SliderBackground, Image = "rbxasset://textures/DeveloperFramework/slider_bg.png", - Position = UDim2.new(0, 0, 0.5, 0), - ScaleType = Enum.ScaleType.Slice, - Size = UDim2.new(UDim.new(1, 0), UDim.new(0, BAR_HEIGHT)), - SliceCenter = BAR_SLICE_CENTER, + Position = UDim2.new(0, 0, 0.5, 0), + ScaleType = Enum.ScaleType.Slice, + Size = UDim2.new(UDim.new(1, 0), UDim.new(0, BAR_HEIGHT)), + SliceCenter = BAR_SLICE_CENTER, }, Foreground = Decoration.Image, ForegroundStyle = { AnchorPoint = Vector2.new(0, 0.5), Image = "rbxasset://textures/DeveloperFramework/slider_bg.png", - Color = theme:GetColor("DialogMainButton"), + Color = StyleKey.DialogMainButton, ScaleType = Enum.ScaleType.Slice, Size = UDim2.new(UDim.new(1, 0), UDim.new(0, BAR_HEIGHT)), SliceCenter = BAR_SLICE_CENTER, [StyleModifier.Disabled] = { - Color = theme:GetColor("Button"), + Color = StyleKey.Button, }, }, LowerKnobBackground = Decoration.Image, LowerKnobBackgroundStyle = knobStyle, UpperKnobBackground = Decoration.Image, UpperKnobBackgroundStyle = knobStyle, - }) - - return { - Default = Default, } -end +else + return function(theme, getColor) + local common = Common(theme, getColor) + + local BackgroundColor = StyleValue.new("BackgroundColor", { + Light = Color3.fromRGB(204, 204, 204), + Dark = Color3.fromRGB(37, 37, 37), + }) + + local KnobColor = StyleValue.new("KnobColor", { + Light = Color3.fromRGB(255, 255, 255), + Dark = Color3.fromRGB(85, 85, 85), + }) + + local KnobImage = StyleValue.new("KnobImage", { + Light = "rbxasset://textures/DeveloperFramework/slider_knob_light.png", + Dark = "rbxasset://textures/DeveloperFramework/slider_knob.png", + }) + + local knobStyle = { + AnchorPoint = Vector2.new(0.5, 0.5), + Color = KnobColor:get(theme.Name), + Image = KnobImage:get(theme.Name), + Size = UDim2.new(0, SLIDER_HANDLE_SIZE, 0, SLIDER_HANDLE_SIZE), + [StyleModifier.Disabled] = { + Color = theme:GetColor("Button"), + }, + } + + local Default = Style.extend(common.MainText, { + KnobSize = Vector2.new(18, 18), + Background = Decoration.Image, + BackgroundStyle = { + AnchorPoint = Vector2.new(0, 0.5), + Color = BackgroundColor:get(theme.Name), + Image = "rbxasset://textures/DeveloperFramework/slider_bg.png", + Position = UDim2.new(0, 0, 0.5, 0), + ScaleType = Enum.ScaleType.Slice, + Size = UDim2.new(UDim.new(1, 0), UDim.new(0, BAR_HEIGHT)), + SliceCenter = BAR_SLICE_CENTER, + }, + Foreground = Decoration.Image, + ForegroundStyle = { + AnchorPoint = Vector2.new(0, 0.5), + Image = "rbxasset://textures/DeveloperFramework/slider_bg.png", + Color = theme:GetColor("DialogMainButton"), + ScaleType = Enum.ScaleType.Slice, + Size = UDim2.new(UDim.new(1, 0), UDim.new(0, BAR_HEIGHT)), + SliceCenter = BAR_SLICE_CENTER, + [StyleModifier.Disabled] = { + Color = theme:GetColor("Button"), + }, + }, + LowerKnobBackground = Decoration.Image, + LowerKnobBackgroundStyle = knobStyle, + UpperKnobBackground = Decoration.Image, + UpperKnobBackgroundStyle = knobStyle, + }) + + return { + Default = Default, + } + end +end \ No newline at end of file diff --git a/BuiltInPlugins/LocalizationTools/Packages/Framework/UI/RoundBox.lua b/BuiltInPlugins/LocalizationTools/Packages/Framework/UI/RoundBox.lua index dae4417961..ffff6a790a 100644 --- a/BuiltInPlugins/LocalizationTools/Packages/Framework/UI/RoundBox.lua +++ b/BuiltInPlugins/LocalizationTools/Packages/Framework/UI/RoundBox.lua @@ -1,12 +1,11 @@ --[[ A round Box decoration with a border. - Required Props: - Theme Theme: A Theme ContextItem, which is provided via mapToProps. - Optional Props: Style Style: The style with which to render this component. StyleModifier StyleModifier: The StyleModifier index into Style. + Theme Theme: A Theme ContextItem, which is provided via mapToProps. + Stylizer Stylizer: A Stylizer ContextItem, which is provided via mapToProps. Style Values: Color3 Color: The color tint of the image. @@ -22,9 +21,12 @@ local Framework = script.Parent.Parent local Roact = require(Framework.Parent.Roact) local ContextServices = require(Framework.ContextServices) -local Typecheck = require(Framework.Util).Typecheck -local FFlagRoundBoxZIndexProp = game:DefineFastFlag("RoundBoxZIndexProp", false) +local Util = require(Framework.Util) +local Typecheck = Util.Typecheck +local FlagsList = Util.Flags.new({ + FFlagRefactorDevFrameworkTheme = {"RefactorDevFrameworkTheme"}, +}) local RoundBox = Roact.PureComponent:extend("RoundBox") Typecheck.wrap(RoundBox, script) @@ -32,7 +34,12 @@ Typecheck.wrap(RoundBox, script) function RoundBox:render() local props = self.props local theme = props.Theme - local style = theme:getStyle("Framework", self) + local style + if FlagsList:get("FFlagRefactorDevFrameworkTheme") then + style = props.Stylizer + else + style = theme:getStyle("Framework", self) + end local color = style.Color local borderColor = style.BorderColor @@ -52,7 +59,7 @@ function RoundBox:render() Image = backgroundImage, ScaleType = Enum.ScaleType.Slice, SliceCenter = sliceCenter, - ZIndex = FFlagRoundBoxZIndexProp and zIndex or nil + ZIndex = zIndex }, { Border = Roact.createElement("ImageLabel", { Size = UDim2.new(1, 0, 1, 0), @@ -68,7 +75,8 @@ function RoundBox:render() end ContextServices.mapToProps(RoundBox, { - Theme = ContextServices.Theme, + Stylizer = FlagsList:get("FFlagRefactorDevFrameworkTheme") and ContextServices.Stylizer or nil, + Theme = (not FlagsList:get("FFlagRefactorDevFrameworkTheme")) and ContextServices.Theme or nil, }) return RoundBox diff --git a/BuiltInPlugins/LocalizationTools/Packages/Framework/UI/RoundBox/style.lua b/BuiltInPlugins/LocalizationTools/Packages/Framework/UI/RoundBox/style.lua index 0355f256cc..8faeba599a 100644 --- a/BuiltInPlugins/LocalizationTools/Packages/Framework/UI/RoundBox/style.lua +++ b/BuiltInPlugins/LocalizationTools/Packages/Framework/UI/RoundBox/style.lua @@ -1,23 +1,40 @@ local Framework = script.Parent.Parent.Parent +local StyleKey = require(Framework.Style.StyleKey) + local Util = require(Framework.Util) local Style = Util.Style +local FlagsList = Util.Flags.new({ + FFlagRefactorDevFrameworkTheme = {"RefactorDevFrameworkTheme"}, +}) local StudioFrameworkStyles = Framework.StudioUI.StudioFrameworkStyles local Common = require(StudioFrameworkStyles.Common) -return function(theme, getColor) - local common = Common(theme, getColor) - - local Default = Style.extend(common.Background, common.Border, { +if FlagsList:get("FFlagRefactorDevFrameworkTheme") then + return { + Color = StyleKey.MainBackground, + BorderColor = StyleKey.Border, Transparency = 0, BorderTransparency = 0, BackgroundImage = "rbxasset://textures/StudioToolbox/RoundedBackground.png", BorderImage = "rbxasset://textures/StudioToolbox/RoundedBorder.png", SliceCenter = Rect.new(3, 3, 13, 13), - }) - - return { - Default = Default, } -end +else + return function(theme, getColor) + local common = Common(theme, getColor) + + local Default = Style.extend(common.Background, common.Border, { + Transparency = 0, + BorderTransparency = 0, + BackgroundImage = "rbxasset://textures/StudioToolbox/RoundedBackground.png", + BorderImage = "rbxasset://textures/StudioToolbox/RoundedBorder.png", + SliceCenter = Rect.new(3, 3, 13, 13), + }) + + return { + Default = Default, + } + end +end \ No newline at end of file diff --git a/BuiltInPlugins/LocalizationTools/Packages/Framework/UI/RoundBox/test.spec.lua b/BuiltInPlugins/LocalizationTools/Packages/Framework/UI/RoundBox/test.spec.lua index 3110398a8d..d9715cf163 100644 --- a/BuiltInPlugins/LocalizationTools/Packages/Framework/UI/RoundBox/test.spec.lua +++ b/BuiltInPlugins/LocalizationTools/Packages/Framework/UI/RoundBox/test.spec.lua @@ -7,12 +7,24 @@ return function() local FrameworkStyles = require(Framework.UI.FrameworkStyles) local RoundBox = require(script.Parent) + local StudioTheme = require(Framework.Style.Themes.StudioTheme) + + local Util = require(Framework.Util) + local FlagsList = Util.Flags.new({ + FFlagRefactorDevFrameworkTheme = {"RefactorDevFrameworkTheme"}, + }) + local function createTestRoundBoxDecoration() - local theme = Theme.new(function() - return { - Framework = FrameworkStyles.new(), - } - end) + local theme + if FlagsList:get("FFlagRefactorDevFrameworkTheme") then + theme = StudioTheme.mock() + else + theme = Theme.new(function() + return { + Framework = FrameworkStyles.new(), + } + end) + end return provide({theme}, { RoundBox = Roact.createElement(RoundBox), }) diff --git a/BuiltInPlugins/LocalizationTools/Packages/Framework/UI/ScrollingFrame.lua b/BuiltInPlugins/LocalizationTools/Packages/Framework/UI/ScrollingFrame.lua index 58a2082545..6c62cf563e 100644 --- a/BuiltInPlugins/LocalizationTools/Packages/Framework/UI/ScrollingFrame.lua +++ b/BuiltInPlugins/LocalizationTools/Packages/Framework/UI/ScrollingFrame.lua @@ -2,11 +2,7 @@ A scrolling frame with a colored background, providing a consistent look with the native Studio Start Page. - Required Props: - Theme Theme: the theme supplied from mapToProps() - Optional Props: - Style Style: a style table supplied from props and theme:getStyle() callback OnScrollUpdate: A callback function that will update the index change. UDim2 Position: The position of the scrolling frame. UDim2 Size: The size of the scrolling frame. @@ -15,6 +11,9 @@ table AutoSizeLayoutOptions: The options of the UILayout instance if auto-sizing. UDim2 CanvasSize: The size of the scrolling frame's canvas. integer ElementPadding: The padding between children when AutoSizeCanvas is true. + Style Style: a style table supplied from props and theme:getStyle() + Stylizer Stylizer: A Stylizer ContextItem, which is provided via mapToProps + Theme Theme: the theme supplied from mapToProps() Style Values: string BottomImage: The image that appears in the bottom 3rd of the scrollbar @@ -26,12 +25,13 @@ boolean ScrollingEnabled: Whether scrolling in this frame will change the CanvasPosition. integer ZIndex: The draw index of the frame. ]] - local Framework = script.Parent.Parent local Roact = require(Framework.Parent.Roact) local Util = require(Framework.Util) local FlagsList = Util.Flags.new({ + FFlagRefactorDevFrameworkTheme = {"RefactorDevFrameworkTheme"}, FFlagStudioDevFrameworkPackage = {"StudioDevFrameworkPackage"}, + FFlagFixContentNotFullyShownAfterResize = {"FixContentNotFullyShownAfterResize"}, }) local Cryo local isUsedAsPackage = require(Framework.Util.isUsedAsPackage) @@ -51,6 +51,18 @@ local Typecheck = Util.Typecheck local ScrollingFrame = Roact.PureComponent:extend("ScrollingFrame") Typecheck.wrap(ScrollingFrame, script) +local function getStyle(self) + local props = self.props + local theme = props.Theme + local style + if FlagsList:get("FFlagRefactorDevFrameworkTheme") then + style = props.Stylizer + else + style = theme:getStyle("Framework", self) + end + return style +end + function ScrollingFrame:init() self.scrollingRef = Roact.createRef() self.layoutRef = Roact.createRef() @@ -64,7 +76,23 @@ function ScrollingFrame:init() self.updateCanvasSize = function(rbx) if self.scrollingRef.current and self.layoutRef.current then local contentSize = self.layoutRef.current.AbsoluteContentSize - self.scrollingRef.current.CanvasSize = UDim2.new(0, contentSize.X, 0, contentSize.Y) + local contentSizeX = contentSize.X + local contentSizeY = contentSize.Y + if FlagsList:get("FFlagFixContentNotFullyShownAfterResize") then + local props = self.props + local style = getStyle(self) + local scrollingFrameProps = self.getScrollingFrameProps(props, style) + -- for vertical scroll, canvas size on x axis should not update when content size changes + -- for horizon one, y axis should not change + -- for both scrolling, canvas size can be fully controlled by content + if scrollingFrameProps.ScrollingDirection == Enum.ScrollingDirection.Y then + contentSizeX = 0 + elseif scrollingFrameProps.ScrollingDirection == Enum.ScrollingDirection.X then + contentSizeY = 0 + end + end + + self.scrollingRef.current.CanvasSize = UDim2.new(0, contentSizeX, 0, contentSizeY) end end @@ -84,14 +112,23 @@ function ScrollingFrame:init() self.getScrollingFrameProps = function(props, style) -- after filtering out parent's props and other component specific props, -- what is left should be ScrollingFrame specific props + local updatedProps + if FlagsList:get("FFlagRefactorDevFrameworkTheme") then + updatedProps = Cryo.Dictionary.join(props, { + Stylizer = Cryo.None + }) + else + updatedProps = props + end return Cryo.Dictionary.join( style, - props, + updatedProps, self.propFilters.parentContainerProps, { Size = UDim2.new(1, 0, 1, 0), [Roact.Children] = Cryo.None, [Roact.Change.CanvasPosition] = self.onScroll, + [Roact.Change.AbsoluteSize] = FlagsList:get("FFlagFixContentNotFullyShownAfterResize") and self.updateCanvasSize or nil, [Roact.Ref] = self.scrollingRef, }) end @@ -103,8 +140,7 @@ end function ScrollingFrame:render() local props = self.props - local theme = props.Theme - local style = theme:getStyle("Framework", self) + local style = getStyle(self) local position = props.Position local size = props.Size @@ -139,8 +175,8 @@ function ScrollingFrame:render() end ContextServices.mapToProps(ScrollingFrame, { - Theme = ContextServices.Theme, + Stylizer = FlagsList:get("FFlagRefactorDevFrameworkTheme") and ContextServices.Stylizer or nil, + Theme = (not FlagsList:get("FFlagRefactorDevFrameworkTheme")) and ContextServices.Theme or nil, }) - return ScrollingFrame \ No newline at end of file diff --git a/BuiltInPlugins/LocalizationTools/Packages/Framework/UI/ScrollingFrame/example.lua b/BuiltInPlugins/LocalizationTools/Packages/Framework/UI/ScrollingFrame/example.lua index 87b1ed2ce1..3fc04c911d 100644 --- a/BuiltInPlugins/LocalizationTools/Packages/Framework/UI/ScrollingFrame/example.lua +++ b/BuiltInPlugins/LocalizationTools/Packages/Framework/UI/ScrollingFrame/example.lua @@ -6,7 +6,6 @@ return function(plugin) local Theme = ContextServices.Theme local UI = require(Framework.UI) - local Container = UI.Container local ScrollingFrame = UI.ScrollingFrame local StudioUI = require(Framework.StudioUI) @@ -16,15 +15,21 @@ return function(plugin) local pluginItem = Plugin.new(plugin) local Util = require(Framework.Util) - local StyleTable = Util.StyleTable - local Style = Util.Style - local StyleModifier = Util.StyleModifier - - local theme = Theme.new(function(theme, getColor) - return { - Framework = StudioFrameworkStyles.new(theme, getColor) - } - end) + local FlagsList = Util.Flags.new({ + FFlagRefactorDevFrameworkTheme = {"RefactorDevFrameworkTheme"}, + }) + local StudioTheme = require(Framework.Style.Themes.StudioTheme) + + local theme + if FlagsList:get("FFlagRefactorDevFrameworkTheme") then + theme = StudioTheme.new() + else + theme = Theme.new(function(theme, getColor) + return { + Framework = StudioFrameworkStyles.new(theme, getColor) + } + end) + end local ExampleButton = Roact.PureComponent:extend("ExampleButton") @@ -56,10 +61,9 @@ return function(plugin) }) end - return ContextServices.provide({pluginItem, theme}, { Main = Roact.createElement(Dialog, { - Enabled = enabled, + Enabled = self.state.enabled, Title = "ToggleButton Example", Size = Vector2.new(200, 200), Resizable = false, diff --git a/BuiltInPlugins/LocalizationTools/Packages/Framework/UI/ScrollingFrame/style.lua b/BuiltInPlugins/LocalizationTools/Packages/Framework/UI/ScrollingFrame/style.lua index 2e46b067f9..2b68854f59 100644 --- a/BuiltInPlugins/LocalizationTools/Packages/Framework/UI/ScrollingFrame/style.lua +++ b/BuiltInPlugins/LocalizationTools/Packages/Framework/UI/ScrollingFrame/style.lua @@ -1,23 +1,41 @@ local Framework = script.Parent.Parent.Parent local Util = require(Framework.Util) +local Cryo = Util.Cryo +local deepCopy = Util.deepCopy local Style = Util.Style +local FlagsList = Util.Flags.new({ + FFlagRefactorDevFrameworkTheme = {"RefactorDevFrameworkTheme"}, +}) local StudioFrameworkStyles = Framework.StudioUI.StudioFrameworkStyles local Common = require(StudioFrameworkStyles.Common) +local UIFolderData = require(Framework.UI.UIFolderData) +local InfiniteScrollingFrame = require(UIFolderData.InfiniteScrollingFrame.style) -return function(theme, getColor) - local common = Common(theme, getColor) - - local Default = Style.extend(common.Scroller, { +if FlagsList:get("FFlagRefactorDevFrameworkTheme") then + local infiniteScrollingFrame = deepCopy(InfiniteScrollingFrame) + return Cryo.Dictionary.join(infiniteScrollingFrame, { AutoSizeCanvas = true, AutoSizeLayoutElement = "UIListLayout", AutoSizeLayoutOptions = { Padding = UDim.new(0, 4), }, }) +else + return function(theme, getColor) + local common = Common(theme, getColor) + + local Default = Style.extend(common.Scroller, { + AutoSizeCanvas = true, + AutoSizeLayoutElement = "UIListLayout", + AutoSizeLayoutOptions = { + Padding = UDim.new(0, 4), + }, + }) - return { - Default = Default, - } + return { + Default = Default, + } + end end diff --git a/BuiltInPlugins/LocalizationTools/Packages/Framework/UI/SelectInput.lua b/BuiltInPlugins/LocalizationTools/Packages/Framework/UI/SelectInput.lua index dfb8b097fb..a86a9ee2d4 100644 --- a/BuiltInPlugins/LocalizationTools/Packages/Framework/UI/SelectInput.lua +++ b/BuiltInPlugins/LocalizationTools/Packages/Framework/UI/SelectInput.lua @@ -4,13 +4,15 @@ Required Props: table Items: An array of each item that should appear in the dropdown. callback OnItemActivated: A callback for when the user selects a dropdown entry. - Theme Theme: a Theme object supplied by mapToProps() Focus Focus: a Focus object supplied by mapToProps() Optional Props: string PlaceholderText: A placeholder to display if there is no item selected. callback OnRenderItem: A function used to render a dropdown menu item. number SelectedIndex: The currently selected item index. + Style Style: The style with which to render this component. + Stylizer Stylizer: A Stylizer ContextItem, which is provided via mapToProps. + Theme Theme: a Theme object supplied by mapToProps() Style Values: Style BackgroundStyle: The style with which to render the background. @@ -36,6 +38,10 @@ local StyleModifier = Util.StyleModifier local SelectInput = Roact.PureComponent:extend("SelectInput") Typecheck.wrap(SelectInput, script) +local FlagsList = Util.Flags.new({ + FFlagRefactorDevFrameworkTheme = {"RefactorDevFrameworkTheme"}, +}) + local BORDER_SIZE = 1 function SelectInput:init() @@ -73,7 +79,13 @@ end function SelectInput:render() local props = self.props local state = self.state - local style = props.Theme:getStyle("Framework", self) + + local style + if FlagsList:get("FFlagRefactorDevFrameworkTheme") then + style = props.Stylizer + else + style = props.Theme:getStyle("Framework", self) + end local items = props.Items local isOpen = state.isOpen @@ -126,7 +138,8 @@ end ContextServices.mapToProps(SelectInput, { Focus = ContextServices.Focus, - Theme = ContextServices.Theme, + Stylizer = FlagsList:get("FFlagRefactorDevFrameworkTheme") and ContextServices.Stylizer or nil, + Theme = (not FlagsList:get("FFlagRefactorDevFrameworkTheme")) and ContextServices.Theme or nil, }) return SelectInput diff --git a/BuiltInPlugins/LocalizationTools/Packages/Framework/UI/SelectInput/style.lua b/BuiltInPlugins/LocalizationTools/Packages/Framework/UI/SelectInput/style.lua index 9ce9585bda..56453ad7f6 100644 --- a/BuiltInPlugins/LocalizationTools/Packages/Framework/UI/SelectInput/style.lua +++ b/BuiltInPlugins/LocalizationTools/Packages/Framework/UI/SelectInput/style.lua @@ -3,38 +3,73 @@ local Framework = script.Parent.Parent.Parent local StudioFrameworkStyles = Framework.StudioUI.StudioFrameworkStyles local Common = require(StudioFrameworkStyles.Common) local UIFolderData = require(Framework.UI.UIFolderData) -local Util = require(Framework.Util) local RoundBox = require(UIFolderData.RoundBox.style) + +local StyleKey = require(Framework.Style.StyleKey) + +local Util = require(Framework.Util) +local Cryo = Util.Cryo +local deepCopy = Util.deepCopy local Style = Util.Style local StyleModifier = Util.StyleModifier +local FlagsList = Util.Flags.new({ + FFlagRefactorDevFrameworkTheme = {"RefactorDevFrameworkTheme"}, +}) -return function(theme, getColor) - local common = Common(theme, getColor) - local roundBox = RoundBox(theme, getColor) - +if FlagsList:get("FFlagRefactorDevFrameworkTheme") then + local roundBox = deepCopy(RoundBox) return { - Default = Style.extend(common.Border, { - Padding = 10, - BackgroundStyle = roundBox.Default, - [StyleModifier.Hover] = { - BackgroundStyle = Style.extend(roundBox.Default, common.BorderHover), - }, - DropdownMenu = { - BackgroundStyle = roundBox.Default, - Width = 240, - MaxHeight = 240, - Offset = Vector2.new(0, 0), - }, - Size = UDim2.new(0, 240, 0, 32), - ArrowOffset = 10, - ArrowSize = UDim2.new(0, 12, 0, 12), - ArrowImage = "rbxasset://textures/StudioToolbox/ArrowDownIconWhite.png", - ArrowColor = theme:GetColor("MainText"), - PlaceholderTextColor = theme:GetColor("DimmedText"), - Text = Style.extend(common.MainText, { - TextXAlignment = Enum.TextXAlignment.Left, - }) - }) + Padding = 10, + BackgroundStyle = roundBox, + [StyleModifier.Hover] = { + BackgroundStyle = Cryo.Dictionary.join(roundBox, { + BorderColor = StyleKey.DialogMainButton, + }), + }, + DropdownMenu = { + BackgroundStyle = roundBox, + Width = 240, + MaxHeight = 240, + Offset = Vector2.new(0, 0), + }, + Size = UDim2.new(0, 240, 0, 32), + ArrowOffset = 10, + ArrowSize = UDim2.new(0, 12, 0, 12), + ArrowImage = "rbxasset://textures/StudioToolbox/ArrowDownIconWhite.png", + ArrowColor = StyleKey.MainText, + PlaceholderTextColor = StyleKey.DimmedText, + Text = Cryo.Dictionary.join(Common.MainText, { + TextXAlignment = Enum.TextXAlignment.Left, + }), } +else + return function(theme, getColor) + local common = Common(theme, getColor) + local roundBox = RoundBox(theme, getColor) + return { + Default = Style.extend(common.Border, { + Padding = 10, + BackgroundStyle = roundBox.Default, + [StyleModifier.Hover] = { + BackgroundStyle = Style.extend(roundBox.Default, common.BorderHover), + }, + DropdownMenu = { + BackgroundStyle = roundBox.Default, + Width = 240, + MaxHeight = 240, + Offset = Vector2.new(0, 0), + }, + Size = UDim2.new(0, 240, 0, 32), + ArrowOffset = 10, + ArrowSize = UDim2.new(0, 12, 0, 12), + ArrowImage = "rbxasset://textures/StudioToolbox/ArrowDownIconWhite.png", + ArrowColor = theme:GetColor("MainText"), + PlaceholderTextColor = theme:GetColor("DimmedText"), + Text = Style.extend(common.MainText, { + TextXAlignment = Enum.TextXAlignment.Left, + }) + }) + } + end end \ No newline at end of file diff --git a/BuiltInPlugins/LocalizationTools/Packages/Framework/UI/Separator.lua b/BuiltInPlugins/LocalizationTools/Packages/Framework/UI/Separator.lua index b32f8ad0ba..910b2b6960 100644 --- a/BuiltInPlugins/LocalizationTools/Packages/Framework/UI/Separator.lua +++ b/BuiltInPlugins/LocalizationTools/Packages/Framework/UI/Separator.lua @@ -1,9 +1,6 @@ --[[ A simple border to separate elements. - Required Props: - Theme Theme: A Theme ContextItem, which is provided via mapToProps. - Optional Props: Enum.DominantAxis DominantAxis: Specifies whether the separator fills the space horizontally or vertically. Width will make the separator @@ -12,7 +9,9 @@ number LayoutOrder: The layout order of this component in a UILayout. UDim2 Position: The position of the center of the separator. Style Style: The style with which to render this component. + Stylizer Stylizer: A Stylizer ContextItem, which is provided via mapToProps. StyleModifier StyleModifier: The StyleModifier index into Style. + Theme Theme: A Theme ContextItem, which is provided via mapToProps. number ZIndex: The render index of this component. Style Values: @@ -20,12 +19,16 @@ number StretchMargin: The padding in pixels to subtract from either side of the separator's dominant axis. number Weight: The thickness of the separator line. ]] - local Framework = script.Parent.Parent local Roact = require(Framework.Parent.Roact) local ContextServices = require(Framework.ContextServices) local t = require(Framework.Util.Typecheck.t) -local Typecheck = require(Framework.Util).Typecheck + +local Util = require(Framework.Util) +local Typecheck = Util.Typecheck +local FlagsList = Util.Flags.new({ + FFlagRefactorDevFrameworkTheme = {"RefactorDevFrameworkTheme"}, +}) local Separator = Roact.PureComponent:extend("Separator") Typecheck.wrap(Separator, script) @@ -39,7 +42,13 @@ function Separator:render() local dominantAxis = props.DominantAxis or Enum.DominantAxis.Width local theme = props.Theme - local style = theme:getStyle("Framework", self) + + local style + if FlagsList:get("FFlagRefactorDevFrameworkTheme") then + style = props.Stylizer + else + style = theme:getStyle("Framework", self) + end local color = style.Color local stretchMargin = style.StretchMargin @@ -67,7 +76,8 @@ function Separator:render() end ContextServices.mapToProps(Separator, { - Theme = ContextServices.Theme, + Stylizer = FlagsList:get("FFlagRefactorDevFrameworkTheme") and ContextServices.Stylizer or nil, + Theme = (not FlagsList:get("FFlagRefactorDevFrameworkTheme")) and ContextServices.Theme or nil, }) return Separator diff --git a/BuiltInPlugins/LocalizationTools/Packages/Framework/UI/Separator/style.lua b/BuiltInPlugins/LocalizationTools/Packages/Framework/UI/Separator/style.lua index f649ad9fd6..6be52cc262 100644 --- a/BuiltInPlugins/LocalizationTools/Packages/Framework/UI/Separator/style.lua +++ b/BuiltInPlugins/LocalizationTools/Packages/Framework/UI/Separator/style.lua @@ -1,17 +1,30 @@ local Framework = script.Parent.Parent.Parent +local StyleKey = require(Framework.Style.StyleKey) + local Util = require(Framework.Util) local Style = Util.Style +local FlagsList = Util.Flags.new({ + FFlagRefactorDevFrameworkTheme = {"RefactorDevFrameworkTheme"}, +}) -return function(theme, getColor) - - local Default = Style.new({ - Color = theme:GetColor("Border"), +if FlagsList:get("FFlagRefactorDevFrameworkTheme") then + return { + Color = StyleKey.Border, StretchMargin = 0, Weight = 1 - }) - - return { - Default = Default, } -end +else + return function(theme, getColor) + + local Default = Style.new({ + Color = theme:GetColor("Border"), + StretchMargin = 0, + Weight = 1 + }) + + return { + Default = Default, + } + end +end \ No newline at end of file diff --git a/BuiltInPlugins/LocalizationTools/Packages/Framework/UI/Slider/test.spec.lua b/BuiltInPlugins/LocalizationTools/Packages/Framework/UI/Slider/test.spec.lua index 36b9c3be38..5f1d7d8c29 100644 --- a/BuiltInPlugins/LocalizationTools/Packages/Framework/UI/Slider/test.spec.lua +++ b/BuiltInPlugins/LocalizationTools/Packages/Framework/UI/Slider/test.spec.lua @@ -43,8 +43,6 @@ return function() local element = createTestSlider(props) local instance = Roact.mount(element, container) - container.Parent = workspace - local frame = container:FindFirstChildOfClass("Frame") expect(frame).to.be.ok() @@ -56,4 +54,4 @@ return function() Roact.unmount(instance) end) -end \ No newline at end of file +end diff --git a/BuiltInPlugins/LocalizationTools/Packages/Framework/UI/TextInput.lua b/BuiltInPlugins/LocalizationTools/Packages/Framework/UI/TextInput.lua index 4e84e6536e..98aef86460 100644 --- a/BuiltInPlugins/LocalizationTools/Packages/Framework/UI/TextInput.lua +++ b/BuiltInPlugins/LocalizationTools/Packages/Framework/UI/TextInput.lua @@ -5,9 +5,6 @@ It does not handle labels, error messages or tooltips. They should be implemented by higher order wrappers. Descended from TextEntry in UILibrary and LabeledTextInput in TerrainTools. - Required Props: - Theme Theme: A Theme ContextItem, which is provided via mapToProps. - Optional Props: boolean Enabled: Whether the input is editable. Defaults to true. number LayoutOrder: The layout order of this component in a list. @@ -17,6 +14,8 @@ string PlaceholderText: Placeholder text to show when the input is empty. string Text: Text to populate the input with. Style Style: The style with which to render this component. + Stylizer Stylizer: A Stylizer ContextItem, which is provided via mapToProps. + Theme Theme: A Theme ContextItem, which is provided via mapToProps. StyleModifier StyleModifier: The StyleModifier index into Style. boolean ShouldFocus: Set focus onto the box so that the user can start typing. UDim2 Position: The position of this component. @@ -31,12 +30,17 @@ number TextSize: The font size of the text. Color3 TextColor: The color of the search term text. ]] - local Framework = script.Parent.Parent local Roact = require(Framework.Parent.Roact) local ContextServices = require(Framework.ContextServices) -local Typecheck = require(Framework.Util).Typecheck + +local Util = require(Framework.Util) +local Typecheck = Util.Typecheck +local FlagsList = Util.Flags.new({ + FFlagRefactorDevFrameworkTheme = {"RefactorDevFrameworkTheme"}, +}) + local Container = require(Framework.UI.Container) local RoundBox = require(Framework.UI.RoundBox) local StyleModifier = require(Framework.Util.StyleModifier) @@ -98,7 +102,13 @@ function TextInput:render() local position = props.Position local theme = props.Theme - local style = theme:getStyle("Framework", self) + local style + if FlagsList:get("FFlagRefactorDevFrameworkTheme") then + style = props.Stylizer + else + style = theme:getStyle("Framework", self) + end + local font = style.Font local textSize = style.TextSize local textColor = style.TextColor @@ -179,7 +189,8 @@ function TextInput:render() end ContextServices.mapToProps(TextInput, { - Theme = ContextServices.Theme, + Stylizer = FlagsList:get("FFlagRefactorDevFrameworkTheme") and ContextServices.Stylizer or nil, + Theme = (not FlagsList:get("FFlagRefactorDevFrameworkTheme")) and ContextServices.Theme or nil, }) return TextInput \ No newline at end of file diff --git a/BuiltInPlugins/LocalizationTools/Packages/Framework/UI/TextInput/style.lua b/BuiltInPlugins/LocalizationTools/Packages/Framework/UI/TextInput/style.lua index 921e462009..ee8b7da296 100644 --- a/BuiltInPlugins/LocalizationTools/Packages/Framework/UI/TextInput/style.lua +++ b/BuiltInPlugins/LocalizationTools/Packages/Framework/UI/TextInput/style.lua @@ -1,39 +1,66 @@ local Framework = script.Parent.Parent.Parent +local StyleKey = require(Framework.Style.StyleKey) + local Util = require(Framework.Util) +local Cryo = Util.Cryo +local deepCopy = Util.deepCopy local Style = Util.Style local StyleModifier = Util.StyleModifier +local FlagsList = Util.Flags.new({ + FFlagRefactorDevFrameworkTheme = {"RefactorDevFrameworkTheme"}, +}) local Common = require(Framework.StudioUI.StudioFrameworkStyles.Common) local UIFolderData = require(Framework.UI.UIFolderData) -local UI = require(Framework.UI) -local Container = UI.Container local RoundBox = require(UIFolderData.RoundBox.style) -return function(theme, getColor) - local common = Common(theme, getColor) +if FlagsList:get("FFlagRefactorDevFrameworkTheme") then + local roundBox = deepCopy(RoundBox) + return { + PlaceholderTextColor = StyleKey.DimmedText, + + ["&RoundedBorder"] = { + Padding = { + Left = 10, + Top = 5, + Right = 10, + Bottom = 5 + }, + BackgroundStyle = RoundBox, + [StyleModifier.Hover] = { + BackgroundStyle = Cryo.Dictionary.join(roundBox, { + BorderColor = StyleKey.DialogMainButton, + }) + }, + } + } +else + return function(theme, getColor) + local common = Common(theme, getColor) - local Default = Style.extend(common.MainText, common.Border, { - PlaceholderTextColor = theme:GetColor("DimmedText"), - }) + local Default = Style.extend(common.MainText, common.Border, { + PlaceholderTextColor = theme:GetColor("DimmedText"), + }) - local roundBox = RoundBox(theme, getColor) + local roundBox = RoundBox(theme, getColor) - local RoundedBorder = Style.extend(Default, { - Padding = { - Left = 10, - Top = 5, - Right = 10, - Bottom = 5 - }, - BackgroundStyle = roundBox.Default, - [StyleModifier.Hover] = { - BackgroundStyle = Style.extend(roundBox.Default, common.BorderHover) - }, - }) + local RoundedBorder = Style.extend(Default, { + Padding = { + Left = 10, + Top = 5, + Right = 10, + Bottom = 5 + }, + BackgroundStyle = roundBox.Default, + [StyleModifier.Hover] = { + BackgroundStyle = Style.extend(roundBox.Default, common.BorderHover) + }, + }) - return { - Default = Default, - RoundedBorder = RoundedBorder - } + return { + Default = Default, + RoundedBorder = RoundedBorder + } + end end diff --git a/BuiltInPlugins/LocalizationTools/Packages/Framework/UI/TextLabel.lua b/BuiltInPlugins/LocalizationTools/Packages/Framework/UI/TextLabel.lua index 62fd8f0067..a22d1ccbda 100644 --- a/BuiltInPlugins/LocalizationTools/Packages/Framework/UI/TextLabel.lua +++ b/BuiltInPlugins/LocalizationTools/Packages/Framework/UI/TextLabel.lua @@ -3,9 +3,10 @@ Required Props: string Text: The text to display in this button. - Theme Theme: A Theme ContextItem, which is provided via mapToProps. Optional Props: + Theme Theme: A Theme ContextItem, which is provided via mapToProps. + Stylizer Stylizer: A Stylizer ContextItem, which is provided via mapToProps. number LayoutOrder: The layout order of this component in a list. UDim2 Size: The size of this component. UDim2 Position: The position of this component. @@ -28,15 +29,17 @@ local Framework = script.Parent.Parent local Roact = require(Framework.Parent.Roact) local ContextServices = require(Framework.ContextServices) + local Util = require(Framework.Util) local Typecheck = Util.Typecheck local prioritize = Util.prioritize +local FlagsList = Util.Flags.new({ + FFlagRefactorDevFrameworkTheme = {"RefactorDevFrameworkTheme"}, +}) local TextLabel = Roact.PureComponent:extend("TextLabel") Typecheck.wrap(TextLabel, script) -local FFlagTextLabelProps = game:DefineFastFlag("TextLabelProps", false) - function TextLabel:render() local layoutOrder = self.props.LayoutOrder local size = self.props.Size @@ -45,7 +48,13 @@ function TextLabel:render() local theme = self.props.Theme local textWrapped = self.props.TextWrapped local zIndex = self.props.ZIndex - local style = theme:getStyle("Framework", self) + + local style + if FlagsList:get("FFlagRefactorDevFrameworkTheme") then + style = self.props.Stylizer + else + style = theme:getStyle("Framework", self) + end local backgroundTransparency = prioritize(self.props.BackgroundTransparency, style.BackgroundTransparency, 1) local font = prioritize(self.props.Font, style.Font) @@ -54,8 +63,6 @@ function TextLabel:render() local transparency = prioritize(self.props.TextTransparency, style.TextTransparency) local textXAlignment = prioritize(self.props.TextXAlignment, style.TextXAlignment) local textYAlignment = prioritize(self.props.TextYAlignment, style.TextYAlignment) - local position = self.props.Position - return Roact.createElement("TextLabel", { BackgroundTransparency = backgroundTransparency, @@ -67,15 +74,16 @@ function TextLabel:render() TextColor3 = textColor, TextSize = textSize, TextTransparency = transparency, - TextWrapped = FFlagTextLabelProps and textWrapped or nil, + TextWrapped = textWrapped, TextXAlignment = textXAlignment, TextYAlignment = textYAlignment, - ZIndex = FFlagTextLabelProps and zIndex or nil, + ZIndex = zIndex, }, self.props[Roact.Children]) end ContextServices.mapToProps(TextLabel, { - Theme = ContextServices.Theme, + Stylizer = FlagsList:get("FFlagRefactorDevFrameworkTheme") and ContextServices.Stylizer or nil, + Theme = (not FlagsList:get("FFlagRefactorDevFrameworkTheme")) and ContextServices.Theme or nil, }) return TextLabel diff --git a/BuiltInPlugins/LocalizationTools/Packages/Framework/UI/TextLabel/style.lua b/BuiltInPlugins/LocalizationTools/Packages/Framework/UI/TextLabel/style.lua index d48aec8fa4..ac53342673 100644 --- a/BuiltInPlugins/LocalizationTools/Packages/Framework/UI/TextLabel/style.lua +++ b/BuiltInPlugins/LocalizationTools/Packages/Framework/UI/TextLabel/style.lua @@ -2,21 +2,32 @@ local Framework = script.Parent.Parent.Parent local Util = require(Framework.Util) local Style = Util.Style +local FlagsList = Util.Flags.new({ + FFlagRefactorDevFrameworkTheme = {"RefactorDevFrameworkTheme"}, +}) local StudioFrameworkStyles = Framework.StudioUI.StudioFrameworkStyles local Common = require(StudioFrameworkStyles.Common) local StyleModifier = Util.StyleModifier -return function(theme, getColor) - local common = Common(theme, getColor) - - local Default = Style.extend(common.MainText, { +if FlagsList:get("FFlagRefactorDevFrameworkTheme") then + return { [StyleModifier.Disabled] = { TextTransparency = 0.5, }, - }) - - return { - Default = Default, } -end +else + return function(theme, getColor) + local common = Common(theme, getColor) + + local Default = Style.extend(common.MainText, { + [StyleModifier.Disabled] = { + TextTransparency = 0.5, + }, + }) + + return { + Default = Default, + } + end +end \ No newline at end of file diff --git a/BuiltInPlugins/LocalizationTools/Packages/Framework/UI/TextLabel/test.spec.lua b/BuiltInPlugins/LocalizationTools/Packages/Framework/UI/TextLabel/test.spec.lua index d8788444dc..5f739cb55f 100644 --- a/BuiltInPlugins/LocalizationTools/Packages/Framework/UI/TextLabel/test.spec.lua +++ b/BuiltInPlugins/LocalizationTools/Packages/Framework/UI/TextLabel/test.spec.lua @@ -7,12 +7,24 @@ return function() local FrameworkStyles = require(Framework.UI.FrameworkStyles) local TextLabel = require(script.Parent) + local StudioTheme = require(Framework.Style.Themes.StudioTheme) + + local Util = require(Framework.Util) + local FlagsList = Util.Flags.new({ + FFlagRefactorDevFrameworkTheme = {"RefactorDevFrameworkTheme"}, + }) + local function createTestTextLabelDecoration() - local theme = Theme.new(function() - return { - Framework = FrameworkStyles.new(), - } - end) + local theme + if FlagsList:get("FFlagRefactorDevFrameworkTheme") then + theme = StudioTheme.mock() + else + theme = Theme.new(function() + return { + Framework = FrameworkStyles.new(), + } + end) + end return provide({theme}, { TextLabel = Roact.createElement(TextLabel,{ Text = "hello world" diff --git a/BuiltInPlugins/LocalizationTools/Packages/Framework/UI/ToggleButton.lua b/BuiltInPlugins/LocalizationTools/Packages/Framework/UI/ToggleButton.lua index 14676ca8a7..84fadc753c 100644 --- a/BuiltInPlugins/LocalizationTools/Packages/Framework/UI/ToggleButton.lua +++ b/BuiltInPlugins/LocalizationTools/Packages/Framework/UI/ToggleButton.lua @@ -4,9 +4,11 @@ Required Props: callback OnClick: The function that will be called when this button is clicked to turn on and off. - Theme Theme: A Theme ContextItem, which is provided via mapToProps. Optional Props: + Style Style: The style with which to render this component. + Theme Theme: A Theme ContextItem, which is provided via mapToProps. + Stylizer Stylizer: A Stylizer ContextItem, which is provided via mapToProps. Vector2 AnchorPoint: The pivot point of this component's Position prop. boolean Disabled: Whether or not this button can be clicked. number LayoutOrder: The layout order of this component. @@ -17,13 +19,15 @@ string Text: A text to be displayed over the image if any. number ZIndex: The render index of this component. ]] - local Framework = script.Parent.Parent local Roact = require(Framework.Parent.Roact) local ContextServices = require(Framework.ContextServices) local Util = require(Framework.Util) local Typecheck = Util.Typecheck +local FlagsList = Util.Flags.new({ + FFlagRefactorDevFrameworkTheme = {"RefactorDevFrameworkTheme"}, +}) local Button = require(Framework.UI.Button) local HoverArea = require(Framework.UI.HoverArea) @@ -59,7 +63,12 @@ function ToggleButton:render() local theme = self.props.Theme local zIndex = self.props.ZIndex - local style = theme:getStyle("Framework", self) + local style + if FlagsList:get("FFlagRefactorDevFrameworkTheme") then + style = self.props.Stylizer + else + style = theme:getStyle("Framework", self) + end local styleModifier if isDisabled then @@ -84,7 +93,8 @@ function ToggleButton:render() end ContextServices.mapToProps(ToggleButton, { - Theme = ContextServices.Theme, + Stylizer = FlagsList:get("FFlagRefactorDevFrameworkTheme") and ContextServices.Stylizer or nil, + Theme = (not FlagsList:get("FFlagRefactorDevFrameworkTheme")) and ContextServices.Theme or nil, }) return ToggleButton diff --git a/BuiltInPlugins/LocalizationTools/Packages/Framework/UI/ToggleButton/example.lua b/BuiltInPlugins/LocalizationTools/Packages/Framework/UI/ToggleButton/example.lua index 2497509430..f7a98aba24 100644 --- a/BuiltInPlugins/LocalizationTools/Packages/Framework/UI/ToggleButton/example.lua +++ b/BuiltInPlugins/LocalizationTools/Packages/Framework/UI/ToggleButton/example.lua @@ -19,15 +19,21 @@ return function(plugin) local pluginItem = Plugin.new(plugin) local Util = require(Framework.Util) - local StyleTable = Util.StyleTable - local Style = Util.Style - local StyleModifier = Util.StyleModifier + local FlagsList = Util.Flags.new({ + FFlagRefactorDevFrameworkTheme = {"RefactorDevFrameworkTheme"}, + }) + local StudioTheme = require(Framework.Style.Themes.StudioTheme) - local theme = Theme.new(function(theme, getColor) - return { - Framework = StudioFrameworkStyles.new(theme, getColor) - } - end) + local theme + if FlagsList:get("FFlagRefactorDevFrameworkTheme") then + theme = StudioTheme.new() + else + theme = Theme.new(function(theme, getColor) + return { + Framework = StudioFrameworkStyles.new(theme, getColor) + } + end) + end local ExampleButton = Roact.PureComponent:extend("ExampleButton") @@ -84,7 +90,7 @@ return function(plugin) Disabled = true, Selected = false, LayoutOrder = 0, - OnClick = self.onToggle, + OnClick = self.onToggle1, Size = UDim2.fromOffset(40, 24), }), ToggleButton = Roact.createElement(ToggleButton, { diff --git a/BuiltInPlugins/LocalizationTools/Packages/Framework/UI/ToggleButton/style.lua b/BuiltInPlugins/LocalizationTools/Packages/Framework/UI/ToggleButton/style.lua index a85c8628a2..6b8d8f09bf 100644 --- a/BuiltInPlugins/LocalizationTools/Packages/Framework/UI/ToggleButton/style.lua +++ b/BuiltInPlugins/LocalizationTools/Packages/Framework/UI/ToggleButton/style.lua @@ -1,9 +1,14 @@ local Framework = script.Parent.Parent.Parent +local StyleKey = require(Framework.Style.StyleKey) + local Util = require(Framework.Util) local Style = Util.Style local StyleModifier = Util.StyleModifier local StyleValue = Util.StyleValue +local FlagsList = Util.Flags.new({ + FFlagRefactorDevFrameworkTheme = {"RefactorDevFrameworkTheme"}, +}) local UI = require(Framework.UI) local Decoration = UI.Decoration @@ -11,61 +16,97 @@ local Decoration = UI.Decoration local StudioFrameworkStyles = Framework.StudioUI.StudioFrameworkStyles local Common = require(StudioFrameworkStyles.Common) -return function(theme, getColor) - local common = Common(theme, getColor) - local themeName = theme.Name - - local onImage = StyleValue.new("onImage", { - Light = "rbxasset://textures/RoactStudioWidgets/toggle_on_light.png", - Dark = "rbxasset://textures/RoactStudioWidgets/toggle_on_dark.png", - }) - - local offImage = StyleValue.new("offImage", { - Light = "rbxasset://textures/RoactStudioWidgets/toggle_off_light.png", - Dark = "rbxasset://textures/RoactStudioWidgets/toggle_off_dark.png", - }) - - local disabledImage = StyleValue.new("disabledImage", { - Light = "rbxasset://textures/RoactStudioWidgets/toggle_disable_light.png", - Dark = "rbxasset://textures/RoactStudioWidgets/toggle_disable_dark.png", - }) - - local Default = Style.extend(common.MainText, { +if FlagsList:get("FFlagRefactorDevFrameworkTheme") then + return { Background = Decoration.Image, BackgroundStyle = { - Image = offImage:get(themeName), + Image = StyleKey.ToggleOffImage, }, [StyleModifier.Selected] = { BackgroundStyle = { - Image = onImage:get(themeName), + Image = StyleKey.ToggleOnImage, }, }, [StyleModifier.Disabled] = { BackgroundStyle = { - Image = disabledImage:get(themeName), + Image = StyleKey.ToggleDisabledImage, }, }, - }) - local Checkbox = Style.new({ - Background = Decoration.Image, - BackgroundStyle = { - Image = "rbxasset://textures/GameSettings/UncheckedBox.png", - }, - [StyleModifier.Selected] = { + ["&Checkbox"] = { + Background = Decoration.Image, BackgroundStyle = { - Image = "rbxasset://textures/GameSettings/CheckedBoxLight.png", + Image = "rbxasset://textures/GameSettings/UncheckedBox.png", + }, + [StyleModifier.Selected] = { + BackgroundStyle = { + Image = "rbxasset://textures/GameSettings/CheckedBoxLight.png", + }, + }, + [StyleModifier.Disabled] = { + BackgroundStyle = { + Image = "rbxasset://textures/GameSettings/UncheckedBox.png", + }, }, }, - [StyleModifier.Disabled] = { + } +else + return function(theme, getColor) + local common = Common(theme, getColor) + local themeName = theme.Name + + local onImage = StyleValue.new("onImage", { + Light = "rbxasset://textures/RoactStudioWidgets/toggle_on_light.png", + Dark = "rbxasset://textures/RoactStudioWidgets/toggle_on_dark.png", + }) + + local offImage = StyleValue.new("offImage", { + Light = "rbxasset://textures/RoactStudioWidgets/toggle_off_light.png", + Dark = "rbxasset://textures/RoactStudioWidgets/toggle_off_dark.png", + }) + + local disabledImage = StyleValue.new("disabledImage", { + Light = "rbxasset://textures/RoactStudioWidgets/toggle_disable_light.png", + Dark = "rbxasset://textures/RoactStudioWidgets/toggle_disable_dark.png", + }) + + local Default = Style.extend(common.MainText, { + Background = Decoration.Image, + BackgroundStyle = { + Image = offImage:get(themeName), + }, + [StyleModifier.Selected] = { + BackgroundStyle = { + Image = onImage:get(themeName), + }, + }, + [StyleModifier.Disabled] = { + BackgroundStyle = { + Image = disabledImage:get(themeName), + }, + }, + }) + + local Checkbox = Style.new({ + Background = Decoration.Image, BackgroundStyle = { Image = "rbxasset://textures/GameSettings/UncheckedBox.png", }, - }, - }) + [StyleModifier.Selected] = { + BackgroundStyle = { + Image = "rbxasset://textures/GameSettings/CheckedBoxLight.png", + }, + }, + [StyleModifier.Disabled] = { + BackgroundStyle = { + Image = "rbxasset://textures/GameSettings/UncheckedBox.png", + }, + }, + }) - return { - Default = Default, - Checkbox = Checkbox, - } -end + return { + Default = Default, + Checkbox = Checkbox, + } + end +end \ No newline at end of file diff --git a/BuiltInPlugins/LocalizationTools/Packages/Framework/UI/ToggleButton/test.spec.lua b/BuiltInPlugins/LocalizationTools/Packages/Framework/UI/ToggleButton/test.spec.lua index 32a617b695..c4db80d801 100644 --- a/BuiltInPlugins/LocalizationTools/Packages/Framework/UI/ToggleButton/test.spec.lua +++ b/BuiltInPlugins/LocalizationTools/Packages/Framework/UI/ToggleButton/test.spec.lua @@ -12,17 +12,29 @@ return function() local ToggleButton = require(script.Parent) local Box = require(Framework.UI.Box) + local StudioTheme = require(Framework.Style.Themes.StudioTheme) + + local Util = require(Framework.Util) + local FlagsList = Util.Flags.new({ + FFlagRefactorDevFrameworkTheme = {"RefactorDevFrameworkTheme"}, + }) + local DEFAULT_PROPS = { Selected = true, OnClick = function() end, } local function createTestToggle(props) local mouse = Mouse.new({}) - local theme = Theme.new(function() - return { - Framework = FrameworkStyles.new(), - } - end) + local theme + if FlagsList:get("FFlagRefactorDevFrameworkTheme") then + theme = StudioTheme.mock() + else + theme = Theme.new(function() + return { + Framework = FrameworkStyles.new(), + } + end) + end return provide({theme, mouse}, { ToggleButton = Roact.createElement(ToggleButton, props or DEFAULT_PROPS), }) diff --git a/BuiltInPlugins/LocalizationTools/Packages/Framework/UI/Tooltip.lua b/BuiltInPlugins/LocalizationTools/Packages/Framework/UI/Tooltip.lua index 8066caf1e7..d25cfd053f 100644 --- a/BuiltInPlugins/LocalizationTools/Packages/Framework/UI/Tooltip.lua +++ b/BuiltInPlugins/LocalizationTools/Packages/Framework/UI/Tooltip.lua @@ -4,11 +4,12 @@ after a short delay. Required Props: - Theme Theme: A Theme ContextItem, which is provided via mapToProps. Focus Focus: A Focus ContextItem, which is provided via mapToProps. string Text: The text to display in the tooltip. Optional Props: + Theme Theme: A Theme ContextItem, which is provided via mapToProps. + Stylizer Stylizer: A Stylizer ContextItem, which is provided via mapToProps. boolean Enabled: Whether the tooltip will display on hover. integer Priority: The display order of this element, compared to other focused elements or elements that show on top. @@ -20,7 +21,6 @@ number ShowDelay: The time in seconds before the tooltip appears after the user stops moving the mouse over the element. ]] - local RunService = game:GetService("RunService") local TextService = game:GetService("TextService") @@ -32,7 +32,12 @@ local ShowOnTop = require(Framework.UI.ShowOnTop) local DropShadow = require(Framework.UI.DropShadow) local Box = require(Framework.UI.Box) local TextLabel = require(Framework.UI.TextLabel) -local Typecheck = require(Framework.Util).Typecheck + +local Util = require(Framework.Util) +local Typecheck = Util.Typecheck +local FlagsList = Util.Flags.new({ + FFlagRefactorDevFrameworkTheme = {"RefactorDevFrameworkTheme"}, +}) local Tooltip = Roact.PureComponent:extend("Tooltip") Typecheck.wrap(Tooltip, script) @@ -51,9 +56,14 @@ function Tooltip:init(props) end function Tooltip:didMount() - local theme = self.props.Theme - local style = theme:getStyle("Framework", self) + + local style + if FlagsList:get("FFlagRefactorDevFrameworkTheme") then + style = self.props.Stylizer + else + style = theme:getStyle("Framework", self) + end local showDelay = style.ShowDelay self.connectHover = function() @@ -106,7 +116,14 @@ function Tooltip:render() local state = self.state local theme = props.Theme - local style = theme:getStyle("Framework", self) + + local style + if FlagsList:get("FFlagRefactorDevFrameworkTheme") then + style = props.Stylizer + else + style = theme:getStyle("Framework", self) + end + local padding = style.Padding local dropShadowPadding = style.DropShadow and style.DropShadow.Radius or 0 local offset = style.Offset @@ -188,8 +205,9 @@ function Tooltip:render() end ContextServices.mapToProps(Tooltip, { - Theme = ContextServices.Theme, Focus = ContextServices.Focus, + Stylizer = FlagsList:get("FFlagRefactorDevFrameworkTheme") and ContextServices.Stylizer or nil, + Theme = (not FlagsList:get("FFlagRefactorDevFrameworkTheme")) and ContextServices.Theme or nil, }) return Tooltip \ No newline at end of file diff --git a/BuiltInPlugins/LocalizationTools/Packages/Framework/UI/Tooltip/style.lua b/BuiltInPlugins/LocalizationTools/Packages/Framework/UI/Tooltip/style.lua index 03ddd7ecba..074930b4eb 100644 --- a/BuiltInPlugins/LocalizationTools/Packages/Framework/UI/Tooltip/style.lua +++ b/BuiltInPlugins/LocalizationTools/Packages/Framework/UI/Tooltip/style.lua @@ -1,27 +1,44 @@ local Framework = script.Parent.Parent.Parent local Util = require(Framework.Util) +local Cryo = Util.Cryo local Style = Util.Style +local FlagsList = Util.Flags.new({ + FFlagRefactorDevFrameworkTheme = {"RefactorDevFrameworkTheme"}, +}) local StudioFrameworkStyles = Framework.StudioUI.StudioFrameworkStyles local Common = require(StudioFrameworkStyles.Common) local UIFolderData = require(Framework.UI.UIFolderData) local DropShadow = require(UIFolderData.DropShadow.style) -return function(theme, getColor) - local common = Common(theme, getColor) - local dropShadow = DropShadow(theme, getColor) - - local Default = Style.extend(common.MainText, { +if FlagsList:get("FFlagRefactorDevFrameworkTheme") then + local dropShadow = DropShadow + return { Padding = 5, MaxWidth = 200, ShowDelay = 0.3, Offset = Vector2.new(10, 5), - DropShadow = Style.extend(dropShadow.Default, { + DropShadow = Cryo.Dictionary.join(dropShadow, { Radius = 3, }), - }) - - return { - Default = Default, } -end +else + return function(theme, getColor) + local common = Common(theme, getColor) + local dropShadow = DropShadow(theme, getColor) + + local Default = Style.extend(common.MainText, { + Padding = 5, + MaxWidth = 200, + ShowDelay = 0.3, + Offset = Vector2.new(10, 5), + DropShadow = Style.extend(dropShadow.Default, { + Radius = 3, + }), + }) + + return { + Default = Default, + } + end +end \ No newline at end of file diff --git a/BuiltInPlugins/LocalizationTools/Packages/Framework/UI/Tooltip/test.spec.lua b/BuiltInPlugins/LocalizationTools/Packages/Framework/UI/Tooltip/test.spec.lua index 965a4ac846..8f7a0a6fc9 100644 --- a/BuiltInPlugins/LocalizationTools/Packages/Framework/UI/Tooltip/test.spec.lua +++ b/BuiltInPlugins/LocalizationTools/Packages/Framework/UI/Tooltip/test.spec.lua @@ -11,18 +11,28 @@ return function() local provide = ContextServices.provide local Tooltip = require(script.Parent) - local Box = require(Framework.UI.Box) + local StudioTheme = require(Framework.Style.Themes.StudioTheme) + + local Util = require(Framework.Util) + local FlagsList = Util.Flags.new({ + FFlagRefactorDevFrameworkTheme = {"RefactorDevFrameworkTheme"}, + }) local DEFAULT_PROPS = {} local function createTooltip(props) local mouse = Mouse.new({}) - local target = container or Instance.new("ScreenGui") + local target = Instance.new("ScreenGui") local focus = Focus.new(target) - local theme = Theme.new(function() - return { - Framework = FrameworkStyles.new(), - } - end) + local theme + if FlagsList:get("FFlagRefactorDevFrameworkTheme") then + theme = StudioTheme.mock() + else + theme = Theme.new(function() + return { + Framework = FrameworkStyles.new(), + } + end) + end return provide({theme, mouse, focus}, { Tooltip = Roact.createElement(Tooltip, props or DEFAULT_PROPS), }) @@ -48,4 +58,4 @@ return function() Roact.unmount(instance) end) -end \ No newline at end of file +end diff --git a/BuiltInPlugins/LocalizationTools/Packages/Framework/UI/TreeView.lua b/BuiltInPlugins/LocalizationTools/Packages/Framework/UI/TreeView.lua index af58fcf1af..a0f53a305c 100644 --- a/BuiltInPlugins/LocalizationTools/Packages/Framework/UI/TreeView.lua +++ b/BuiltInPlugins/LocalizationTools/Packages/Framework/UI/TreeView.lua @@ -2,7 +2,6 @@ TreeView - Displays a hierarchical data set. Required Props: - Theme Theme: The theme supplied from mapToProps() UDim2 Size: The size of the component table RootItems: The root items displayed in the tree view. callback GetChildren: This should return a list of children for a given item - GetChildren(item: Item) => Item[] @@ -10,7 +9,9 @@ table Expansion: Which items should be expanded - Set Optional Props: + Theme Theme: The theme supplied from mapToProps() Style Style: a style table supplied from props and theme:getStyle() + Stylizer Stylizer: A Stylizer ContextItem, which is provided via mapToProps. callback SortChildren: A comparator function to sort two items in the tree - SortChildren(left: Item, right: Item) => boolean callback GetItemKey: Return a unique key for an item - GetItemKey(item: Item) => string @@ -36,6 +37,11 @@ local Typecheck = require(Framework.Util).Typecheck local UI = Framework.UI local Container = require(UI.Container) +local Util = require(Framework.Util) + +local FlagsList = Util.Flags.new({ + FFlagRefactorDevFrameworkTheme = {"RefactorDevFrameworkTheme"}, +}) local TreeView = Roact.PureComponent:extend("TreeView") local ScrollingFrame = require(Framework.UI.ScrollingFrame) @@ -100,7 +106,12 @@ function TreeView:render() local state = self.state local rows = state.rows local theme = props.Theme - local style = theme:getStyle("Framework", self) + local style + if FlagsList:get("FFlagRefactorDevFrameworkTheme") then + style = props.Stylizer + else + style = theme:getStyle("Framework", self) + end local children = {} for index, row in ipairs(rows) do @@ -126,7 +137,8 @@ function TreeView:render() end ContextServices.mapToProps(TreeView, { - Theme = ContextServices.Theme, + Stylizer = FlagsList:get("FFlagRefactorDevFrameworkTheme") and ContextServices.Stylizer or nil, + Theme = (not FlagsList:get("FFlagRefactorDevFrameworkTheme")) and ContextServices.Theme or nil, }) return TreeView \ No newline at end of file diff --git a/BuiltInPlugins/LocalizationTools/Packages/Framework/UI/TreeView/style.lua b/BuiltInPlugins/LocalizationTools/Packages/Framework/UI/TreeView/style.lua index 5bdcc55094..0b00cf1a29 100644 --- a/BuiltInPlugins/LocalizationTools/Packages/Framework/UI/TreeView/style.lua +++ b/BuiltInPlugins/LocalizationTools/Packages/Framework/UI/TreeView/style.lua @@ -8,18 +8,31 @@ local UIFolderData = require(Framework.UI.UIFolderData) local ScrollingFrame = require(UIFolderData.ScrollingFrame.style) local RoundBox = require(UIFolderData.RoundBox.style) -return function(theme, getColor) - local roundBox = RoundBox(theme, getColor) - local scrollingFrame = ScrollingFrame(theme, getColor) +local FlagsList = Util.Flags.new({ + FFlagRefactorDevFrameworkTheme = {"RefactorDevFrameworkTheme"}, +}) - local Default = Style.new({ +if FlagsList:get("FFlagRefactorDevFrameworkTheme") then + return { Background = Decoration.RoundBox, - BackgroundStyle = roundBox.Default, - ScrollingFrame = Style.extend(scrollingFrame.Default, {}), + BackgroundStyle = RoundBox, + ScrollingFrame = ScrollingFrame, Padding = 1 - }) - - return { - Default = Default, } -end +else + return function(theme, getColor) + local roundBox = RoundBox(theme, getColor) + local scrollingFrame = ScrollingFrame(theme, getColor) + + local Default = Style.new({ + Background = Decoration.RoundBox, + BackgroundStyle = roundBox.Default, + ScrollingFrame = Style.extend(scrollingFrame.Default, {}), + Padding = 1 + }) + + return { + Default = Default, + } + end +end \ No newline at end of file diff --git a/BuiltInPlugins/LocalizationTools/Packages/Framework/UI/TreeView/test.spec.lua b/BuiltInPlugins/LocalizationTools/Packages/Framework/UI/TreeView/test.spec.lua index 46bd84fa70..8d06ee8a86 100644 --- a/BuiltInPlugins/LocalizationTools/Packages/Framework/UI/TreeView/test.spec.lua +++ b/BuiltInPlugins/LocalizationTools/Packages/Framework/UI/TreeView/test.spec.lua @@ -8,6 +8,13 @@ return function() local TreeView = require(script.Parent) local TextLabel = require(Framework.UI.TextLabel) + local StudioTheme = require(Framework.Style.Themes.StudioTheme) + + local Util = require(Framework.Util) + local FlagsList = Util.Flags.new({ + FFlagRefactorDevFrameworkTheme = {"RefactorDevFrameworkTheme"}, + }) + local items = { { name = "Workspace", @@ -54,11 +61,16 @@ return function() } local function createTreeView() - local theme = Theme.new(function() - return { - Framework = FrameworkStyles.new(), - } - end) + local theme + if FlagsList:get("FFlagRefactorDevFrameworkTheme") then + theme = StudioTheme.mock() + else + theme = Theme.new(function() + return { + Framework = FrameworkStyles.new(), + } + end) + end return provide({theme}, { TreeView = Roact.createElement(TreeView, { RootItems = items, diff --git a/BuiltInPlugins/LocalizationTools/Packages/Framework/Util.lua b/BuiltInPlugins/LocalizationTools/Packages/Framework/Util.lua index 661416fbb9..6a3e12c089 100644 --- a/BuiltInPlugins/LocalizationTools/Packages/Framework/Util.lua +++ b/BuiltInPlugins/LocalizationTools/Packages/Framework/Util.lua @@ -12,11 +12,14 @@ return strict({ Cryo = require(script.Cryo), CrossPluginCommunication = require(script.CrossPluginCommunication), deepEqual = require(script.deepEqual), + deepJoin = require(script.deepJoin), + deepCopy = require(script.deepCopy), -- TODO DEVTOOLS-4459: Remove this export FitFrame = require(script.FitFrame), Flags = require(script.Flags), + getTestVariation = require(script.getTestVariation), Immutable = require(script.Immutable), LayoutOrderIterator = require(script.LayoutOrderIterator), Math = require(script.Math), @@ -25,6 +28,7 @@ return strict({ Symbol = require(script.Symbol), ThunkWithArgsMiddleware = require(script.ThunkWithArgsMiddleware), strict = strict, + tableCache = require(script.tableCache), -- Style and Theming Utilities Palette = require(script.Palette), @@ -36,4 +40,4 @@ return strict({ -- Document Generation and Type Enforcement Utilities Typecheck = require(script.Typecheck), -}) \ No newline at end of file +}) diff --git a/BuiltInPlugins/LocalizationTools/Packages/Framework/Util/CrossPluginCommunication.lua b/BuiltInPlugins/LocalizationTools/Packages/Framework/Util/CrossPluginCommunication.lua index 37caa1bece..d3b82f5783 100644 --- a/BuiltInPlugins/LocalizationTools/Packages/Framework/Util/CrossPluginCommunication.lua +++ b/BuiltInPlugins/LocalizationTools/Packages/Framework/Util/CrossPluginCommunication.lua @@ -22,10 +22,12 @@ CrossPluginCommunication.__index = CrossPluginCommunication CrossPluginCommunication.BASE_FOLDER_NAME = "CrossPluginCommunication" -function CrossPluginCommunication.new(pluginNamespace) +function CrossPluginCommunication.new(pluginNamespace, hostService) assert(t.string(pluginNamespace), "pluginNamespace must be a string") + assert(t.optional(t.instance(pluginNamespace)), "hostService must be an instance if defined") local self = { + hostService = hostService or RobloxPluginGuiService, pluginNamespace = pluginNamespace } @@ -49,7 +51,7 @@ function CrossPluginCommunication:ensureFolderExists(parent, name) end function CrossPluginCommunication:getNamespaceFolder() - local base = self:ensureFolderExists(RobloxPluginGuiService, self.BASE_FOLDER_NAME) + local base = self:ensureFolderExists(self.hostService, self.BASE_FOLDER_NAME) return self:ensureFolderExists(base, self.pluginNamespace) end diff --git a/BuiltInPlugins/LocalizationTools/Packages/Framework/Util/CrossPluginCommunication.spec.lua b/BuiltInPlugins/LocalizationTools/Packages/Framework/Util/CrossPluginCommunication.spec.lua index 60338f2b0c..f3c1035f50 100644 --- a/BuiltInPlugins/LocalizationTools/Packages/Framework/Util/CrossPluginCommunication.spec.lua +++ b/BuiltInPlugins/LocalizationTools/Packages/Framework/Util/CrossPluginCommunication.spec.lua @@ -1,5 +1,3 @@ -local RobloxPluginGuiService = game:GetService("RobloxPluginGuiService") - local Framework = script.Parent.Parent local CrossPluginCommunication = require(Framework.Util.CrossPluginCommunication) @@ -8,8 +6,10 @@ return function() -- TODO DEVTOOLS-4397: Move cleanup to afterEach hook after the dependency structure is fixed + local hostService = workspace + local function createTestComms() - return CrossPluginCommunication.new(TEST_NAMESPACE) + return CrossPluginCommunication.new(TEST_NAMESPACE, hostService) end describe("Invoke", function() @@ -32,7 +32,7 @@ return function() comms:OnInvoke("blah", function() end) - local namespace = RobloxPluginGuiService:FindFirstChild(CrossPluginCommunication.BASE_FOLDER_NAME):FindFirstChild(TEST_NAMESPACE) + local namespace = hostService:FindFirstChild(CrossPluginCommunication.BASE_FOLDER_NAME):FindFirstChild(TEST_NAMESPACE) assert(namespace) diff --git a/BuiltInPlugins/LocalizationTools/Packages/Framework/Util/Flags.lua b/BuiltInPlugins/LocalizationTools/Packages/Framework/Util/Flags.lua index beb3097029..508d25912e 100644 --- a/BuiltInPlugins/LocalizationTools/Packages/Framework/Util/Flags.lua +++ b/BuiltInPlugins/LocalizationTools/Packages/Framework/Util/Flags.lua @@ -94,7 +94,7 @@ function Flags.new(featuresMap, config) assert(type(config.shouldFetchLiveValues) == "boolean", "shouldFetchLiveValues expected to be a boolean") assert(type(config.defaultValueIfMissing) == "boolean", "Default values for flags must be a boolean") assert(type(featuresMap) == "table", "Flags.new expects a table mapping keys to flag names.") - local isMap = type(next(featuresMap)) == "nil" or type(next(featuresMap)) == "string" + local isMap = type((next(featuresMap))) == "nil" or type((next(featuresMap))) == "string" assert(isMap, "Flags.new expects a map of string keys.") local self = { @@ -186,4 +186,4 @@ function Flags:clearAllLocalOverrides() self.localOverrides = {} end -return Flags \ No newline at end of file +return Flags diff --git a/BuiltInPlugins/LocalizationTools/Packages/Framework/Util/Palette.spec.lua b/BuiltInPlugins/LocalizationTools/Packages/Framework/Util/Palette.spec.lua index 8794c80717..02911b17c7 100644 --- a/BuiltInPlugins/LocalizationTools/Packages/Framework/Util/Palette.spec.lua +++ b/BuiltInPlugins/LocalizationTools/Packages/Framework/Util/Palette.spec.lua @@ -1,5 +1,16 @@ return function() - local Palette = require(script.Parent.Palette) + + local Util = script.Parent + local Flags = require(Util.Flags) + local FlagsList = Flags.new({ + FFlagRefactorDevFrameworkTheme = {"RefactorDevFrameworkTheme"}, + }) + + if FlagsList:get("FFlagRefactorDevFrameworkTheme") then + return + end + + local Palette = require(Util.Palette) it("should be a table of color StyleValues", function() expect(Palette).to.be.ok() diff --git a/BuiltInPlugins/LocalizationTools/Packages/Framework/Util/Promise.lua b/BuiltInPlugins/LocalizationTools/Packages/Framework/Util/Promise.lua index ea1b998dc7..77dfacfe53 100644 --- a/BuiltInPlugins/LocalizationTools/Packages/Framework/Util/Promise.lua +++ b/BuiltInPlugins/LocalizationTools/Packages/Framework/Util/Promise.lua @@ -41,8 +41,6 @@ ]] -local FFlagDevFrameworkUnhandledPromiseRejections = game:DefineFastFlag("DevFrameworkUnhandledPromiseRejections", false) - local PROMISE_DEBUG = false -- If promise debugging is on, use a version of pcall that warns on failure. @@ -92,15 +90,13 @@ Promise.Status = { Rejected = "Rejected", } -if FFlagDevFrameworkUnhandledPromiseRejections then - --[[ - This can be overridden to change the global callback for unhandled rejections. +--[[ + This can be overridden to change the global callback for unhandled rejections. - By default it is disabled (set to nil) so that consumers can choose how to log - unhandled rejections, and not pollute the output (e.g. with "warn"). - ]] - Promise.onUnhandledRejection = nil -end + By default it is disabled (set to nil) so that consumers can choose how to log + unhandled rejections, and not pollute the output (e.g. with "warn"). +]] +Promise.onUnhandledRejection = nil --[[ Constructs a new Promise with the given initializing callback. @@ -143,12 +139,10 @@ function Promise.new(callback) -- Queues representing functions we should invoke when we update! _queuedResolve = {}, _queuedReject = {}, - } - if FFlagDevFrameworkUnhandledPromiseRejections then -- If an error occurs with no handlers, this will be set to true. - promise._unhandledRejection = false - end + _unhandledRejection = false, + } setmetatable(promise, Promise) @@ -259,9 +253,7 @@ end The given callbacks are invoked depending on that result. ]] function Promise:andThen(successHandler, failureHandler) - if FFlagDevFrameworkUnhandledPromiseRejections then - self._unhandledRejection = false - end + self._unhandledRejection = false -- Create a new promise to follow this part of the chain return Promise.new(function(resolve, reject) @@ -305,9 +297,7 @@ end This matches the execution model of normal Roblox functions. ]] function Promise:await() - if FFlagDevFrameworkUnhandledPromiseRejections then - self._unhandledRejection = false - end + self._unhandledRejection = false if self._status == Promise.Status.Started then local result @@ -334,6 +324,8 @@ function Promise:await() elseif self._status == Promise.Status.Rejected then error(tostring(self._value[1]), 2) end + + return end function Promise:_resolve(...) @@ -386,33 +378,31 @@ function Promise:_reject(...) callback(...) end else - if FFlagDevFrameworkUnhandledPromiseRejections then - self._unhandledRejection = true - local err = tostring((...)) - - -- At this point, no error handler is available. - -- An error handler might still be attached if the error occurred - -- synchronously. We'll wait one tick, and if there are still no - -- handlers, call the global onUnhandledRejection handler. - spawn(function() - -- The error was handled while we were waiting - if not self._unhandledRejection then - return - end + self._unhandledRejection = true + local err = tostring((...)) + + -- At this point, no error handler is available. + -- An error handler might still be attached if the error occurred + -- synchronously. We'll wait one tick, and if there are still no + -- handlers, call the global onUnhandledRejection handler. + spawn(function() + -- The error was handled while we were waiting + if not self._unhandledRejection then + return + end - local message = ("Unhandled promise rejection:\n\n%s\n\n%s"):format( - err, - self._source - ) + local message = ("Unhandled promise rejection:\n\n%s\n\n%s"):format( + err, + self._source + ) - -- Ignore failures in logging the rejection - pcall(function() - if Promise.onUnhandledRejection then - Promise.onUnhandledRejection(message) - end - end) + -- Ignore failures in logging the rejection + pcall(function() + if Promise.onUnhandledRejection then + Promise.onUnhandledRejection(message) + end end) - end + end) end end diff --git a/BuiltInPlugins/LocalizationTools/Packages/Framework/Util/Promise.spec.lua b/BuiltInPlugins/LocalizationTools/Packages/Framework/Util/Promise.spec.lua index 68497501d2..3c29f9c9a9 100644 --- a/BuiltInPlugins/LocalizationTools/Packages/Framework/Util/Promise.spec.lua +++ b/BuiltInPlugins/LocalizationTools/Packages/Framework/Util/Promise.spec.lua @@ -1,10 +1,6 @@ return function() - local Framework = script.Parent.Parent - local Promise = require(script.Parent.Promise) - local FFlagDevFrameworkUnhandledPromiseRejections = game:GetFastFlag("DevFrameworkUnhandledPromiseRejections") - describe("Promise.new", function() it("should instantiate with a callback", function() local promise = Promise.new(function() end) @@ -285,10 +281,6 @@ return function() -- onUnhandledRejection callback. describe("unhandled rejections", function() - if not FFlagDevFrameworkUnhandledPromiseRejections then - return - end - local calls local originalOnUnhandledRejection @@ -359,10 +351,8 @@ return function() expect(promise._unhandledRejection).to.equal(true) - local caught = false promise:catch(function(err) expect(err:find("it did not work")).to.be.ok() - caught = true end) waitUntilNextTick() diff --git a/BuiltInPlugins/LocalizationTools/Packages/Framework/Util/StyleModifier.lua b/BuiltInPlugins/LocalizationTools/Packages/Framework/Util/StyleModifier.lua index 2076dbf5ea..01ee072ce7 100644 --- a/BuiltInPlugins/LocalizationTools/Packages/Framework/Util/StyleModifier.lua +++ b/BuiltInPlugins/LocalizationTools/Packages/Framework/Util/StyleModifier.lua @@ -14,45 +14,12 @@ end) ]] -local doesNotExistError = [[ -The value '%s' does not exist in StyleModifier.]] - local Framework = script.Parent.Parent -local Symbol = require(Framework.Util.Symbol) -local Flags = require(Framework.Util.Flags) -local FlagsList = Flags.new({ - FFlagDevFrameworkEnumUtility = "DevFrameworkEnumUtility", +local enumerate = require(Framework.Util.enumerate) +return enumerate("StyleModifier", { + "Hover", + "Pressed", + "Selected", + "Disabled" }) - -if FlagsList:get("FFlagDevFrameworkEnumUtility") then - local enumerate = require(Framework.Util.enumerate) - return enumerate("StyleModifier", { - "Hover", - "Pressed", - "Selected", - "Disabled" - }) -else - local StyleModifier = { - Hover = Symbol.named("Hover"), - Pressed = Symbol.named("Pressed"), - Selected = Symbol.named("Selected"), - Disabled = Symbol.named("Disabled"), - } - - setmetatable(StyleModifier, { - __index = function(key) - if StyleModifier[key] then - return StyleModifier[key] - else - error(string.format(doesNotExistError, key)) - end - end, - __newindex = function(key) - error(string.format(doesNotExistError, key)) - end, - }) - - return StyleModifier -end diff --git a/BuiltInPlugins/LocalizationTools/Packages/Framework/Util/Symbol.lua b/BuiltInPlugins/LocalizationTools/Packages/Framework/Util/Symbol.lua index d9e26d9c65..2363bf1454 100644 --- a/BuiltInPlugins/LocalizationTools/Packages/Framework/Util/Symbol.lua +++ b/BuiltInPlugins/LocalizationTools/Packages/Framework/Util/Symbol.lua @@ -1,6 +1,5 @@ --[[ A 'Symbol' is an opaque marker type. - Symbols have the type 'userdata', but when printed to the console, the name of the symbol is shown. ]] @@ -9,7 +8,6 @@ local Symbol = {} --[[ Creates a Symbol with the given name. - When printed or coerced to a string, the symbol will turn into the string given as its name. ]] diff --git a/BuiltInPlugins/LocalizationTools/Packages/Framework/Util/Symbol.spec.lua b/BuiltInPlugins/LocalizationTools/Packages/Framework/Util/Symbol.spec.lua index cde9be065b..aa5ad93ef1 100644 --- a/BuiltInPlugins/LocalizationTools/Packages/Framework/Util/Symbol.spec.lua +++ b/BuiltInPlugins/LocalizationTools/Packages/Framework/Util/Symbol.spec.lua @@ -11,7 +11,7 @@ return function() it("should coerce to the given name", function() local symbol = Symbol.named("foo") - expect(tostring(symbol):find("foo")).to.be.ok() + expect((tostring(symbol):find("foo"))).to.be.ok() end) it("should be unique when constructed", function() @@ -42,4 +42,4 @@ return function() expect(symbolA).never.to.equal(symbolB) end) end) -end \ No newline at end of file +end diff --git a/BuiltInPlugins/LocalizationTools/Packages/Framework/Util/Typecheck/DocParser.lua b/BuiltInPlugins/LocalizationTools/Packages/Framework/Util/Typecheck/DocParser.lua index a7654d586d..c730d15d52 100644 --- a/BuiltInPlugins/LocalizationTools/Packages/Framework/Util/Typecheck/DocParser.lua +++ b/BuiltInPlugins/LocalizationTools/Packages/Framework/Util/Typecheck/DocParser.lua @@ -106,6 +106,7 @@ function DocParser:parseComments(comments) Required = {}, Optional = {}, Style = {}, + Summary = nil, } local parseMode = ParseMode.Summary diff --git a/BuiltInPlugins/LocalizationTools/Packages/Framework/Util/Typecheck/FrameworkTypes.lua b/BuiltInPlugins/LocalizationTools/Packages/Framework/Util/Typecheck/FrameworkTypes.lua index f0a8c83f91..3452568da5 100644 --- a/BuiltInPlugins/LocalizationTools/Packages/Framework/Util/Typecheck/FrameworkTypes.lua +++ b/BuiltInPlugins/LocalizationTools/Packages/Framework/Util/Typecheck/FrameworkTypes.lua @@ -9,7 +9,6 @@ local t = require(script.Parent.t) local FrameworkTypes = {} local Flags = require(Framework.Util.Flags) local FlagsList = Flags.new({ - FFlagDevFrameworkEnumUtility = "DevFrameworkEnumUtility", FFlagRefactorDevFrameworkContextItems = {"RefactorDevFrameworkContextItems"}, }) @@ -37,6 +36,14 @@ function FrameworkTypes.Theme(value) return true end +function FrameworkTypes.Stylizer(value) + local errMsg = "Stylizer expected, got %s." + if not t.table(value) or not t.callback(value.getConsumerItem) then + return false, errMsg:format(type(value)) + end + return true +end + function FrameworkTypes.Plugin(value) local errMsg = "Plugin expected, got %s." if not t.table(value) or not t.callback(value.get) then @@ -81,16 +88,8 @@ end function FrameworkTypes.StyleModifier(value) local errMsg = "StyleModifier expected, got %s." - if FlagsList:get("FFlagDevFrameworkEnumUtility") then - if StyleModifier.isEnumValue(value) then - return true - end - else - for _, v in pairs(StyleModifier) do - if value == v then - return true - end - end + if StyleModifier.isEnumValue(value) then + return true end return false, errMsg:format(type(value)) diff --git a/BuiltInPlugins/LocalizationTools/Packages/Framework/Util/Typecheck/t.lua b/BuiltInPlugins/LocalizationTools/Packages/Framework/Util/Typecheck/t.lua index 44c8d50af8..045d7ac0dd 100644 --- a/BuiltInPlugins/LocalizationTools/Packages/Framework/Util/Typecheck/t.lua +++ b/BuiltInPlugins/LocalizationTools/Packages/Framework/Util/Typecheck/t.lua @@ -1,3 +1,5 @@ +--!nocheck + --[[ DeveloperFramework uses t from Osyris https://github.com/osyrisrblx/t diff --git a/BuiltInPlugins/LocalizationTools/Packages/Framework/Util/Typecheck/wrap.lua b/BuiltInPlugins/LocalizationTools/Packages/Framework/Util/Typecheck/wrap.lua index 69b0b3a844..9384db05dd 100644 --- a/BuiltInPlugins/LocalizationTools/Packages/Framework/Util/Typecheck/wrap.lua +++ b/BuiltInPlugins/LocalizationTools/Packages/Framework/Util/Typecheck/wrap.lua @@ -21,7 +21,10 @@ Typecheck uses interfaces from Osyris's "t" library. For more info, see: https://github.com/osyrisrblx/t ]] - +local Util = script.Parent.Parent +local FlagsList = require(Util.Flags).new({ + FFlagRefactorDevFrameworkTheme = {"RefactorDevFrameworkTheme"}, +}) local DocParser = require(script.Parent.DocParser) local propsTraceback = [[%s @@ -52,7 +55,12 @@ local function validate(component, propsInterface, styleInterface) local success, errorMessage = propsInterface(self.props) if success then if styleInterface then - local style = self.props.Theme:getStyle("Framework", self) + local style + if FlagsList:get("FFlagRefactorDevFrameworkTheme") then + style = self.props.Stylizer + else + style = self.props.Theme:getStyle("Framework", self) + end success, errorMessage = styleInterface(style) if not success then errorMessage = styleTraceback:format(errorMessage, tostring(component)) diff --git a/BuiltInPlugins/LocalizationTools/Packages/Framework/Util/Typecheck/wrap.spec.lua b/BuiltInPlugins/LocalizationTools/Packages/Framework/Util/Typecheck/wrap.spec.lua index a9275f1b92..e9854468f0 100644 --- a/BuiltInPlugins/LocalizationTools/Packages/Framework/Util/Typecheck/wrap.spec.lua +++ b/BuiltInPlugins/LocalizationTools/Packages/Framework/Util/Typecheck/wrap.spec.lua @@ -3,7 +3,10 @@ Required Props: UDim2 Size: The size of the component. + + Optional Props: Theme Theme: The Theme ContextItem from mapToProps. + Stylizer Stylizer: A Stylizer ContextItem, which is provided via mapToProps. Style Values: Color3 Color: The color of the component. @@ -14,6 +17,11 @@ return function() local Roact = require(Framework.Parent.Roact) local ContextServices = require(Framework.ContextServices) local wrap = require(Framework.Util.Typecheck.wrap) + local StudioTheme = require(Framework.Style.Themes.StudioTheme) + local ui = require(Framework.Style.ComponentSymbols) + local FlagsList = require(Framework.Util.Flags).new({ + FFlagRefactorDevFrameworkTheme = {"RefactorDevFrameworkTheme"}, + }) local WrapTestComponent = Roact.PureComponent:extend("WrapTestComponent") wrap(WrapTestComponent, script) @@ -21,7 +29,12 @@ return function() function WrapTestComponent:render() local props = self.props local size = props.Size - local style = props.Theme:getStyle("Framework", self) + local style + if FlagsList:get("FFlagRefactorDevFrameworkTheme") then + style = props.Stylizer + else + style = props.Theme:getStyle("Framework", self) + end local color = style.Color return Roact.createElement("Frame", { @@ -31,19 +44,29 @@ return function() end ContextServices.mapToProps(WrapTestComponent, { - Theme = ContextServices.Theme, + Stylizer = FlagsList:get("FFlagRefactorDevFrameworkTheme") and ContextServices.Stylizer or nil, + Theme = (not FlagsList:get("FFlagRefactorDevFrameworkTheme")) and ContextServices.Theme or nil, }) local function createWrapTestComponent(props, styleTable) - local theme = ContextServices.Theme.new(function() - return { - Framework = { - WrapTestComponent = { - Default = styleTable, + local theme + if FlagsList:get("FFlagRefactorDevFrameworkTheme") then + theme = StudioTheme.mock() + ui:add("WrapTestComponent") + theme:extend({ + [ui.WrapTestComponent] = styleTable, + }) + else + theme = ContextServices.Theme.new(function() + return { + Framework = { + WrapTestComponent = { + Default = styleTable, + }, }, - }, - } - end) + } + end) + end return ContextServices.provide({theme}, { Test = Roact.createElement(WrapTestComponent, props), diff --git a/BuiltInPlugins/LocalizationTools/Packages/Framework/Util/deepCopy.lua b/BuiltInPlugins/LocalizationTools/Packages/Framework/Util/deepCopy.lua new file mode 100644 index 0000000000..9a1e54beea --- /dev/null +++ b/BuiltInPlugins/LocalizationTools/Packages/Framework/Util/deepCopy.lua @@ -0,0 +1,22 @@ +--[[ + Copies a table and it's metatables. + Used in Stylizer to prevent mutating tables that convert Symbols into color values. +--]] +local function deepCopy(t) + if type(t) ~= "table" or type(t.render) == "function" then + return t + end + local meta = getmetatable(t) + local target = {} + for k, v in pairs(t) do + if type(v) == "table" then + target[k] = deepCopy(v) + else + target[k] = v + end + end + setmetatable(target, meta) + return target +end + +return deepCopy \ No newline at end of file diff --git a/BuiltInPlugins/LocalizationTools/Packages/Framework/Util/deepCopy.spec.lua b/BuiltInPlugins/LocalizationTools/Packages/Framework/Util/deepCopy.spec.lua new file mode 100644 index 0000000000..97052d6ebe --- /dev/null +++ b/BuiltInPlugins/LocalizationTools/Packages/Framework/Util/deepCopy.spec.lua @@ -0,0 +1,37 @@ +return function() + local deepEqual = require(script.Parent.deepEqual) + local deepCopy = require(script.Parent.deepCopy) + + it("should fail when copied table and result are equal", function() + local originalTable = { + test = "hello" + } + + local copy = deepCopy(originalTable) + expect(copy).to.never.equal(originalTable) + end) + + it("should fail when copied inner tables are equal to the original", function() + local originalTable = { + string = "hello", + table = { + inner = "test", + } + } + + local copy = deepCopy(originalTable) + expect(copy.table).to.never.equal(originalTable.table) + end) + + it("should have all values in the table and its copy be equal", function() + local originalTable = { + string = "hello", + table = { + inner = "test", + } + } + local copy = deepCopy(originalTable) + expect(deepEqual(originalTable, copy)).to.equal(true) + end) + +end \ No newline at end of file diff --git a/BuiltInPlugins/LocalizationTools/Packages/Framework/Util/deepJoin.lua b/BuiltInPlugins/LocalizationTools/Packages/Framework/Util/deepJoin.lua new file mode 100644 index 0000000000..2151aa74b9 --- /dev/null +++ b/BuiltInPlugins/LocalizationTools/Packages/Framework/Util/deepJoin.lua @@ -0,0 +1,31 @@ +local function deepJoin(t1, t2) + local new = {} + + for key, value in pairs(t1) do + if typeof(value) == "table" then + if t2[key] and typeof(t2[key]) == "table" then + new[key] = deepJoin(value, t2[key]) + else + -- this essentially acts like a deepcopy to prevent + -- references getting all tangled up + new[key] = deepJoin(value, {}) + end + else + new[key] = value + end + end + + for key, value in pairs(t2) do + if typeof(value) == "table" then + if not t1[key] then + new[key] = deepJoin(value, {}) + end + else + new[key] = value + end + end + + return new +end + +return deepJoin diff --git a/BuiltInPlugins/LocalizationTools/Packages/Framework/Util/deepJoin.spec.lua b/BuiltInPlugins/LocalizationTools/Packages/Framework/Util/deepJoin.spec.lua new file mode 100644 index 0000000000..05d07cc90b --- /dev/null +++ b/BuiltInPlugins/LocalizationTools/Packages/Framework/Util/deepJoin.spec.lua @@ -0,0 +1,76 @@ +return function() + local Library = script.Parent + local deepJoin = require(Library.deepJoin) + + it("should join two tables together", function() + local tableA = {key1 = "Value1"} + local tableB = {key2 = "Value2"} + + local result = deepJoin(tableA, tableB) + + expect(result.key1).to.equal("Value1") + expect(result.key2).to.equal("Value2") + end) + + it("should add all the entries from the first table", function() + local tableA = {key1 = "Value1", key2 = "Value2"} + local tableB = {} + + local result = deepJoin(tableA, tableB) + + expect(result.key1).to.equal("Value1") + expect(result.key2).to.equal("Value2") + end) + + it("should add all the entries from the second table", function() + local tableA = {} + local tableB = {key1 = "Value1", key2 = "Value2"} + + local result = deepJoin(tableA, tableB) + + expect(result.key1).to.equal("Value1") + expect(result.key2).to.equal("Value2") + end) + + it("should join values in nested tables", function() + local tableA = { + set = { + key1 = "Value1", + }, + } + + local tableB = { + set = { + key2 = "Value2", + }, + } + + local result = deepJoin(tableA, tableB) + + expect(result.set).to.be.ok() + expect(result.set.key1).to.equal("Value1") + expect(result.set.key2).to.equal("Value2") + end) + + it("should prioritize the second table if values overlap", function() + local tableA = { + outsideKey = "Old", + set = { + insideKey = "Old", + }, + } + + local tableB = { + outsideKey = "New", + set = { + insideKey = "New", + }, + } + + local result = deepJoin(tableA, tableB) + + expect(result.outsideKey).to.equal("New") + expect(result.set).to.be.ok() + expect(result.set.insideKey).to.equal("New") + end) +end diff --git a/BuiltInPlugins/LocalizationTools/Packages/Framework/Util/getTestVariation.lua b/BuiltInPlugins/LocalizationTools/Packages/Framework/Util/getTestVariation.lua new file mode 100644 index 0000000000..d09873db1d --- /dev/null +++ b/BuiltInPlugins/LocalizationTools/Packages/Framework/Util/getTestVariation.lua @@ -0,0 +1,30 @@ +-- This function hides ABTestService away and provides a quick interface for getting a test's variation. +-- Should the requested test not exist, it will return the Control group. + +local ABTestService = game:GetService("ABTestService") + +local AB_TEST_GROUP_CONTROL = "Control" + +-- abTestName : (string) the name of test. ex) "AllUsers.Studio.ExampleTestName" +-- abTestService : (table, optional) an optional override for ABTestService +-- RETURNS : (int) the variation that the user is in. 0 = Control +return function(abTestName, abTestService) + assert(type(abTestName) == "string", "Expected abTestName to be a string") + if abTestService then + assert(type(abTestService.GetVariant) == "function", "Expected the abTestService object to have a GetVariant member function") + else + abTestService = ABTestService + end + + local variation = abTestService:GetVariant(abTestName) + if variation == AB_TEST_GROUP_CONTROL then + return 0 + else + local _, _, variationNumber = string.find(variation, "Variation(%d+)") + if not variationNumber then + return 0 + end + + return tonumber(variationNumber) + end +end diff --git a/BuiltInPlugins/LocalizationTools/Packages/Framework/Util/getTestVariation.spec.lua b/BuiltInPlugins/LocalizationTools/Packages/Framework/Util/getTestVariation.spec.lua new file mode 100644 index 0000000000..5febceee91 --- /dev/null +++ b/BuiltInPlugins/LocalizationTools/Packages/Framework/Util/getTestVariation.spec.lua @@ -0,0 +1,41 @@ +return function() + local getTestVariation = require(script.Parent.getTestVariation) + local VARIATION_NAME_CONTROL = "Control" + + local function createTestService(result) + local DebugABService = {} + + function DebugABService:GetVariant(testName) + return result + end + + return DebugABService + end + + it("should expect some test name", function() + expect(function() + getTestVariation() + end).to.throw() + end) + + it("should return zero when in the control group", function() + local debugABTestService = createTestService(VARIATION_NAME_CONTROL) + local variation = getTestVariation("AllUsers.Studio.ShouldShowZero", debugABTestService) + expect(variation).to.equal(0) + end) + + it("should return the number of the variation when not in the control group", function() + local debugABTestService = createTestService("Variation1") + local variation = getTestVariation("AllUsers.Studio.ShouldShowNumber", debugABTestService) + expect(variation).to.equal(1) + + -- there aren't any variations past 8, but it should work with any number + debugABTestService = createTestService("Variation23") + variation = getTestVariation("AllUsers.Studio.ShouldShowNumber", debugABTestService) + expect(variation).to.equal(23) + + debugABTestService = createTestService("Variation456") + variation = getTestVariation("AllUsers.Studio.ShouldShowNumber", debugABTestService) + expect(variation).to.equal(456) + end) +end \ No newline at end of file diff --git a/BuiltInPlugins/LocalizationTools/Packages/Framework/Util/tableCache.lua b/BuiltInPlugins/LocalizationTools/Packages/Framework/Util/tableCache.lua new file mode 100644 index 0000000000..34e844e1f2 --- /dev/null +++ b/BuiltInPlugins/LocalizationTools/Packages/Framework/Util/tableCache.lua @@ -0,0 +1,31 @@ +--[[ + Returns a table of unique values keyed on name for each component. +]] + +local function createTableCache(tableName) + local UniqueTable = {} + + UniqueTable.__tostring = function(t) + return ("%s(%s)"):format(tableName, t.name) + end + + function UniqueTable:add(name) + assert(type(name) == "string", ("%s must be created using a string name!"):format(tableName)) + + if rawget(UniqueTable, name) then + return rawget(UniqueTable, name) + else + local result = setmetatable({ + name = name + }, UniqueTable) + + UniqueTable[name] = result + + return result + end + end + + return UniqueTable +end + +return createTableCache \ No newline at end of file diff --git a/BuiltInPlugins/LocalizationTools/Packages/Framework/Util/tableCache.spec.lua b/BuiltInPlugins/LocalizationTools/Packages/Framework/Util/tableCache.spec.lua new file mode 100644 index 0000000000..3c8f879956 --- /dev/null +++ b/BuiltInPlugins/LocalizationTools/Packages/Framework/Util/tableCache.spec.lua @@ -0,0 +1,40 @@ +return function() + local tableCache = require(script.Parent.tableCache) + local TABLE_CACHE_NAME = "tableCacheTest" + + describe("add", function() + it("should coerce to the given name", function() + local symbol = tableCache(TABLE_CACHE_NAME):add("foo") + expect((tostring(symbol):find("foo"))).to.be.ok() + end) + + it("should have table entires", function() + local cache = tableCache(TABLE_CACHE_NAME) + local testA = cache:add("abc") + expect(typeof(testA)).to.equal("table") + expect(tostring(testA)).to.equal("tableCacheTest(abc)") + end) + + it("should not have duplicate entries", function() + local cache = tableCache(TABLE_CACHE_NAME .. "new") + local testA = cache:add("abc") + local testB = cache:add("abc") + expect(testA).to.equal(testB) + + local count = 0 + for _,v in pairs(cache) do + if typeof(v) ~= "function" then + count = count + 1 + end + end + expect(count).to.equal(1) + end) + + it("should get the same entry for the same lookup", function() + local cache = tableCache(TABLE_CACHE_NAME) + local testA = cache["abc"] + local testB = cache["abc"] + expect(testA).to.equal(testB) + end) + end) +end diff --git a/BuiltInPlugins/LocalizationTools/Packages/UILibrary.lua b/BuiltInPlugins/LocalizationTools/Packages/UILibrary.lua index 7769f987a4..08a7c2267b 100644 --- a/BuiltInPlugins/LocalizationTools/Packages/UILibrary.lua +++ b/BuiltInPlugins/LocalizationTools/Packages/UILibrary.lua @@ -4,7 +4,7 @@ local FFlagEnableToolboxVideos = game:GetFastFlag("EnableToolboxVideos") -local Src = script +local Src = script._internal local Components = Src.Components local Utils = Src.Utils @@ -24,8 +24,6 @@ local Favorites = require(Components.Preview.Favorites) local ImagePreview = require(Components.Preview.ImagePreview) local AudioPreview = require(Components.Preview.AudioPreview) local AudioControl = FFlagEnableToolboxVideos and nil or require(Components.Preview.AudioControl) --- TODO FFlagRemoveUILibraryTimeline remove import -local Keyframe = require(Components.Timeline.Keyframe) local InfiniteScrollingFrame = require(Components.InfiniteScrollingFrame) local LoadingBar = require(Components.LoadingBar) local LoadingIndicator = require(Components.LoadingIndicator) @@ -35,8 +33,6 @@ local RadioButtons = require(Components.RadioButtons) local RoundFrame = require(Components.RoundFrame) local RoundTextBox = require(Components.RoundTextBox) local RoundTextButton = require(Components.RoundTextButton) --- TODO FFlagRemoveUILibraryTimeline remove import -local Scrubber = require(Components.Timeline.Scrubber) local SearchBar = require(Components.SearchBar) local Separator = require(Components.Separator) local StyledDialog = require(Components.StyledDialog) @@ -69,9 +65,6 @@ local Signal = require(Utils.Signal) local Dialog = require(Components.PluginWidget.Dialog) -game:DefineFastFlag("RemoveUILibraryTimeline", false) -local FFlagRemoveUILibraryTimeline = game:GetFastFlag("RemoveUILibraryTimeline") - local function createStrictTable(t) return setmetatable(t, { __index = function(_, index) @@ -100,7 +93,6 @@ local UILibrary = createStrictTable({ AudioPreview = AudioPreview, AudioControl = AudioControl, InfiniteScrollingFrame = InfiniteScrollingFrame, - Keyframe = (not FFlagRemoveUILibraryTimeline) and Keyframe or nil, LoadingBar = LoadingBar, LoadingIndicator = LoadingIndicator, ModelPreview = ModelPreview, @@ -109,7 +101,6 @@ local UILibrary = createStrictTable({ RoundFrame = RoundFrame, RoundTextBox = RoundTextBox, RoundTextButton = RoundTextButton, - Scrubber = (not FFlagRemoveUILibraryTimeline) and Scrubber or nil, SearchBar = SearchBar, Separator = Separator, StyledDialog = StyledDialog, @@ -164,14 +155,4 @@ local UILibrary = createStrictTable({ createTheme = require(Src.createTheme), }) -local virtualFolder = Instance.new("Folder") -virtualFolder.Name = "UILibraryInternals-Do-Not-Access-Directly" --- The number of parents to the plugin cannot change since UILibrary components reach out of UILibrary --- to get the plugin's copy of Roact -virtualFolder.Parent = script.Parent - -for _,v in pairs(script:GetChildren()) do - v.Parent = virtualFolder -end - return UILibrary \ No newline at end of file diff --git a/BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Camera.lua b/BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Camera.lua new file mode 100644 index 0000000000..2075f7aba5 --- /dev/null +++ b/BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Camera.lua @@ -0,0 +1,29 @@ +local Library = script.Parent + +local Roact = require(Library.Parent.Parent.Roact) +local Symbol = require(Library.Utils.Symbol) +local cameraKey = Symbol.named("MarkeplaceCamera") + +local CameraProvider = Roact.PureComponent:extend("CameraProvider") + +function CameraProvider:init(prop) + local camera = Instance.new("Camera") + camera.Name = "MarketplaceCamera" + + self._context[cameraKey] = camera +end + +function CameraProvider:render() + return Roact.oneChild(self.props[Roact.Children]) +end + +local function getCamera(component) + assert(component._context[cameraKey] ~= nil, "No CameraProvider Found") + local camera = component._context[cameraKey] + return camera +end + +return { + Provider = CameraProvider, + getCamera = getCamera, +} \ No newline at end of file diff --git a/BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Components/BaseDialog.lua b/BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Components/BaseDialog.lua new file mode 100644 index 0000000000..edb28acf30 --- /dev/null +++ b/BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Components/BaseDialog.lua @@ -0,0 +1,128 @@ +--[[ + A basic dialog with content and a set of buttons. + Other dialogs can use this component to provide more specific implementations. + While this component allows the creation of any arbitrary buttons, in most + cases a StyledDialog is preferred if the normal UILibrary buttons are desired. + + Required Props: + array Buttons = An array of items used to render + the buttons for this dialog. + function RenderButton(button, index, activated) = A function + used to render a button. This function is called for each + item in the Buttons array. It should return a Roact component + that connects a signal to the activated parameter. + + Props: + Vector2 Size = The starting size of the dialog. + Vector2 MinSize = The minimum size of the dialog, if it is resizable. + bool Resizable = Whether the dialog can be resized. + int BorderPadding = The padding to add around the edges of the dialog. + int ButtonPadding = The padding to add between buttons. + int ButtonHeight = The height of the buttons in the dialog, in pixels. + string Title = The title to display at the top of the window. + + function OnClose = A callback for when the user closed the dialog by + clicking the X in the corner of the window. + function OnButtonClicked(button) = A callback for when the user clicked + a button in the dialog. Returns the button that was clicked. +]] + +local Library = script.Parent.Parent +local Roact = require(Library.Parent.Parent.Roact) + +local Theming = require(Library.Theming) +local withTheme = Theming.withTheme + +local Dialog = require(Library.Components.PluginWidget.Dialog) + +local BaseDialog = Roact.PureComponent:extend("BaseDialog") + +function BaseDialog:init() + self.buttonClicked = function(button) + if self.props.OnButtonClicked then + self.props.OnButtonClicked(button) + end + end +end + +function BaseDialog:render() + return withTheme(function(theme) + local props = self.props + + local title = props.Title + local size = props.Size + local minSize = props.MinSize + local resizable = props.Resizable + local borderPadding = props.BorderPadding or 0 + + local buttons = props.Buttons + local buttonPadding = props.ButtonPadding or 0 + local buttonHeight = props.ButtonHeight or 0 + local renderButton = props.RenderButton + + assert(buttons ~= nil and type(buttons) == "table", + "BaseDialog requires a Buttons table.") + assert(renderButton ~= nil and type(renderButton) == "function", + "BaseDialog requires a RenderButton function.") + assert(buttonHeight ~= nil and type(buttonHeight) == "number", + "BaseDialog requires a ButtonHeight value.") + + local buttonComponents = { + Layout = Roact.createElement("UIListLayout", { + SortOrder = Enum.SortOrder.LayoutOrder, + FillDirection = Enum.FillDirection.Horizontal, + HorizontalAlignment = Enum.HorizontalAlignment.Center, + Padding = UDim.new(0, buttonPadding), + }), + } + + for index, button in ipairs(buttons) do + table.insert(buttonComponents, renderButton(button, index, function() + self.buttonClicked(button) + end)) + end + + return Roact.createElement(Dialog, { + Options = { + Size = size, + Resizable = resizable, + MinSize = minSize, + Modal = true, + InitialEnabled = true, + }, + Title = title, + OnClose = props.OnClose, + }, { + Background = Roact.createElement("Frame", { + Size = UDim2.new(1, 0, 1, 0), + BorderSizePixel = 0, + BackgroundColor3 = theme.dialog.background, + }, { + Padding = Roact.createElement("UIPadding", { + PaddingTop = UDim.new(0, borderPadding), + PaddingBottom = UDim.new(0, borderPadding), + PaddingLeft = UDim.new(0, borderPadding), + PaddingRight = UDim.new(0, borderPadding), + }), + + Content = Roact.createElement("Frame", { + LayoutOrder = 1, + Size = UDim2.new(1, 0, 1, -(buttonHeight + borderPadding)), + AnchorPoint = Vector2.new(0.5, 0), + Position = UDim2.new(0.5, 0, 0, 0), + BackgroundTransparency = 1, + }, self.props[Roact.Children]), + + Buttons = Roact.createElement("Frame", { + LayoutOrder = 2, + Size = UDim2.new(1, 0, 0, buttonHeight), + AnchorPoint = Vector2.new(0.5, 1), + Position = UDim2.new(0.5, 0, 1, 0), + BackgroundTransparency = 1, + }, buttonComponents), + }) + }) + end) +end + +return BaseDialog diff --git a/BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Components/BaseDialog.spec.lua b/BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Components/BaseDialog.spec.lua new file mode 100644 index 0000000000..ff5f454355 --- /dev/null +++ b/BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Components/BaseDialog.spec.lua @@ -0,0 +1,121 @@ +return function() + local Library = script.Parent.Parent + local Roact = require(Library.Parent.Parent.Roact) + + local MockWrapper = require(Library.MockWrapper) + + local BaseDialog = require(script.Parent.BaseDialog) + + local function createTestBaseDialog(props, children, container) + return Roact.createElement(MockWrapper, { + Container = container, + }, { + BaseDialog = Roact.createElement(BaseDialog, props, children) + }) + end + + it("should create and destroy without errors", function() + local element = createTestBaseDialog({ + Buttons = {}, + RenderButton = function(item) + end, + }) + local instance = Roact.mount(element) + Roact.unmount(instance) + end) + + it("should render correctly", function() + local container = Instance.new("Folder") + + local element = createTestBaseDialog({ + Buttons = {}, + RenderButton = function(item) + end, + }, {}, container) + + local instance = Roact.mount(element, container) + + local gui = container:FindFirstChildOfClass("BillboardGui") + expect(gui).to.be.ok() + expect(gui.FocusProvider).to.be.ok() + expect(gui.FocusProvider.Padding).to.be.ok() + expect(gui.FocusProvider.Content).to.be.ok() + expect(gui.FocusProvider.Buttons).to.be.ok() + + Roact.unmount(instance) + end) + + it("should require a Buttons table", function() + local element = createTestBaseDialog({ + RenderButton = function(item) + end, + }) + expect(function() + Roact.mount(element) + end).to.throw() + + element = createTestBaseDialog({ + Buttons = true, + RenderButton = function(item) + end, + }) + expect(function() + Roact.mount(element) + end).to.throw() + end) + + it("should require a RenderButtons function", function() + local element = createTestBaseDialog({ + Buttons = {}, + }) + expect(function() + Roact.mount(element) + end).to.throw() + + element = createTestBaseDialog({ + Buttons = {}, + RenderButton = true, + }) + expect(function() + Roact.mount(element) + end).to.throw() + end) + + it("should render its buttons", function() + local container = Instance.new("Folder") + + local element = createTestBaseDialog({ + Buttons = {"Frame"}, + RenderButton = function() + return Roact.createElement("Frame") + end, + }, {}, container) + + local instance = Roact.mount(element, container) + + local gui = container:FindFirstChildOfClass("BillboardGui") + local buttonContainer = gui.FocusProvider.Buttons + expect(buttonContainer["1"]).to.be.ok() + + Roact.unmount(instance) + end) + + it("should render its children", function() + local container = Instance.new("Folder") + + local element = createTestBaseDialog({ + Buttons = {}, + RenderButton = function() + end, + }, { + Frame = Roact.createElement("Frame"), + }, container) + + local instance = Roact.mount(element, container) + + local gui = container:FindFirstChildOfClass("BillboardGui") + expect(gui.FocusProvider.Content.Frame).to.be.ok() + + Roact.unmount(instance) + end) +end \ No newline at end of file diff --git a/BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Components/BulletPoint.lua b/BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Components/BulletPoint.lua new file mode 100644 index 0000000000..8721cc0e00 --- /dev/null +++ b/BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Components/BulletPoint.lua @@ -0,0 +1,93 @@ +--[[ + A line of text prefaced with a bullet point. Useful for lists of entries. + + Props: + string Text = The text to display after the bullet point + int LayoutOrder = Order in which the element is placed + int TextSize = The size of text + bool TextWrapped = Sets text wrapped + bool TextTruncate = Sets text truncate +]] + +local TextService = game:GetService("TextService") + +local Library = script.Parent.Parent +local Roact = require(Library.Parent.Parent.Roact) + +local Theming = require(Library.Theming) +local withTheme = Theming.withTheme + +local BulletPoint = Roact.PureComponent:extend("BulletPoint") + +local TEXT_SIZE = 20 + +function BulletPoint:init() + self.frameRef = Roact.createRef() + self.textConnection = nil + + self.updateFrameSize = function() + local frame = self.frameRef.current + local sizeX = frame.AbsoluteSize.x + local textSize = TextService:GetTextSize( + self.props.Text, + self.props.TextSize or TEXT_SIZE, + Enum.Font.SourceSans, + Vector2.new(sizeX, math.huge) + ) + frame.Size = UDim2.new(1, 0, 0, textSize.y) + end +end + +function BulletPoint:didMount() + local frame = self.frameRef.current + self.textConnection = frame:GetPropertyChangedSignal("AbsoluteSize"):connect(self.updateFrameSize) + self.updateFrameSize() +end + +function BulletPoint:willUnmount() + self.textConnection:Disconnect() + self.textConnection = nil +end + +function BulletPoint:render() + return withTheme(function(theme) + + local textSize = self.props.TextSize or TEXT_SIZE + local text = self.props.Text or "" + + return Roact.createElement("Frame", { + BackgroundTransparency = 1, + LayoutOrder = self.props.LayoutOrder or 1, + Size = UDim2.new(1, 0, 0, 0), + + [Roact.Ref] = self.frameRef, + }, { + Text = Roact.createElement("TextLabel", { + BackgroundTransparency = 1, + Position = UDim2.new(0, 16, 0, -1), + Size = UDim2.new(1, -16, 1, 0), + Text = text, + Font = theme.bulletPoint.font, + TextColor3 = theme.bulletPoint.text, + TextXAlignment = Enum.TextXAlignment.Left, + TextYAlignment = Enum.TextYAlignment.Top, + TextWrapped = self.props.TextWrapped or nil, + TextSize = textSize, + TextTruncate = self.props.TextTruncate or nil, + }), + + Dot = Roact.createElement("TextLabel", { + BackgroundTransparency = 1, + Size = UDim2.new(0, 4, 0, 4), + AnchorPoint = Vector2.new(0, 0.5), + TextColor3 = theme.bulletPoint.text, + Text = "•", + TextYAlignment = Enum.TextYAlignment.Top, + Font = theme.bulletPoint.font, + TextSize = textSize, + }), + }) + end) +end + +return BulletPoint diff --git a/BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Components/BulletPoint.spec.lua b/BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Components/BulletPoint.spec.lua new file mode 100644 index 0000000000..f2ad833346 --- /dev/null +++ b/BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Components/BulletPoint.spec.lua @@ -0,0 +1,36 @@ +return function() + local Library = script.Parent.Parent + local Roact = require(Library.Parent.Parent.Roact) + + local MockWrapper = require(Library.MockWrapper) + + local BulletPoint = require(script.Parent.BulletPoint) + + local function createTestBulletPoint(container) + return Roact.createElement(MockWrapper, { + Container = container, + }, { + BulletPoint = Roact.createElement(BulletPoint, { + Text = "test", + TextSize = 20, + }), + }) + end + + it("should create and destroy without errors", function() + local element = createTestBulletPoint() + local instance = Roact.mount(element) + Roact.unmount(instance) + end) + + it("should render correctly", function() + local container = Instance.new("Folder") + local instance = Roact.mount(createTestBulletPoint(container), container) + local bulletPoint = container:FindFirstChildOfClass("Frame") + + expect(bulletPoint.Text).to.be.ok() + expect(bulletPoint.Dot).to.be.ok() + + Roact.unmount(instance) + end) +end \ No newline at end of file diff --git a/BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Components/Button.lua b/BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Components/Button.lua new file mode 100644 index 0000000000..65825419c4 --- /dev/null +++ b/BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Components/Button.lua @@ -0,0 +1,142 @@ +--!nolint UnknownGlobal +--^ DEVTOOLS-4930 + +--[[ + A button with rounded corners. Colors are based on Theme. + + Required Props: + function RenderContents(theme, hovered) = A function that returns the + contents that will display in the button. The parameters passed + allow the function to style the contents based on the button's current + theme and/or produce different contents if the button is hovered. + + Props: + string Style = The theme to use for this button. Ex. "Default", "Primary". + Styles for buttons can be found in createTheme.lua. + string StyleState = Normally controlled by the button (e.g. hovered), but can + be overwritten with something like 'disabled' to pull from override themes + + UDim2 Size = The size of the button. + UDim2 Position = The position of the button. + Vector2 AnchorPoint = The center point of the button. + int LayoutOrder = The order in which this button appears in a UILayout. + int ZIndex = The display index of this button. + int BorderSizePixel = Border size of the button +]] + +local Library = script.Parent.Parent + +local Roact = require(Library.Parent.Parent.Roact) + +local Theming = require(Library.Theming) +local withTheme = Theming.withTheme +local join = require(Library.join) + +local RoundFrame = require(Library.Components.RoundFrame) + +local Button = Roact.PureComponent:extend("Button") + +function Button:init(initialProps) + self.state = { + hovered = false, + pressed = false + } + + self.onClick = function() + if self.props.OnClick then + self.props.OnClick() + end + end + + self.mouseEnter = function() + self:setState({ + hovered = true, + }) + end + + self.mouseLeave = function() + self:setState({ + hovered = false, + pressed = false + }) + end + + self.onMouseDown = function() + self:setState({ + hovered = true, + pressed = true, + }) + end + + self.onMouseUp = function() + self:setState({ + pressed = false, + }) + end +end + +function Button:render() + return withTheme(function(theme) + local props = self.props + local state = self.state + local hovered = state.hovered + local style = props.Style + local styleState = props.StyleState + local size = props.Size + local position = props.Position + local anchorPoint = props.AnchorPoint + local layoutOrder = props.LayoutOrder + local renderContents = props.RenderContents + local zIndex = props.ZIndex + local borderSize = props.BorderSizePixel + + assert(renderContents ~= nil and type(renderContents) == "function", + "Button requires a RenderContents function.") + + local buttonTheme = style and theme.button[style] or theme.button.Default + if styleState then + buttonTheme = join(buttonTheme, buttonTheme[styleState]) + elseif pressed then + buttonTheme = join(buttonTheme, buttonTheme.pressed) + elseif hovered then + buttonTheme = join(buttonTheme, buttonTheme.hovered) + end + + local isRound = buttonTheme.isRound + local content = renderContents(buttonTheme, hovered, pressed) + + local buttonProps = { + Size = size, + Position = position, + AnchorPoint = anchorPoint, + LayoutOrder = layoutOrder, + ZIndex = zIndex, + + BackgroundColor3 = buttonTheme.backgroundColor, + BorderColor3 = buttonTheme.borderColor, + BorderSizePixel = borderSize, + } + + if isRound then + return Roact.createElement(RoundFrame, join(buttonProps, { + OnActivated = self.onClick, + OnMouseEnter = self.mouseEnter, + OnMouseLeave = self.mouseLeave, + [Roact.Event.MouseButton1Down] = self.onMouseDown, + [Roact.Event.MouseButton1Up] = self.onMouseUp, + }), content) + else + return Roact.createElement("ImageButton", join(buttonProps, { + AutoButtonColor = false, + + [Roact.Event.MouseEnter] = self.mouseEnter, + [Roact.Event.MouseLeave] = self.mouseLeave, + [Roact.Event.Activated] = self.onClick, + [Roact.Event.MouseButton1Down] = self.onMouseDown, + [Roact.Event.MouseButton1Up] = self.onMouseUp, + }), content) + end + end) +end + +return Button diff --git a/BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Components/Button.spec.lua b/BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Components/Button.spec.lua new file mode 100644 index 0000000000..3a47c1896a --- /dev/null +++ b/BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Components/Button.spec.lua @@ -0,0 +1,79 @@ +return function() + local Library = script.Parent.Parent + local Roact = require(Library.Parent.Parent.Roact) + + local MockWrapper = require(Library.MockWrapper) + + local Button = require(script.Parent.Button) + + local function createTestButton(props, container) + return Roact.createElement(MockWrapper, { + Container = container, + }, { + Button = Roact.createElement(Button, props) + }) + end + + it("should create and destroy without errors", function() + local element = createTestButton({ + RenderContents = function() + end, + }) + local instance = Roact.mount(element) + Roact.unmount(instance) + end) + + it("should render correctly", function() + local container = Instance.new("Folder") + + local element = createTestButton({ + RenderContents = function() + end, + }, container) + + local instance = Roact.mount(element, container) + + local button = container:FindFirstChildOfClass("ImageButton") + expect(button).to.be.ok() + expect(button.Border).to.be.ok() + + Roact.unmount(instance) + end) + + it("should require a RenderContents function", function() + local element = createTestButton() + expect(function() + Roact.mount(element) + end).to.throw() + + element = createTestButton({ + RenderContents = true, + }) + expect(function() + Roact.mount(element) + end).to.throw() + end) + + it("should render the children in RenderContents", function() + local container = Instance.new("Folder") + + local element = createTestButton({ + RenderContents = function() + return { + SomeFrame = Roact.createElement("Frame"), + OtherFrame = Roact.createElement("Frame"), + } + end, + }, container) + + local instance = Roact.mount(element, container) + + local button = container:FindFirstChildOfClass("ImageButton") + expect(button).to.be.ok() + expect(button.Border).to.be.ok() + expect(button.Border.SomeFrame).to.be.ok() + expect(button.Border.OtherFrame).to.be.ok() + + Roact.unmount(instance) + end) +end \ No newline at end of file diff --git a/BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Components/CheckBox.lua b/BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Components/CheckBox.lua new file mode 100644 index 0000000000..8c8bf6f16e --- /dev/null +++ b/BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Components/CheckBox.lua @@ -0,0 +1,96 @@ +--[[ + Clickable checkbox, from a CheckBoxSet. + + Props: + string Id = Unique identifier of this CheckBox + string Title = Text to display on this CheckBox + bool Selected = Whether to display this CheckBox as selected + bool Enabled = Whether this CheckBox accepts input + int Height = How big the CheckBox should be + int TextSize = How big the CheckBox's text should be + func OnActivated = What happens when the CheckBox is clicked + int titlePadding = How many pixels to the right of the icon the title is put +]] + +local TextService = game:GetService("TextService") + +local Library = script.Parent.Parent +local Roact = require(Library.Parent.Parent.Roact) +local Theming = require(Library.Theming) +local withTheme = Theming.withTheme + +local CheckBox = Roact.PureComponent:extend("CheckBox") + +function CheckBox:init() + self.onActivated = function() + if self.props.Enabled then + self.props.OnActivated() + end + end +end + +function CheckBox:render() + return withTheme(function(theme) + local props = self.props + + local title = props.Title + local height = props.Height + local enabled = props.Enabled + local layoutOrder = props.LayoutOrder + local selected = props.Selected + local textSize = props.TextSize + local titlePadding = props.TitlePadding or 5 + + local titleSize = TextService:GetTextSize( + title, + textSize, + theme.checkBox.font, + Vector2.new() + ) + local titleWidth = titleSize.X + + return Roact.createElement("Frame", { + Size = UDim2.new(1, 0, 0, height), + BackgroundTransparency = 1, + BorderSizePixel = 0, + LayoutOrder = layoutOrder or 1, + }, { + Background = Roact.createElement("ImageButton", { + Size = UDim2.new(0, height, 0, height), + BackgroundTransparency = 1, + ImageTransparency = enabled and 0 or 0.4, + Image = theme.checkBox.backgroundImage, + ImageColor3 = theme.checkBox.backgroundColor, + + [Roact.Event.Activated] = self.onActivated, + }, { + Selection = Roact.createElement("ImageLabel", { + BackgroundTransparency = 1, + Size = UDim2.new(1, 0, 1, 0), + Visible = enabled and selected, + Image = theme.checkBox.selectedImage, + }), + + TitleLabel = Roact.createElement("TextButton", { + BackgroundTransparency = 1, + BorderSizePixel = 0, + Size = UDim2.new(0, titleWidth, 1, 0), + AnchorPoint = Vector2.new(0, 0.5), + Position = UDim2.new(1, titlePadding, 0.5, 0), + + TextColor3 = theme.checkBox.titleColor, + Font = theme.checkBox.font, + TextSize = textSize, + TextXAlignment = Enum.TextXAlignment.Left, + TextYAlignment = Enum.TextYAlignment.Center, + TextTransparency = enabled and 0 or 0.5, + Text = title, + + [Roact.Event.Activated] = self.onActivated, + }), + }), + }) + end) +end + +return CheckBox \ No newline at end of file diff --git a/BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Components/CheckBox.spec.lua b/BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Components/CheckBox.spec.lua new file mode 100644 index 0000000000..2a2d149eb1 --- /dev/null +++ b/BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Components/CheckBox.spec.lua @@ -0,0 +1,64 @@ +return function() + local Library = script.Parent.Parent + local Roact = require(Library.Parent.Parent.Roact) + + local MockWrapper = require(Library.MockWrapper) + + local CheckBox = require(script.Parent.CheckBox) + + local function createTestCheckBox(enabled, selected) + return Roact.createElement(MockWrapper, {}, { + checkBox = Roact.createElement(CheckBox, { + Title = "Title", + TextSize = 24, + Enabled = enabled, + Selected = selected, + OnClicked = function() + end, + }) + }) + end + + it("should create and destroy without errors", function() + local element = createTestCheckBox(true, false) + local instance = Roact.mount(element) + Roact.unmount(instance) + end) + + it("should render correctly", function () + local container = Instance.new("Folder") + local instance = Roact.mount(createTestCheckBox(true, false), container) + local frame = container:FindFirstChildOfClass("Frame") + + expect(frame.Background).to.be.ok() + expect(frame.Background.Selection).to.be.ok() + expect(frame.Background.TitleLabel).to.be.ok() + + Roact.unmount(instance) + end) + + it("should change color when highlighted", function () + local container = Instance.new("Folder") + + -- selected + local instance = Roact.mount(createTestCheckBox(true, true), container) + local frame = container:FindFirstChildOfClass("Frame") + expect(frame.Background.Selection.Visible).to.equal(true) + + -- unselected + instance = Roact.update(instance, createTestCheckBox(true, false)) + expect(frame.Background.Selection.Visible).to.equal(false) + Roact.unmount(instance) + end) + + it("should gray out when disabled", function () + local container = Instance.new("Folder") + local instance = Roact.mount(createTestCheckBox(false, true), container) + local frame = container:FindFirstChildOfClass("Frame") + + expect(frame.Background.Selection.Visible).to.equal(false) + expect(frame.Background.TitleLabel.TextTransparency).never.to.equal(0) + + Roact.unmount(instance) + end) +end \ No newline at end of file diff --git a/BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Components/DetailedDropdown.lua b/BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Components/DetailedDropdown.lua new file mode 100644 index 0000000000..9221875dd1 --- /dev/null +++ b/BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Components/DetailedDropdown.lua @@ -0,0 +1,353 @@ +--[[ + A dropdown menu styled to match the Roblox Studio start page. + Consists of a button used to open the dropdown as well as the menu itself. + Note that the logic for opening and closing the menu is contained within this component, + but the consumer is responsible for showing the current value in the button. + + Required Props: + UDim2 Size = The size of the button that opens the dropdown. + UDim2 Position = The position of the button that opens the dropdown. + int DisplayTextSize = The size of the text in the dropdown and button. + int DescriptionTextSize = The size of the subtext in the dropdown + int ItemHeight = The height of each entry in the dropdown, in pixels. + string ButtonText = Text to display currently selected option in menu + array Items = An ordered array of each item that should appear in the dropdown. + The array is formatted like this: + { + {Key = "Item1", Display = "SomeLocalizedTextForItem1", Description = "SomeLocalizedDescriptionForItem1"}, + {Key = "Item2", Display = "SomeLocalizedTextForItem2", Description = "SomeLocalizedDescriptionForItem2"}, + {Key = "Item3", Display = "SomeLocalizedTextForItem3", Description = "SomeLocalizedDescriptionForItem3"}, + } + Key is how the item will be referenced in code. Text is what will appear to the user. + function OnItemClicked(item) = A callback when the user selects an item in the dropdown. + Returns the item as it was defined in the Items array. + bool Enabled = Enables component if true and accepts input + + Optional Props: + int MaxItems = The maximum number of entries that can display at a time. + If this is less than the number of entries in the dropdown, a scrollbar will appear. + bool ShowRibbon = Whether to show a colored ribbon next to the currently + hovered dropdown entry. Usually should be enabled for Light theme only. + int TextPadding = The amount of padding, in pixels, around the text elements. + int IconSize = The size of the arrow icon in the button. + int IconPadding = The distance from the right side of the arrow icon to the button edge. + int ScrollBarPadding = The padding which appears on either side of the scrollbar. + int ScrollBarThickness = The horizontal width of the scrollbar. +]] +local FFlagStudioFixUILibDropdownStyle = game:GetFastFlag("StudioFixUILibDropdownStyle") +local FFlagStudioFixUILibDropdownText = game:GetFastFlag("StudioFixUILibDropdownText") + +-- Defaults +local TEXT_PADDING = 10 +local ICON_SIZE = 12 +local ICON_PADDING = 4 + +local RIBBON_WIDTH = 5 +local VERTICAL_OFFSET = 2 + +local MAX_WIDTH = 300 + +local TextService = game:GetService("TextService") + +local Library = script.Parent.Parent +local Roact = require(Library.Parent.Parent.Roact) + +local Theming = require(Library.Theming) +local withTheme = Theming.withTheme + +local DropdownMenu = require(Library.Components.DropdownMenu) +local RoundFrame = require(Library.Components.RoundFrame) +local createFitToContent = require(Library.Components.createFitToContent) + +local DetailedDropdown = Roact.PureComponent:extend("DetailedDropdown") + +local FitToContent = createFitToContent("Frame", "UIListLayout", { + SortOrder = Enum.SortOrder.LayoutOrder, + Padding = UDim.new(0, TEXT_PADDING), +}) + +function DetailedDropdown:init() + self.state = { + showDropdown = false, + isButtonHovered = false, + dropdownItem = nil, + } + self.buttonRef = Roact.createRef() + + self.onItemClicked = function(item) + if self.props.Enabled then + self.props.OnItemClicked(item.Key) + self.hideDropdown() + end + end + + self.showDropdown = function() + if self.props.Enabled then + self:setState({ + showDropdown = true, + }) + end + end + + self.hideDropdown = function() + if self.props.Enabled then + self:setState({ + showDropdown = false, + }) + end + end + + self.onKeyMouseEnter = function(item) + if self.props.Enabled then + self:setState({ + dropdownItem = item, + }) + end + end + + self.onKeyMouseLeave = function(item) + if self.props.Enabled then + if self.state.dropdownItem == item then + self:setState({ + dropdownItem = Roact.None, + }) + end + end + end + + self.onMouseEnter = function() + if self.props.Enabled then + self:setState({ + isButtonHovered = true, + }) + end + end + + self.onMouseLeave = function() + if self.props.Enabled then + self:setState({ + isButtonHovered = false, + }) + end + end +end + +function DetailedDropdown:createMainTextLabel(key, displayText, displayTextSize, displayTextColor, textPadding, font, height) + return Roact.createElement("TextLabel", { + Size = UDim2.new(1, 0, 0, height), + Font = font, + TextSize = displayTextSize, + Text = displayText, + TextXAlignment = Enum.TextXAlignment.Left, + TextColor3 = displayTextColor, + BackgroundTransparency = 1, + TextWrapped = true, + LayoutOrder = 0, + }, { + Padding = Roact.createElement("UIPadding", { + PaddingTop = UDim.new(0, textPadding), + PaddingLeft = UDim.new(0, textPadding), + }), + }) +end + +function DetailedDropdown:createDescriptionTextLabel(key, descriptionText, descriptionTextSize, descriptionTextColor, textPadding, font, height) + return Roact.createElement("TextLabel", { + Size = UDim2.new(1, 0, 0, height), + Font = font, + TextSize = descriptionTextSize, + Text = descriptionText, + TextXAlignment = Enum.TextXAlignment.Left, + TextColor3 = descriptionTextColor, + BackgroundTransparency = 1, + TextWrapped = true, + LayoutOrder = 1, + }, { + Padding = Roact.createElement("UIPadding", { + PaddingLeft = UDim.new(0, textPadding), + PaddingRight = UDim.new(0, textPadding), + }), + }) +end + +function DetailedDropdown:render() + return withTheme(function(theme) + local props = self.props + local state = self.state + local dropdownTheme = theme.detailedDropdown + + local showDropdown = state.showDropdown + local buttonRef = self.buttonRef and self.buttonRef.current + local buttonExtents + if buttonRef then + local buttonMin = buttonRef.AbsolutePosition + local buttonSize = buttonRef.AbsoluteSize + local buttonMax = buttonMin + buttonSize + buttonExtents = Rect.new(buttonMin.X, buttonMin.Y, buttonMax.X, buttonMax.Y) + end + + local items = props.Items or {} + local selectedItem = props.SelectedItem + local size = props.Size + local position = props.Position + local displayTextSize = props.DisplayTextSize + local descriptionTextSize = props.DescriptionTextSize + local itemHeight = props.ItemHeight + local maxItems = props.MaxItems + local showRibbon = props.ShowRibbon + local enabled = props.Enabled + + local textPadding = props.TextPadding or TEXT_PADDING + local iconSize = props.IconSize or ICON_SIZE + local iconPadding = props.IconPadding or ICON_PADDING + local scrollBarPadding = props.ScrollBarPadding + local scrollBarThickness = props.ScrollBarThickness + + local dropdownItem = state.dropdownItem + local isButtonHovered = state.isButtonHovered + local buttonText = props.ButtonText + + local maxItemWidth = 0 + local maxWidth = props.MaxWidth or MAX_WIDTH + local maxHeight = maxItems and (maxItems * itemHeight) or nil + + for _, data in ipairs(items) do + local displayTextBound = TextService:GetTextSize(data.Display, + displayTextSize, dropdownTheme.font, Vector2.new(math.huge, math.huge)) + + local displayItemWidth = displayTextBound.X + textPadding * 2 + + local descriptionTextBound = TextService:GetTextSize(data.Description, + descriptionTextSize, dropdownTheme.font, Vector2.new(math.huge, math.huge)) + + local descriptionItemWidth = descriptionTextBound.X + textPadding * 2 + + maxItemWidth = math.max(maxItemWidth, displayItemWidth, descriptionItemWidth) + end + + maxWidth = math.min(maxItemWidth, maxWidth) + + local hoverTheme = dropdownTheme.selected + if FFlagStudioFixUILibDropdownStyle then + hoverTheme = dropdownTheme.hovered + end + + local buttonTheme = (showDropdown or isButtonHovered) and hoverTheme + or dropdownTheme + + return Roact.createElement("ImageButton", { + LayoutOrder = props.LayoutOrder or 0, + AnchorPoint = props.AnchorPoint or Vector2.new(0,0), + Size = size, + Position = position, + BackgroundTransparency = 1, + Image = "", + + [Roact.Ref] = self.buttonRef, + + [Roact.Event.Activated] = self.showDropdown, + [Roact.Event.MouseEnter] = self.onMouseEnter, + [Roact.Event.MouseLeave] = self.onMouseLeave, + }, { + RoundFrame = Roact.createElement(RoundFrame, { + Size = UDim2.new(1, 0, 1, 0), + BackgroundColor3 = enabled and buttonTheme.backgroundColor or buttonTheme.disabled, + BorderColor3 = buttonTheme.borderColor, + }), + + ArrowIcon = Roact.createElement("ImageLabel", { + Size = UDim2.new(0, iconSize, 0, iconSize), + Position = UDim2.new(1, -iconPadding, 0.5, 0), + AnchorPoint = Vector2.new(1, 0.5), + BackgroundTransparency = 1, + ImageColor3 = enabled and buttonTheme.displayText or buttonTheme.disabledText, + Image = dropdownTheme.arrowImage, + }), + + TextLabel = Roact.createElement("TextLabel", { + Size = UDim2.new(1, FFlagStudioFixUILibDropdownText and -iconSize or 0, 1, 0), + BackgroundTransparency = 1, + Font = dropdownTheme.font, + TextColor3 = enabled and buttonTheme.displayText or buttonTheme.disabledText, + TextSize = displayTextSize, + Text = buttonText, + TextXAlignment = Enum.TextXAlignment.Left, + TextTruncate = FFlagStudioFixUILibDropdownText and Enum.TextTruncate.AtEnd or nil, + }, { + Padding = Roact.createElement("UIPadding", { + PaddingLeft = UDim.new(0, textPadding), + }), + }), + + Dropdown = showDropdown and buttonRef and Roact.createElement(DropdownMenu, { + OnItemClicked = self.onItemClicked, + OnFocusLost = self.hideDropdown, + SourceExtents = buttonExtents, + Offset = Vector2.new(0, VERTICAL_OFFSET), + MaxHeight = maxHeight, + ShowBorder = false, + ScrollBarPadding = scrollBarPadding, + ScrollBarThickness = scrollBarThickness, + + Items = items, + RenderItem = function(item, index, activated) + local key = item.Key + local selected = key == selectedItem + local displayText = item.Display + local descriptionText = item.Description + local isHovered = dropdownItem == key + local displayTextColor = isHovered and dropdownTheme.hovered.displayText + or dropdownTheme.displayText + local descriptionTextColor = dropdownTheme.descriptionText + + local displayTextBound = TextService:GetTextSize(displayText, + displayTextSize, dropdownTheme.font, Vector2.new(maxWidth, math.huge)) + + local descriptionTextBound = TextService:GetTextSize(descriptionText, + descriptionTextSize, dropdownTheme.font, Vector2.new(maxWidth, math.huge)) + + local itemColor = dropdownTheme.backgroundColor + if FFlagStudioFixUILibDropdownStyle and selected then + itemColor = dropdownTheme.selected.backgroundColor + elseif isHovered then + itemColor = dropdownTheme.hovered.backgroundColor + end + + return Roact.createElement("ImageButton", { + Size = UDim2.new(0, maxWidth, 0, displayTextBound.Y + descriptionTextBound.Y + textPadding * 2), + BackgroundColor3 = itemColor, + BorderSizePixel = 0, + LayoutOrder = index, + AutoButtonColor = false, + [Roact.Event.Activated] = activated, + [Roact.Event.MouseEnter] = function() + self.onKeyMouseEnter(key) + end, + [Roact.Event.MouseLeave] = function() + self.onKeyMouseLeave(key) + end, + }, { + Roact.createElement(FitToContent, { + LayoutOrder = index, + BackgroundTransparency = 1, + } , { + Ribbon = isHovered and showRibbon and Roact.createElement("Frame", { + Size = UDim2.new(0, RIBBON_WIDTH, 1, 0), + BackgroundColor3 = dropdownTheme.selected.backgroundColor, + BorderSizePixel = 0, + }), + + MainTextLabel = self:createMainTextLabel(key, displayText, displayTextSize, displayTextColor, + textPadding, dropdownTheme.font, displayTextBound.Y), + + DescriptionTextLabel = self:createDescriptionTextLabel(key, descriptionText, descriptionTextSize, descriptionTextColor, + textPadding, dropdownTheme.font, descriptionTextBound.Y), + }) + }) + end, + }) + }) + end) +end + +return DetailedDropdown diff --git a/BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Components/DetailedDropdown.spec.lua b/BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Components/DetailedDropdown.spec.lua new file mode 100644 index 0000000000..80388c2ca8 --- /dev/null +++ b/BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Components/DetailedDropdown.spec.lua @@ -0,0 +1,34 @@ +return function() + local Library = script.Parent.Parent + local Roact = require(Library.Parent.Parent.Roact) + + local MockWrapper = require(Library.MockWrapper) + + local DetailedDropdown = require(script.Parent.DetailedDropdown) + + local function createTestDetailedDropdown(props, children) + return Roact.createElement(MockWrapper, {}, { + DetailedDropdown = Roact.createElement(DetailedDropdown, props, children) + }) + end + + it("should create and destroy without errors", function() + local element = createTestDetailedDropdown() + local instance = Roact.mount(element) + Roact.unmount(instance) + end) + + it("should render correctly", function() + local container = Instance.new("Folder") + local instance = Roact.mount(createTestDetailedDropdown(), container) + local button = container:FindFirstChildOfClass("ImageButton") + + expect(button).to.be.ok() + expect(button.RoundFrame).to.be.ok() + expect(button.ArrowIcon).to.be.ok() + expect(button.TextLabel).to.be.ok() + expect(button.TextLabel.Padding).to.be.ok() + + Roact.unmount(instance) + end) +end \ No newline at end of file diff --git a/BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Components/DragTarget.lua b/BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Components/DragTarget.lua new file mode 100644 index 0000000000..a4666cb555 --- /dev/null +++ b/BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Components/DragTarget.lua @@ -0,0 +1,58 @@ +--[[ + A component that can listen to change in mouse position while active, + and then has a callback for removal once the user is done dragging. + + Props: + function OnDragMoved(input) = A callback for when the user drags + the mouse. The input param is the InputObject from the InputChanged event. + + function OnDragEnded() = A callback for when the user has stopped dragging. + + Usage: + From a stateful component, hold on to a dragging state. When the user + presses the mouse on a draggable element, set the dragging state to + true. When dragging is true, render this element. Hook up this element's + OnDragEnded function to setting the dragging state to false. +]] + +local Library = script.Parent.Parent +local Roact = require(Library.Parent.Parent.Roact) + +local Focus = require(Library.Focus) +local CaptureFocus = Focus.CaptureFocus + +local DragTarget = Roact.PureComponent:extend("DragTarget") + +function DragTarget:init() + self.inputChanged = function(rbx, input) + if input.UserInputType == Enum.UserInputType.MouseMovement then + if self.props.OnDragMoved then + self.props.OnDragMoved(input) + end + end + end + + self.inputEnded = function() + if self.props.OnDragEnded then + self.props.OnDragEnded() + end + end +end + +function DragTarget:render() + return Roact.createElement(CaptureFocus, { + OnFocusLost = self.inputEnded, + }, { + DragListener = Roact.createElement("ImageButton", { + BackgroundTransparency = 1, + Size = UDim2.new(1, 0, 1, 0), + + [Roact.Event.InputChanged] = self.inputChanged, + [Roact.Event.InputEnded] = self.inputEnded, + [Roact.Event.MouseButton1Up] = self.inputEnded, + [Roact.Event.MouseButton2Up] = self.inputEnded, + }, self.props[Roact.Children]) + }) +end + +return DragTarget diff --git a/BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Components/DragTarget.spec.lua b/BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Components/DragTarget.spec.lua new file mode 100644 index 0000000000..e94a8dee44 --- /dev/null +++ b/BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Components/DragTarget.spec.lua @@ -0,0 +1,38 @@ +return function() + local Library = script.Parent.Parent + local Roact = require(Library.Parent.Parent.Roact) + + local MockWrapper = require(Library.MockWrapper) + + local DragTarget = require(script.Parent.DragTarget) + + local function createTestDragTarget(container) + return Roact.createElement(MockWrapper, { + Container = container, + }, { + DragTarget = Roact.createElement(DragTarget) + }) + end + + it("should create and destroy without errors", function() + local element = createTestDragTarget() + local instance = Roact.mount(element) + Roact.unmount(instance) + end) + + it("should render correctly", function() + local container = Instance.new("Folder") + + local element = createTestDragTarget(container) + + local instance = Roact.mount(element, container) + + local gui = container:FindFirstChild("MockGui") + expect(gui).to.be.ok() + expect(gui.TopLevelDetector).to.be.ok() + expect(gui.TopLevelDetector.ScrollBlocker).to.be.ok() + expect(gui.TopLevelDetector.ScrollBlocker.DragListener).to.be.ok() + + Roact.unmount(instance) + end) +end \ No newline at end of file diff --git a/BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Components/DropShadow.lua b/BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Components/DropShadow.lua new file mode 100644 index 0000000000..ace6eaede4 --- /dev/null +++ b/BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Components/DropShadow.lua @@ -0,0 +1,58 @@ +--[[ + A rectangular drop shadow that appears behind an element. + + Props: + Vector2 Offset = The offset of this drop shadow from the element it appears beneath. + float Transparency = The transparency of the drop shadow, from 0 to 1. + Color3 Color = The color of the drop shadow. + SizePixel = The size of the drop shadow, in pixels. + ZIndex = The render order of the drop shadow. Make sure it is behind your element. +]] + +local Library = script.Parent.Parent + +local Roact = require(Library.Parent.Parent.Roact) + +local Theming = require(Library.Theming) +local withTheme = Theming.withTheme + +local SLICE_SIZE = 8 +local DROP_SHADOW_SLICE = Rect.new(SLICE_SIZE, SLICE_SIZE, SLICE_SIZE, SLICE_SIZE) + +local DropShadow = Roact.PureComponent:extend("DropShadow") + +function DropShadow:render() + return withTheme(function(theme) + local props = self.props + local shadowTheme = theme.dropShadow + + local shadowColor = props.Color + local shadowTransparency = props.Transparency + local offset = props.Offset or Vector2.new() + local shadowSize = props.SizePixel or SLICE_SIZE + local zindex = props.ZIndex or 0 + + -- SliceScale is multiplicative, so we need to normalize to the slice size + local sliceScale = shadowSize / SLICE_SIZE + + return Roact.createElement("ImageLabel", { + Size = UDim2.new(1, shadowSize, 1, shadowSize), + AnchorPoint = Vector2.new(0.5, 0.5), + Position = UDim2.new(0.5, offset.X, 0.5, offset.Y), + ZIndex = zindex, + + BackgroundTransparency = 1, + BorderSizePixel = 0, + + Image = shadowTheme.image, + ImageColor3 = shadowColor, + ImageTransparency = shadowTransparency, + + ScaleType = Enum.ScaleType.Slice, + SliceCenter = DROP_SHADOW_SLICE, + SliceScale = sliceScale, + }) + end) +end + +return DropShadow diff --git a/BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Components/DropShadow.spec.lua b/BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Components/DropShadow.spec.lua new file mode 100644 index 0000000000..e3c71b1050 --- /dev/null +++ b/BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Components/DropShadow.spec.lua @@ -0,0 +1,30 @@ +return function() + local Library = script.Parent.Parent + local Roact = require(Library.Parent.Parent.Roact) + + local MockWrapper = require(Library.MockWrapper) + + local DropShadow = require(script.Parent.DropShadow) + + local function createTestDropShadow(props) + return Roact.createElement(MockWrapper, {}, { + DropShadow = Roact.createElement(DropShadow, props) + }) + end + + it("should create and destroy without errors", function() + local element = createTestDropShadow() + local instance = Roact.mount(element) + Roact.unmount(instance) + end) + + it("should render correctly", function () + local container = Instance.new("Folder") + local instance = Roact.mount(createTestDropShadow(), container) + local shadow = container:FindFirstChildOfClass("ImageLabel") + + expect(shadow).to.be.ok() + + Roact.unmount(instance) + end) +end \ No newline at end of file diff --git a/BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Components/DropdownMenu.lua b/BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Components/DropdownMenu.lua new file mode 100644 index 0000000000..506de613f3 --- /dev/null +++ b/BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Components/DropdownMenu.lua @@ -0,0 +1,241 @@ +--[[ + A generic dropdown menu interface which can accept any kind of components. + The consuming component is in charge of implementing the logic that dictates + when this dropdown menu should show and hide. + + This dropdown detects if it is too close to the corners of the gui and realigns if needed. + For example, if it is too close to the bottom of the gui to render all elements, it + renders its elements above the hosting button instead of below. + + For an example of how this component can be used, see StyledDropdown. + + Required Props: + Rect SourceExtents = A Rect representing the absolute position and size of + the button which is hosting this dropdown. + + table Items = An ordered array of each item that should appear in the dropdown. + Each item in the array can be of any format, and will be passed to the RenderItem function. + function RenderItem(item, index, activated) = A function used to render a dropdown item. + Item is an entry from the Items array that was passed into this component's props. + Index is the index of the current item in the Items array. + Activated is a callback that the item should connect if it is clickable. + + function OnItemClicked(item) = A callback for when the user selects a dropdown entry. + Returns the item as it was defined in the Items array. + function OnFocusLost = A callback for when the user clicks away from the dropdown + without selecting an item. + + Optional Props: + int MaxHeight = An optional maximum height for this dropdown. If the items surpass + the max height, a scrollbar will be added to the dropdown so all items are visible. + int ScrollBarPadding = The padding which appears on either side of the scrollbar. + int ScrollBarThickness = The horizontal width of the scrollbar. + bool ShowBorder = Whether to show a border around the elements in the dropdown. + Vector2 Offset = An offset from the button which is hosting this dropdown. + Note that the dropdown already takes into account the size of the hosting button, + and will already automatically place itself below the button. This offset is optional + and can be used to add some extra padding. + Enum.VerticalAlignment StartDirection=Bottom The direction the DropdownMenu will appear + from SourceExtents by default. This can only be Top/Bottom. This will not lock the + direction of the DropdownMenu. If there is not enough room in the default direction, + it will flip to the other direction +]] + +local ROUNDED_FRAME_SLICE = Rect.new(3, 3, 13, 13) +local SCROLLBAR_THICKNESS = 8 +local SCROLLBAR_PADDING = 2 + +local RunService = game:GetService("RunService") + +local Library = script.Parent.Parent +local Roact = require(Library.Parent.Parent.Roact) + +local Theming = require(Library.Theming) +local withTheme = Theming.withTheme + +local Focus = require(Library.Focus) +local CaptureFocus = Focus.CaptureFocus +local withFocus = Focus.withFocus + +local StyledScrollingFrame = require(Library.Components.StyledScrollingFrame) + +local DropdownMenu = Roact.PureComponent:extend("DropdownMenu") + +function DropdownMenu:init(props) + assert( props.StartDirection == Enum.VerticalAlignment.Top or + props.StartDirection == Enum.VerticalAlignment.Bottom or + props.StartDirection == nil, + + "StartDirection must be Enum.VerticalAlignment.Bottom, Enum.VerticalAlignment.Top, or nil. " + .."Got '"..tostring(props.StartDirection).."'" + ) + + self.direction = props.StartDirection or Enum.VerticalAlignment.Bottom + self.layout = 0 + + self.recalculateSize = function(rbx) + -- We have to wait one step to change the state here, or + -- we will change the state while the component is rendering + -- and the component won't move to the right location. + local nextStep + nextStep = RunService.Heartbeat:Connect(function() + nextStep:Disconnect() + + -- The component may have since been unmounted, in which case + -- we shouldn't update state or it will fail with an error + if not self.mounted then return end + + self:setState({ + menuSize = rbx.AbsoluteContentSize + }) + end) + end + + self.resetLayout = function() + self.layout = 0 + end + + self.nextLayout = function() + self.layout = self.layout + 1 + return self.layout + end +end + +function DropdownMenu:didMount() + self.mounted = true +end + +function DropdownMenu:willUnmount() + self.mounted = false +end + +function DropdownMenu:render() + return withTheme(function(theme) + return withFocus(function(pluginGui) + local props = self.props + local state = self.state + + local items = props.Items + local renderItem = self.props.RenderItem + local sourceExtents = props.SourceExtents + + assert(items ~= nil and type(items) == "table", + "DropdownMenu requires an Items table.") + assert(renderItem ~= nil and type(renderItem) == "function", + "DropdownMenu requires a RenderItem function.") + assert(sourceExtents ~= nil, + "DropdownMenu requires a SourceExtents prop.") + + local components = {} + + local dropdownTheme = theme.dropdownMenu + + local canRender = state.menuSize ~= nil + local menuSize = state.menuSize or Vector2.new() + local width = props.ListWidth or menuSize.X + local height = menuSize.Y + + local offset = props.Offset or Vector2.new() + local showBorder = props.ShowBorder + local scrollBarThickness = props.ScrollBarThickness or SCROLLBAR_THICKNESS + local scrollBarPadding = props.ScrollBarPadding or SCROLLBAR_PADDING + + local maxHeight = props.MaxHeight + if maxHeight == nil or maxHeight > height then + maxHeight = height + elseif maxHeight < height then + -- Add scrollbar gutter + width = width + scrollBarThickness + (scrollBarPadding * 2) + end + + local sourcePosition = sourceExtents.Min + local sourceSize = Vector2.new(sourceExtents.Width, sourceExtents.Height) + local guiSize = pluginGui.AbsoluteSize + + local xPos, yPos + if sourcePosition.X + offset.X + width <= guiSize.X then + xPos = sourcePosition.X + offset.X + else + xPos = sourcePosition.X + sourceSize.X + offset.X - width + end + + local enoughRoomOnBottom = sourcePosition.Y + sourceSize.Y + offset.Y + maxHeight < guiSize.Y + local enoughRoomOnTop = sourcePosition.Y - offset.Y - maxHeight > 0 + + -- Don't flip if there is not enough room on either side. This will just cause a spasm of + -- flip-flopping every render + if enoughRoomOnBottom or enoughRoomOnTop then + if self.direction == Enum.VerticalAlignment.Bottom and not enoughRoomOnBottom then + self.direction = Enum.VerticalAlignment.Top + elseif self.direction == Enum.VerticalAlignment.Top and not enoughRoomOnTop then + self.direction = Enum.VerticalAlignment.Bottom + end + end + + local verticalAlignment + if self.direction == Enum.VerticalAlignment.Bottom then + yPos = sourcePosition.Y + sourceSize.Y + offset.Y + verticalAlignment = Enum.VerticalAlignment.Top + else + yPos = sourcePosition.Y - offset.Y - maxHeight + verticalAlignment = Enum.VerticalAlignment.Bottom + end + + local position = UDim2.new(0, xPos, 0, yPos) + + components.Layout = Roact.createElement("UIListLayout", { + SortOrder = Enum.SortOrder.LayoutOrder, + FillDirection = Enum.FillDirection.Vertical, + VerticalAlignment = verticalAlignment, + [Roact.Change.AbsoluteContentSize] = self.recalculateSize, + }) + + for index, item in ipairs(items) do + table.insert(components, renderItem(item, index, function() + self.props.OnItemClicked(item) + end)) + end + + local contents = { + Border = showBorder and Roact.createElement("ImageLabel", { + Visible = canRender, + Position = position, + Size = UDim2.new(0, width, 0, maxHeight), + ZIndex = 3, + + BackgroundTransparency = 1, + ImageColor3 = dropdownTheme.borderColor, + + Image = dropdownTheme.borderImage, + ScaleType = Enum.ScaleType.Slice, + SliceCenter = ROUNDED_FRAME_SLICE, + }), + } + + if maxHeight and maxHeight < height then + contents.ScrollingContainer = Roact.createElement(StyledScrollingFrame, { + Visible = canRender, + Position = position, + Size = UDim2.new(0, width, 0, maxHeight), + BackgroundTransparency = 1, + CanvasSize = UDim2.new(0, 0, 0, height), + ScrollBarPadding = scrollBarPadding, + ScrollBarThickness = scrollBarThickness, + }, components) + else + contents.Container = Roact.createElement("Frame", { + Visible = canRender, + Position = position, + Size = UDim2.new(0, width, 0, height), + BackgroundTransparency = 1, + }, components) + end + + return Roact.createElement(CaptureFocus, { + OnFocusLost = props.OnFocusLost, + }, contents) + end) + end) +end + +return DropdownMenu diff --git a/BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Components/DropdownMenu.spec.lua b/BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Components/DropdownMenu.spec.lua new file mode 100644 index 0000000000..36fa138ed8 --- /dev/null +++ b/BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Components/DropdownMenu.spec.lua @@ -0,0 +1,228 @@ +return function() + local Library = script.Parent.Parent + local Roact = require(Library.Parent.Parent.Roact) + + local MockWrapper = require(Library.MockWrapper) + + local DropdownMenu = require(script.Parent.DropdownMenu) + + local sourceExtents = Rect.new(0, 0, 150, 150) + + local function createTestDropdownMenu(props, children, container) + return Roact.createElement(MockWrapper, { + Container = container, + }, { + DropdownMenu = Roact.createElement(DropdownMenu, props, children) + }) + end + + it("should create and destroy without errors", function() + local element = createTestDropdownMenu({ + SourceExtents = sourceExtents, + Items = {}, + RenderItem = function(item) + end, + }) + local instance = Roact.mount(element) + Roact.unmount(instance) + end) + + it("should render correctly", function() + local container = Instance.new("Folder") + + local element = createTestDropdownMenu({ + SourceExtents = sourceExtents, + Items = {}, + RenderItem = function(item) + end, + }, {}, container) + + local instance = Roact.mount(element, container) + + local gui = container:FindFirstChild("MockGui") + expect(gui).to.be.ok() + expect(gui.TopLevelDetector).to.be.ok() + expect(gui.TopLevelDetector.ScrollBlocker).to.be.ok() + expect(gui.TopLevelDetector.ScrollBlocker.Container).to.be.ok() + + local dropdownContainer = gui.TopLevelDetector.ScrollBlocker.Container + expect(dropdownContainer.Layout).to.be.ok() + + Roact.unmount(instance) + end) + + it("should require an Items table", function() + local element = createTestDropdownMenu({ + RenderItem = function() + end, + SourceExtents = sourceExtents, + }) + expect(function() + Roact.mount(element) + end).to.throw() + + element = createTestDropdownMenu({ + Items = true, + RenderItem = function() + end, + SourceExtents = sourceExtents, + }) + expect(function() + Roact.mount(element) + end).to.throw() + end) + + it("should require a RenderItem function", function() + local element = createTestDropdownMenu({ + Items = {}, + SourceExtents = sourceExtents, + }) + expect(function() + Roact.mount(element) + end).to.throw() + + element = createTestDropdownMenu({ + Items = {}, + RenderItem = true, + SourceExtents = sourceExtents, + }) + expect(function() + Roact.mount(element) + end).to.throw() + end) + + it("should require a SourceExtents prop", function() + local element = createTestDropdownMenu({ + Items = {}, + RenderItem = function() + end, + }) + expect(function() + Roact.mount(element) + end).to.throw() + end) + + it("should error if given invalid StartDirection", function() + local element = createTestDropdownMenu({ + Items = {}, + SourceExtents = sourceExtents, + RenderItem = function() + end, + StartDirection = 0, + }) + expect(function() + Roact.mount(element) + end).to.throw() + end) + + it("should render items", function() + local container = Instance.new("Folder") + + local element = createTestDropdownMenu({ + SourceExtents = sourceExtents, + Items = {"Frame"}, + RenderItem = function() + return Roact.createElement("Frame") + end, + }, {}, container) + + local instance = Roact.mount(element, container) + + local gui = container:FindFirstChild("MockGui") + local dropdownContainer = gui.TopLevelDetector.ScrollBlocker.Container + expect(dropdownContainer["1"]).to.be.ok() + + Roact.unmount(instance) + end) + + it("should respect the order of items", function() + local container = Instance.new("Folder") + + local element = createTestDropdownMenu({ + SourceExtents = sourceExtents, + Items = {"FirstFrame", "SecondFrame", "ThirdFrame"}, + RenderItem = function(item, index) + return Roact.createElement("TextLabel", { + LayoutOrder = index, + Text = item, + }) + end, + }, {}, container) + + local instance = Roact.mount(element, container) + + local gui = container:FindFirstChild("MockGui") + local dropdownContainer = gui.TopLevelDetector.ScrollBlocker.Container + expect(dropdownContainer["1"].LayoutOrder).to.equal(1) + expect(dropdownContainer["2"].LayoutOrder).to.equal(2) + expect(dropdownContainer["3"].LayoutOrder).to.equal(3) + expect(dropdownContainer["1"].Text).to.equal("FirstFrame") + expect(dropdownContainer["2"].Text).to.equal("SecondFrame") + expect(dropdownContainer["3"].Text).to.equal("ThirdFrame") + + Roact.unmount(instance) + end) + + it("should preserve menu direction when there is enough room", function() + local function getMenuDirection(listLayout) + return listLayout.VerticalAlignment == Enum.VerticalAlignment.Top and -1 or 1 + end + + local container = Instance.new("Folder") + + local elementAtTop = createTestDropdownMenu({ + SourceExtents = Rect.new(0, 0, 150, 150), + Items = {"FirstFrame"}, + RenderItem = function(item, index) + return Roact.createElement("TextLabel", { + LayoutOrder = index, + Text = item, + Size = UDim2.new(1, 0, 0, 20), + }) + end, + }, {}, container) + + local instance = Roact.mount(elementAtTop, container) + + local gui = container:FindFirstChild("MockGui") + local listLayout = gui.TopLevelDetector.ScrollBlocker.Container.Layout + + -- No way to get MockGui canvas size to dock SourceExtents at the bottom/middle unless + -- we mount it and then check the instance's size + local elementAtBottom = createTestDropdownMenu({ + SourceExtents = Rect.new(0, gui.AbsoluteSize.Y + 150, 150, 150), + Items = {"FirstFrame"}, + RenderItem = function(item, index) + return Roact.createElement("TextLabel", { + LayoutOrder = index, + Text = item, + Size = UDim2.new(1, 0, 0, 20), + }) + end, + }, {}, container) + local elementInMiddle = createTestDropdownMenu({ + SourceExtents = Rect.new(0, math.floor(gui.AbsoluteSize.Y/2), 150, 150), + Items = {"FirstFrame"}, + RenderItem = function(item, index) + return Roact.createElement("TextLabel", { + LayoutOrder = index, + Text = item, + Size = UDim2.new(1, 0, 0, 20), + }) + end, + }, {}, container) + + -- The default direction is down, so we should be displaying beneath SourceExtents + expect(getMenuDirection(listLayout)).to.equal(-1) + + -- There is not enough room below, so we flip to top + Roact.update(instance, elementAtBottom) + expect(getMenuDirection(listLayout)).to.equal(1) + + -- There is now enough room below, but we preserve direction, so we are still above SourceExtents + Roact.update(instance, elementInMiddle) + expect(getMenuDirection(listLayout)).to.equal(1) + + Roact.unmount(instance) + end) +end \ No newline at end of file diff --git a/BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Components/ExpandableList.lua b/BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Components/ExpandableList.lua new file mode 100644 index 0000000000..a29b4fdb27 --- /dev/null +++ b/BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Components/ExpandableList.lua @@ -0,0 +1,108 @@ +--[[ + A generic expandable list interface which can accept any kind of components. Intended to be a lightweight control + where it will toggle visibility of a frame which contains content that user will pass in + + Required Props: + table TopLevelItem = Property which takes in table of Roact elements to display top level button. + Will always be displayed and entire element(s) will be clickable to toggle dropdown visibility + table Content = Property which takes in table of Roact elements to display in dropdown area. + + LayoutOrder = props.LayoutOrder (Required, passed through) + Position = props.Position (Required, passed through) + AnchorPoint = props.AnchorPoint (Required, passed through) + + function OnExpandedStateChanged() - Invoked whenever the ExpandableList is opened/closed + bool IsExpanded - Whether the ExpandableList is expanded or not +]] + +local Library = script.Parent.Parent +local Roact = require(Library.Parent.Parent.Roact) + +local Theming = require(Library.Theming) +local withTheme = Theming.withTheme + +local join = require(Library.join) + +local createFitToContent = require(Library.Components.createFitToContent) + +local ExpandableList = Roact.PureComponent:extend("ExpandableList") + +local ContentFit = createFitToContent("Frame", "UIListLayout", { + SortOrder = Enum.SortOrder.LayoutOrder, + Padding = UDim.new(0, 0), +}) + +local TopLevelContentFit = createFitToContent("ImageButton", "UIListLayout", { + SortOrder = Enum.SortOrder.LayoutOrder, + Padding = UDim.new(0, 0), + FillDirection = Enum.FillDirection.Horizontal, +}) + +local TopLevelItem + +function ExpandableList:init() + self.state = { + isButtonHovered = false, + } + self.buttonRef = Roact.createRef() + + self.toggleList = function() + self.props.OnExpandedStateChanged() + end + + self.onMouseEnter = function() + self:setState({ + isButtonHovered = true, + }) + end + + self.onMouseLeave = function() + self:setState({ + isButtonHovered = false, + }) + end +end + + +function ExpandableList:render() + return withTheme(function(theme) + local props = self.props + local state = self.state + + local topLevelItem = props.TopLevelItem + local content = props.Content + + assert(topLevelItem ~= nil and type(topLevelItem) == "table", + "ExpandableList requires a TopLevelItem table.") + assert(content ~= nil and type(content) == "table", + "ExpandableList requires Content table.") + + return Roact.createElement(ContentFit, { + BackgroundTransparency = 1, + LayoutOrder = props.LayoutOrder or 0, + AnchorPoint = props.AnchorPoint or Vector2.new(0,0), + BorderSizePixel = 0, + Position = props.Position, + }, { + TopLevelItem = Roact.createElement(TopLevelContentFit, { + LayoutOrder = 0, + BorderSizePixel = 0, + BackgroundTransparency = 1, + Image = "", + + [Roact.Ref] = self.buttonRef, + [Roact.Event.Activated] = self.toggleList, + [Roact.Event.MouseEnter] = self.onMouseEnter, + [Roact.Event.MouseLeave] = self.onMouseLeave, + }, topLevelItem), + + ExpandableFrame = Roact.createElement(ContentFit, { + LayoutOrder = 1, + BackgroundTransparency = 1, + Visible = props.IsExpanded, + }, content), + }) + end) +end + +return ExpandableList diff --git a/BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Components/ExpandableList.spec.lua b/BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Components/ExpandableList.spec.lua new file mode 100644 index 0000000000..31f1645e77 --- /dev/null +++ b/BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Components/ExpandableList.spec.lua @@ -0,0 +1,143 @@ +return function() + local Library = script.Parent.Parent + local Roact = require(Library.Parent.Parent.Roact) + + local MockWrapper = require(Library.MockWrapper) + + local ExpandableList = require(script.Parent.ExpandableList) + + local function createTestExpandableList(props, children, container) + return Roact.createElement(MockWrapper, { + Container = container, + }, { + ExpandableList = Roact.createElement(ExpandableList, props, children) + }) + end + + it("should create and destroy without errors", function() + local element = createTestExpandableList({ + TopLevelItem = {}, + Content = {}, + }) + local instance = Roact.mount(element) + Roact.unmount(instance) + end) + + it("should require a top level item table", function() + local element = createTestExpandableList({ + Content = {}, + }) + expect(function() + Roact.mount(element) + end).to.throw() + local element = createTestExpandableList({ + TopLevelItem = true, + Content = {} + }) + expect(function() + Roact.mount(element) + end).to.throw() + end) + + it("should require a content table", function() + local element = createTestExpandableList({ + TopLevelItem = {}, + }) + expect(function() + Roact.mount(element) + end).to.throw() + local element = createTestExpandableList({ + TopLevelItem = {}, + Content = true + }) + expect(function() + Roact.mount(element) + end).to.throw() + end) + + it("should render correctly", function () + local container = Instance.new("Folder") + local instance = Roact.mount(createTestExpandableList({ + TopLevelItem = {}, + Content = {}, + }, {}, container), container) + local frame = container:FindFirstChildOfClass("Frame") + + expect(frame.TopLevelItem).to.be.ok() + expect(frame.ExpandableFrame).to.be.ok() + + Roact.unmount(instance) + end) + + it("items should be sized to contents", function () + local container = Instance.new("Folder") + local instance = Roact.mount(createTestExpandableList({ + TopLevelItem = { + ArrowIcon = Roact.createElement("TextLabel", { + Size = UDim2.new(0, 100, 0, 100), + }), + }, + Content = { + Label = Roact.createElement("TextLabel", { + Size = UDim2.new(0, 100, 0, 100), + }), + }, + }, {}, container), container) + local frame = container:FindFirstChildOfClass("Frame") + + expect(frame.TopLevelItem.Size.X.Scale).to.equal(1) + expect(frame.TopLevelItem.Size.Y.Offset).to.equal(100) + + expect(frame.ExpandableFrame.Size.X.Scale).to.equal(1) + expect(frame.ExpandableFrame.Size.Y.Offset).to.equal(100) + + + Roact.unmount(instance) + end) + + it("list should only show top item initially", function () + local container = Instance.new("Folder") + local instance = Roact.mount(createTestExpandableList({ + TopLevelItem = { + ArrowIcon = Roact.createElement("TextLabel", { + Size = UDim2.new(0, 100, 0, 100), + }), + }, + Content = { + Label = Roact.createElement("TextLabel", { + Size = UDim2.new(0, 100, 0, 100), + }), + }, + IsExpanded = false, + }, {}, container), container) + local frame = container:FindFirstChildOfClass("Frame") + + expect(frame.Size.X.Scale).to.equal(1) + expect(frame.Size.Y.Offset).to.equal(100) + + Roact.unmount(instance) + end) + + it("list should only show both items when expanded is true", function () + local container = Instance.new("Folder") + local instance = Roact.mount(createTestExpandableList({ + TopLevelItem = { + ArrowIcon = Roact.createElement("TextLabel", { + Size = UDim2.new(0, 100, 0, 100), + }), + }, + Content = { + Label = Roact.createElement("TextLabel", { + Size = UDim2.new(0, 100, 0, 100), + }), + }, + IsExpanded = true, + }, {}, container), container) + local frame = container:FindFirstChildOfClass("Frame") + + expect(frame.Size.X.Scale).to.equal(1) + expect(frame.Size.Y.Offset).to.equal(200) + + Roact.unmount(instance) + end) +end \ No newline at end of file diff --git a/BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Components/InfiniteScrollingFrame.lua b/BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Components/InfiniteScrollingFrame.lua new file mode 100644 index 0000000000..aff10bcdb8 --- /dev/null +++ b/BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Components/InfiniteScrollingFrame.lua @@ -0,0 +1,136 @@ +--[[ + A scroll frame that will check the left space between currently rendered asset + When scrolling down, it will try to re-render. If we found less than defined space or empty space + between the render asset and the canvas, then we will try to call request more function defined in the property + to fetch more assets. The function is responsible for the paging method to fetch more assets. + After the asset is returned, we will re-calculate canvase size. + This component will send out request to try to load more pages on didMount and after didUpdate. + + Required Properties: + function NextPageFunc - called during re-render when there is more empty spaces. This function should includes all the + parameters needed for the request except for the currentPage. Target page will be determined by the infiScroller. + + Optional Properties: + UDim2 Position - The position of the scrolling frame. + UDim2 Size - The size of the scrolling frame. + int LayoutOrder - sets order of element in layout + int NextPageRequestDistance - space left in layout before making request to fetch more elements + int CanvasHeight - used to specify height of canvas. + Roact ref LayoutRef - used to calculate the height of the canvas. +]] + +local Library = script.Parent.Parent +local Roact = require(Library.Parent.Parent.Roact) + +local StyledScrollingFrame = require(Library.Components.StyledScrollingFrame) + +local InfiniteScrollingFrame = Roact.PureComponent:extend("InfiniteScrollingFrame") + +local DEFAULT_CANVAS_HEIGHT = 900 + +local DEFAULT_REQUEST_DISTANCE = 0 + +local FFlagUILibraryInfiniteScrollingFrameRender = game:DefineFastFlag("UILibraryInfiniteScrollingFrameRender", false) + +function InfiniteScrollingFrame:init(props) + self.state = { + isRequestingNextPage = false, + } + + self.isRequestingNextPage = false + + self.scrollingFrameRef = Roact.createRef() + + self.checkCanvasAndRequest = function() + local scrollingFrame = self.scrollingFrameRef.current + if not scrollingFrame then return end + + local canvasY = scrollingFrame.CanvasPosition.Y + local windowHeight = scrollingFrame.AbsoluteWindowSize.Y + local canvasHeight = scrollingFrame.CanvasSize.Y.Offset + + local requestDistance = self.props.NextPageRequestDistance or DEFAULT_REQUEST_DISTANCE + + -- Where the bottom of the scrolling frame is relative to canvas size + local bottom = canvasY + windowHeight + local dist = canvasHeight - bottom + + if dist <= requestDistance and not self.state.isRequestingNextPage then + if FFlagUILibraryInfiniteScrollingFrameRender then + self.isRequestingNextPage = true + else + self:setState({ + isRequestingNextPage = true, + }) + end + self.requestNextPage() + end + end + + self.onScroll = function() + self.checkCanvasAndRequest(self) + end + + self.requestNextPage = function() + self.props.NextPageFunc() + end +end + +function InfiniteScrollingFrame:didMount() + self.checkCanvasAndRequest(self) +end + +function InfiniteScrollingFrame:didUpdate(previousProps, previousState) + -- check if request has fetched more children + if previousState.isRequestingNextPage then + for k,v in pairs(self.props[Roact.Children]) do + if v ~= previousProps[Roact.Children][k] then + if FFlagUILibraryInfiniteScrollingFrameRender then + self.isRequestingNextPage = false + else + self:setState({ + isRequestingNextPage = false, + }) + end + self.checkCanvasAndRequest(self) + end + end + end +end + +function InfiniteScrollingFrame:render() + local props = self.props + + local nextPageFunc = self.props.NextPageFunc + + assert(nextPageFunc ~= nil and type(nextPageFunc) == "function", + "InfiniteScrollingFrame requires a NextPageFunc function.") + + local position = props.Position + local size = props.Size + local layoutOrder = props.LayoutOrder + + local layout= props.LayoutRef and props.LayoutRef.current + local canvasHeight = DEFAULT_CANVAS_HEIGHT + if layout then + canvasHeight = layout.AbsoluteContentSize.Y + elseif props.CanvasHeight then + canvasHeight = props.CanvasHeight + end + + return Roact.createElement(StyledScrollingFrame, { + Position = position, + Size = size, + LayoutOrder = layoutOrder, + CanvasSize = UDim2.new(1, 0, 0, canvasHeight), + ZIndex = 1, + + ScrollingEnabled = true, + + OnScroll = self.onScroll, + + [Roact.Ref] = self.scrollingFrameRef, + }, props[Roact.Children]) +end + +return InfiniteScrollingFrame diff --git a/BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Components/InfiniteScrollingFrame.spec.lua b/BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Components/InfiniteScrollingFrame.spec.lua new file mode 100644 index 0000000000..a48c231372 --- /dev/null +++ b/BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Components/InfiniteScrollingFrame.spec.lua @@ -0,0 +1,69 @@ +return function() + local Library = script.Parent.Parent + local Roact = require(Library.Parent.Parent.Roact) + + local join = require(Library.join) + local MockWrapper = require(Library.MockWrapper) + + local InfiniteScrollingFrame = require(script.Parent.InfiniteScrollingFrame) + + local function createTestScrollingFrame(props, children) + props = join(props or {}, { + NextPageFunc = function() + return "foo" + end + }) + + return Roact.createElement(MockWrapper, {}, { + ScrollingFrame = Roact.createElement(InfiniteScrollingFrame, props, children) + }) + end + + it("should create and destroy without errors", function() + local element = createTestScrollingFrame() + local instance = Roact.mount(element) + Roact.unmount(instance) + end) + + it("should render correctly", function() + local container = Instance.new("Folder") + local instance = Roact.mount(createTestScrollingFrame(), container) + local frame = container:FindFirstChildOfClass("Frame") + + expect(frame).to.be.ok() + expect(frame.ScrollBarBackground).to.be.ok() + expect(frame.ScrollingFrame).to.be.ok() + + Roact.unmount(instance) + end) + + it("should render its children in the ScrollingFrame", function() + local container = Instance.new("Folder") + local instance = Roact.mount(createTestScrollingFrame({}, { + ChildFrame = Roact.createElement("Frame"), + }), container) + local frame = container:FindFirstChildOfClass("Frame") + + expect(frame).to.be.ok() + expect(frame.ScrollBarBackground).to.be.ok() + expect(frame.ScrollingFrame).to.be.ok() + expect(frame.ScrollingFrame.ChildFrame).to.be.ok() + + Roact.unmount(instance) + end) + + it("should add padding to both sides of the ScrollBar", function() + local container = Instance.new("Folder") + local instance = Roact.mount(createTestScrollingFrame({ + ScrollBarPadding = 2, + ScrollBarThickness = 8, + }), container) + local frame = container:FindFirstChildOfClass("Frame") + + expect(frame).to.be.ok() + expect(frame.ScrollBarBackground).to.be.ok() + expect(frame.ScrollBarBackground.Size.X.Offset).to.equal(12) + + Roact.unmount(instance) + end) +end \ No newline at end of file diff --git a/BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Components/LoadingBar.lua b/BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Components/LoadingBar.lua new file mode 100644 index 0000000000..08a000aba3 --- /dev/null +++ b/BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Components/LoadingBar.lua @@ -0,0 +1,115 @@ +--[[ + LoadingBar - creates a loading bar used for validation/publishing + + 1. loads to holdPercent + 2. waits for onFinish to be non-nil + 3. loads to 100% + 4. loads to 150% (so the user can see the finished loading bar for a short delay) + + Necessary Props: + string LoadingText - the loading bar text + number HoldPercent [0, 1] - percentage to wait at + number LoadingTime - total time it takes to load without waiting for onFinish + bool InstallationFinished - indicates whether or not the installation has fininshed. +]] + +local Library = script.Parent.Parent +local Roact = require(Library.Parent.Parent.Roact) + +local RunService = game:GetService("RunService") + +local RoundFrame = require(Library.Components.RoundFrame) + +local LOADING_TITLE_HEIGHT = 20 +local LOADING_TITLE_PADDING = 10 + +local Theming = require(Library.Theming) +local withTheme = Theming.withTheme + +local LoadingBar = Roact.Component:extend("LoadingBar") + +function LoadingBar:init(props) + self:setState({ + progress = 0, + time = 0, + }) +end + +function LoadingBar:loadUntil(percent) + while self.state.progress < percent do + local dt = RunService.RenderStepped:Wait() + if not self.isMounted then + break + end + local newTime = self.state.time + dt + self:setState({ + time = newTime, + progress = newTime/self.props.LoadingTime + }) + end +end + +function LoadingBar:didMount() + self.isMounted = true + spawn(function() + -- go to 92% + self:loadUntil(self.props.HoldPercent) + + -- wait until props.onFinish + while self.isMounted and not self.props.InstallationFinished do + RunService.RenderStepped:Wait() + end + + -- go to 100% + self:loadUntil(1) + + -- wait for a moment to show "full loading screen" + self:loadUntil(1.5) + end) +end + +function LoadingBar:willUnmount() + self.isMounted = false +end + +function LoadingBar:render() + return withTheme(function(theme) + local props = self.props + local state = self.state + + local progress = math.min(math.max(state.progress, 0), 1) + local loadingText = props.LoadingText .. " ( " .. math.floor((progress * 100) + 0.5) .. "% )" + + return Roact.createElement("Frame", { + BackgroundTransparency = 1, + Size = props.Size, + Position = props.Position, + }, { + LoadingTitle = Roact.createElement("TextLabel", { + BackgroundTransparency = 1, + Font = theme.loadingBar.font, + Position = UDim2.new(0, 0, 0, -(LOADING_TITLE_HEIGHT + LOADING_TITLE_PADDING)), + Size = UDim2.new(1, 0, 0, LOADING_TITLE_HEIGHT), + Text = loadingText, + TextColor3 = theme.loadingBar.text, + TextSize = theme.loadingBar.fontSize, + TextXAlignment = Enum.TextXAlignment.Center, + TextYAlignment = Enum.TextYAlignment.Center, + }), + + LoadingBackgroundBar = Roact.createElement(RoundFrame, { + BorderSizePixel = 0, + BackgroundColor3 = theme.loadingBar.bar.backgroundColor, + Size = UDim2.new(1, 0, 1, 0), + }, { + LoadingBar = Roact.createElement(RoundFrame, { + BorderSizePixel = 0, + BackgroundColor3 = theme.loadingBar.bar.foregroundColor, + Size = UDim2.new(progress, 0, 1, 0), + }), + }), + }) + end) +end + +return LoadingBar \ No newline at end of file diff --git a/BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Components/LoadingIndicator.lua b/BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Components/LoadingIndicator.lua new file mode 100644 index 0000000000..e75ebd6901 --- /dev/null +++ b/BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Components/LoadingIndicator.lua @@ -0,0 +1,139 @@ +--[[ + Loading indicator + + Props: + Vector2 AnchorPoint = Vector2.new(0, 0) + UDim2 Position = UDim2.new(0, 0, 0, 0) + UDim2 Size = UDim2.new(0, 92, 0, 24) + number ZIndex = 0 + boolean Visible = true + number Count = 3 : number of blocks in loading animation + number GapRatio = 1.5 : sets gap between blocks + number EndRatio = 0.25 : used for calculating block width +]] + +local Library = script.Parent.Parent +local Roact = require(Library.Parent.Parent.Roact) + +local Theming = require(Library.Theming) +local withTheme = Theming.withTheme + +local RunService = game:GetService("RunService") + +local LoadingIndicator = Roact.PureComponent:extend("LoadingIndicator") + +local ANIMATION_SPEED = 5 +local DEFAULT_BLOCK_COUNT = 3 + +function LoadingIndicator:init() + self.state = { + animationTime = math.pi / 2, + sinTime = 1, + direction = 1, + index = 1, + } + + self.animationConnection = RunService.RenderStepped:connect(function(deltaTime) + self:updateAnimation(deltaTime) + end) +end + +function LoadingIndicator:willUnmount() + if self.animationConnection then + self.animationConnection:Disconnect() + end +end + +function LoadingIndicator:updateAnimation(deltaTime) + self:setState(function(prevState, props) + local newAnimationTime = prevState.animationTime + deltaTime + local newSinTime = math.sin(newAnimationTime * ANIMATION_SPEED) + + local direction = prevState.direction + local newDirection = direction + local newIndex = prevState.index + + -- If sin has changed sign, move to the next block + if (direction > 0 and newSinTime < 0) or (direction < 0 and newSinTime > 0) then + newDirection = -direction + newIndex = newIndex + 1 + + if newIndex > (self.props.count or DEFAULT_BLOCK_COUNT) then + newIndex = 1 + end + end + + return { + animationTime = newAnimationTime, + sinTime = newSinTime, + direction = newDirection, + index = newIndex, + } + end) +end + +function LoadingIndicator:render() + return withTheme(function(theme) + local props = self.props + local state = self.state + + local loadingIndicatorTheme = theme.loadingIndicator + + local baseColor = loadingIndicatorTheme.baseColor + local endColor = loadingIndicatorTheme.endColor + + local anchorPoint = props.AnchorPoint or Vector2.new(0, 0) + local position = props.Position or UDim2.new(0, 0, 0, 0) + local size = props.Size or UDim2.new(0, 92, 0, 24) + local zindex = props.ZIndex or 0 + local visible = (props.Visible ~= nil and props.Visible) or (props.Visible == nil) + + local blockCount = props.Count or DEFAULT_BLOCK_COUNT + + local gapBetweenBlockRatio = props.GapRatio or 1.5 + local endRatio = props.EndRatio or 0.25 + + local blockWidth = 1 / (blockCount + (blockCount * gapBetweenBlockRatio) - gapBetweenBlockRatio + (2 * endRatio)) + local gapWidth = blockWidth * gapBetweenBlockRatio + + local smallHeight = 0.6 + + local children = { + UIListLayout = Roact.createElement("UIListLayout", { + SortOrder = Enum.SortOrder.LayoutOrder, + FillDirection = Enum.FillDirection.Horizontal, + Padding = UDim.new(gapWidth, 0), + HorizontalAlignment = Enum.HorizontalAlignment.Center, + VerticalAlignment = Enum.VerticalAlignment.Center, + }), + } + + local sinTime = math.abs(state.sinTime) + local index = state.index + + for i = 1, blockCount, 1 do + local height = i == index and smallHeight + ((1 - smallHeight) * sinTime) or smallHeight + + local color = i == index and baseColor:lerp(endColor, sinTime) or baseColor + + children["Frame" .. i] = Roact.createElement("Frame", { + Size = UDim2.new(blockWidth, 0, height, 0), + LayoutOrder = i, + BorderSizePixel = 0, + BackgroundColor3 = color, + }) + end + + return Roact.createElement("Frame", { + AnchorPoint = anchorPoint, + Position = position, + Size = size, + ZIndex = zindex, + BorderSizePixel = 0, + Visible = visible, + BackgroundTransparency = 1, + }, children) + end) +end + +return LoadingIndicator diff --git a/BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Components/LoadingIndicator.spec.lua b/BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Components/LoadingIndicator.spec.lua new file mode 100644 index 0000000000..3a99ec2859 --- /dev/null +++ b/BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Components/LoadingIndicator.spec.lua @@ -0,0 +1,16 @@ +return function() + local Library = script.Parent.Parent + local Roact = require(Library.Parent.Parent.Roact) + + local MockWrapper = require(Library.MockWrapper) + + local LoadingIndicator = require(script.Parent.LoadingIndicator) + + it("should create and destroy without errors", function() + local element = Roact.createElement(MockWrapper, {}, { + LoadingIndicator = Roact.createElement(LoadingIndicator), + }) + local instance = Roact.mount(element) + Roact.unmount(instance) + end) +end diff --git a/BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Components/MultilineTextEntry.lua b/BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Components/MultilineTextEntry.lua new file mode 100644 index 0000000000..aa221dcf69 --- /dev/null +++ b/BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Components/MultilineTextEntry.lua @@ -0,0 +1,143 @@ +--[[ + A multiline text entry with a dynmically appearing scrollbar. + Used in a RoundTextBox when Multiline is true. + + Props: + string Text = The text to display + bool Visible = Whether to display this component + int TextSize = The size of text + function SetText(text) = Callback to tell parent that text has changed + function FocusChanged(focus) = Callback to tell parent that this component has focus + function HoverChanged(hovered) = Callback when the mouse enters or leaves this component. +]] + +local TextService = game:GetService("TextService") + +local Library = script.Parent.Parent +local Roact = require(Library.Parent.Parent.Roact) + +local StyledScrollingFrame = require(Library.Components.StyledScrollingFrame) + +local SCROLL_BAR_OUTSET = 9 + +local MultilineTextEntry = Roact.PureComponent:extend("MultilineTextEntry") + +function MultilineTextEntry:init() + self.frameRef = Roact.createRef() + self.textBoxRef = Roact.createRef() + self.textConnections = nil + + -- TODO: Get rid of function and replace with API call CLIPLAYEREX-2806 when it ships + self.getPositionAtIndex = function(index) + local frame = self.frameRef.current + local sizeX = frame.AbsoluteSize.x - SCROLL_BAR_OUTSET + local textSize = TextService:GetTextSize( + string.sub(self.props.Text, 0, index), + self.props.TextSize, + Enum.Font.SourceSans, + Vector2.new(sizeX, math.huge) + ) + return textSize + end + + self.updateCanvas = function() + local frame = self.frameRef.current + local sizeX = frame.AbsoluteSize.x - SCROLL_BAR_OUTSET + local textBox = self.textBoxRef.current + local textSize = TextService:GetTextSize( + self.props.Text, + self.props.TextSize, + Enum.Font.SourceSans, + Vector2.new(sizeX, math.huge) + ) + frame.CanvasSize = UDim2.new(0, 0, 0, textSize.y) + frame.CanvasPosition = Vector2.new(0, self.getPositionAtIndex(textBox.CursorPosition).y - 2 * self.props.TextSize) + end + + self.textChanged = function(rbx) + if rbx.Text ~= self.props.Text then + self.props.SetText(rbx.Text) + end + end + + self.mouseEnter = function() + self.props.HoverChanged(true) + end + self.mouseLeave = function() + self.props.HoverChanged(false) + end +end + +function MultilineTextEntry:didMount() + local textBox = self.textBoxRef.current + local frame = self.frameRef.current + self.textConnections = { + textBox:GetPropertyChangedSignal("Text"):connect(self.updateCanvas), + frame:GetPropertyChangedSignal("AbsoluteSize"):connect(self.updateCanvas), + } + self.updateCanvas() +end + +function MultilineTextEntry:willUnmount() + for _, connection in ipairs(self.textConnections) do + connection:Disconnect() + end + self.textConnections = nil +end + +function MultilineTextEntry:render() + local visible = self.props.Visible + local text = self.props.Text + local textColor = self.props.TextColor3 + local textSize = self.props.TextSize + local font = self.props.Font + + + return Roact.createElement(StyledScrollingFrame, { + Size = UDim2.new(1, SCROLL_BAR_OUTSET, 1, 0), + BackgroundTransparency = 1, + ClipsDescendants = true, + ShowBackground = false, + + [Roact.Ref] = self.frameRef, + }, { + Padding = Roact.createElement("UIPadding", { + PaddingRight = UDim.new(0, SCROLL_BAR_OUTSET), + }), + + Text = Roact.createElement("TextBox", { + Visible = visible, + MultiLine = true, + TextWrapped = true, + + Size = UDim2.new(1, 0, 1, 0), + BackgroundTransparency = 1, + BorderSizePixel = 0, + + ClearTextOnFocus = false, + Font = font, + TextSize = textSize, + TextXAlignment = Enum.TextXAlignment.Left, + TextYAlignment = Enum.TextYAlignment.Top, + TextColor3 = textColor, + Text = text, + + [Roact.Event.Focused] = function() + self.props.FocusChanged(true) + end, + + [Roact.Event.FocusLost] = function() + self.props.FocusChanged(false) + end, + + [Roact.Event.MouseEnter] = self.mouseEnter, + [Roact.Event.MouseLeave] = self.mouseLeave, + + [Roact.Change.Text] = self.textChanged, + + [Roact.Ref] = self.textBoxRef, + }), + }) +end + +return MultilineTextEntry diff --git a/BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Components/MultilineTextEntry.spec.lua b/BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Components/MultilineTextEntry.spec.lua new file mode 100644 index 0000000000..30bf91066b --- /dev/null +++ b/BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Components/MultilineTextEntry.spec.lua @@ -0,0 +1,47 @@ +return function() + local Library = script.Parent.Parent + local Roact = require(Library.Parent.Parent.Roact) + + local MockWrapper = require(Library.MockWrapper) + + local MultilineTextEntry = require(script.Parent.MultilineTextEntry) + + local function createTestMultilineTextEntry(visible) + return Roact.createElement(MockWrapper, {}, { + MultilineTextEntry = Roact.createElement(MultilineTextEntry, { + Text = "Text", + Visible = visible, + TextSize = 22, + }), + }) + end + + it("should create and destroy without errors", function() + local element = createTestMultilineTextEntry(true) + local instance = Roact.mount(element) + Roact.unmount(instance) + end) + + it("should render correctly", function () + local container = Instance.new("Folder") + local instance = Roact.mount(createTestMultilineTextEntry(true), container) + local frame = container:FindFirstChildOfClass("Frame") + + expect(frame.ScrollingFrame).to.be.ok() + expect(frame.ScrollingFrame).to.be.ok() + expect(frame.ScrollingFrame.Padding).to.be.ok() + expect(frame.ScrollingFrame.Text).to.be.ok() + + Roact.unmount(instance) + end) + + it("should hide its text when not visible", function () + local container = Instance.new("Folder") + local instance = Roact.mount(createTestMultilineTextEntry(false), container) + local frame = container:FindFirstChildOfClass("Frame") + + expect(frame.ScrollingFrame.Text.Visible).to.equal(false) + + Roact.unmount(instance) + end) +end \ No newline at end of file diff --git a/BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Components/PluginWidget/Dialog.lua b/BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Components/PluginWidget/Dialog.lua new file mode 100644 index 0000000000..9e264c33c7 --- /dev/null +++ b/BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Components/PluginWidget/Dialog.lua @@ -0,0 +1,93 @@ +--[[ + Creates a QWidgetPluginGui dialog. + + Props: + table Options = An options table to pass to the Create function. + + bool Enabled = Whether the dialog is currently enabled. + string Title = The title to display at the top of the dialog. + string Name = The name of the dialog. + ZIndexBehavior ZIndexBehavior = The ordering behavior of elements + in the dialog based on ZIndex. + + function OnClose() = A callback for when the dialog closes. +]] + +local Library = script.Parent.Parent.Parent + +local HttpService = game:GetService("HttpService") + +local Plugin = require(Library.Plugin) +local getPlugin = Plugin.getPlugin +local Roact = require(Library.Parent.Parent.Roact) + +local Focus = require(Library.Focus) +local FocusProvider = Focus.Provider + +local Dialog = Roact.PureComponent:extend("Dialog") + +function Dialog:init(props) + local options = props.Options + local title = props.Title or "" + local name = props.Name or title + local id = title .. HttpService:GenerateGUID() + local zIndexBehavior = props.ZIndexBehavior or Enum.ZIndexBehavior.Sibling + + local plugin = getPlugin(self) + local widget = plugin:CreateQWidgetPluginGui(id, options) + widget.Name = name + widget.ZIndexBehavior = zIndexBehavior + self.widget = widget + + if props.OnClose and widget:IsA("PluginGui") then + widget:BindToClose(function() + props.OnClose() + end) + end +end + +function Dialog:updateWidget() + local props = self.props + local enabled = props.Enabled + local title = props.Title + + local widget = self.widget + if widget then + if enabled ~= nil then + widget.Enabled = enabled + end + + if title ~= nil and widget:IsA("PluginGui") then + widget.Title = title + end + end +end + +function Dialog:didMount() + self:updateWidget() +end + +function Dialog:didUpdate() + self:updateWidget() +end + +function Dialog:render() + return self.widget.Enabled and Roact.createElement(Roact.Portal, { + target = self.widget, + }, { + FocusProvider = Roact.createElement(FocusProvider, { + pluginGui = self.widget, + }, self.props[Roact.Children]), + }) +end + +function Dialog:willUnmount() + if self.changedConnection then + self.changedConnection:Disconnect() + end + if self.widget then + self.widget:Destroy() + end +end + +return Dialog \ No newline at end of file diff --git a/BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Components/Preview/ActionBar.lua b/BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Components/Preview/ActionBar.lua new file mode 100644 index 0000000000..63f1b0995a --- /dev/null +++ b/BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Components/Preview/ActionBar.lua @@ -0,0 +1,192 @@ +--[[ + This component is responsible for managing action bar, which provides two components. + Insert button and open more button. + + Necessary properties: + Position = UDim2 + Size = UDim2 + TryInsert = call back + Text = button text + Color = button color + + Optionlal properties: + LayoutOrder = num + AssetId = id, for analytics + InstallDisabled = true if we're a plugin and we are loading, disables install attempts while loading + DisplayResultOfInsertAttempt = if true, overwrites button color/text once you click it based on result of insert + ShowRobuxIcon = Whether to show a robux icon next to the text. +]] + +local Library = script.Parent.Parent.Parent + +local Roact = require(Library.Parent.Parent.Roact) + +local GetTextSize = require(Library.Utils.GetTextSize) + +local Theming = require(Library.Theming) +local withTheme = Theming.withTheme + +local RoundFrame = require(Library.Components.RoundFrame) + +local ActionBar = Roact.PureComponent:extend("ActionBar") + +local BUTTON_STATUS = { + default = 0, + hovered = 1, +} + + +function ActionBar:init(props) + self.state = { + insertButtonStatus = BUTTON_STATUS.default + } + + self.onInsertButtonEnter = function() + self:setState({ + insertButtonStatus = BUTTON_STATUS.hovered + }) + end + + self.onInsertButtonLeave = function() + self:setState({ + insertButtonStatus = BUTTON_STATUS.default + }) + end + + self.onShowMoreActiveted = function() + self.props.TryCreateContextMenu() + end + + self.onInsertActivated = function() + -- If we're working with a plugin, it might still be loading/already clicked and completed + -- In these cases, we do not want to allow an insert attempt + if self.props.InstallDisabled then + return + end + + self.props.TryInsert() + end +end + +function ActionBar:render() + return withTheme(function(theme) + local props = self.props + local size = props.Size + local position = props.Position + local anchorPoint = props.AnchorPoint + local showRobuxIcon = props.ShowRobuxIcon + local isDisabled = props.InstallDisabled + local layoutOrder = props.LayoutOrder + + local text = props.Text + + local actionBarTheme = theme.assetPreview.actionBar + + local color = actionBarTheme.button.backgroundColor + if isDisabled then + color = actionBarTheme.button.backgroundDisabledColor + elseif self.state.insertButtonStatus == BUTTON_STATUS.hovered then + color = actionBarTheme.button.backgroundHoveredColor + end + + local textColor = isDisabled and actionBarTheme.text.colorDisabled or actionBarTheme.text.color + local textWidth = GetTextSize(text, theme.assetPreview.textSizeLarge, theme.assetPreview.fontBold).X + + local padding = -(actionBarTheme.padding * 2 + actionBarTheme.centerPadding) + + return Roact.createElement("Frame", { + Size = size, + Position = position, + AnchorPoint = anchorPoint, + + BackgroundTransparency = 0, + BackgroundColor3 = actionBarTheme.background, + BorderSizePixel = 0, + + LayoutOrder = layoutOrder, + },{ + UIPadding = Roact.createElement("UIPadding", { + PaddingBottom = UDim.new(0, 12), + PaddingLeft = UDim.new(0, 12), + PaddingRight = UDim.new(0, 12), + PaddingTop = UDim.new(0, 12), + }), + + UIListLayout = Roact.createElement("UIListLayout", { + FillDirection = Enum.FillDirection.Horizontal, + HorizontalAlignment = Enum.HorizontalAlignment.Center, + VerticalAlignment = Enum.VerticalAlignment.Center, + SortOrder = Enum.SortOrder.LayoutOrder, + Padding = UDim.new(0, 10), + }), + + ShowMoreButton = Roact.createElement(RoundFrame, { + Size = UDim2.new(0, 28, 0, 28), + + BackgroundColor3 = actionBarTheme.showMore.backgroundColor, + BackgroundTransparency = 0, + BorderSizePixel = 1, + BorderColor3 = actionBarTheme.showMore.borderColor, + + OnActivated = self.onShowMoreActiveted, + + LayoutOrder = 1, + }, { + ShowMoreImageLabel = Roact.createElement("ImageLabel", { + AnchorPoint = Vector2.new(0.5, 0.5), + Position = UDim2.new(0.5, 0, 0.5, 0), + Size = UDim2.new(0, 16, 0, 16), + + Image = actionBarTheme.images.showMore, + BackgroundTransparency = 1, + }) + }), + + InsertButton = Roact.createElement(RoundFrame, { + Size = UDim2.new(1, padding, 1, 0), + BackgroundColor3 = color, + BackgroundTransparency = 0, + BorderSizePixel = 0, + + OnActivated = self.onInsertActivated, + OnMouseEnter = self.onInsertButtonEnter, + OnMouseLeave = self.onInsertButtonLeave, + + LayoutOrder = 2, + }, { + Layout = Roact.createElement("UIListLayout", { + SortOrder = Enum.SortOrder.LayoutOrder, + FillDirection = Enum.FillDirection.Horizontal, + VerticalAlignment = Enum.VerticalAlignment.Center, + HorizontalAlignment = Enum.HorizontalAlignment.Center, + Padding = UDim.new(0, 2) + }), + + Icon = showRobuxIcon and Roact.createElement("ImageLabel", { + LayoutOrder = 1, + Size = actionBarTheme.robuxSize, + BackgroundTransparency = 1, + Image = actionBarTheme.images.robuxSmall, + ImageColor3 = actionBarTheme.images.colorWhite, + }), + + InsertTextLabel = Roact.createElement("TextLabel", { + LayoutOrder = 2, + Size = UDim2.new(0, textWidth, 1, 0), + + Text = text, + Font = theme.assetPreview.fontBold, + TextSize = theme.assetPreview.textSizeMedium, + TextColor3 = textColor, + TextXAlignment = Enum.TextXAlignment.Center, + TextYAlignment = Enum.TextYAlignment.Center, + + BackgroundTransparency = 1, + BorderSizePixel = 0, + }), + }), + }) + end) +end + +return ActionBar \ No newline at end of file diff --git a/BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Components/Preview/ActionBar.spec.lua b/BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Components/Preview/ActionBar.spec.lua new file mode 100644 index 0000000000..5b94387e48 --- /dev/null +++ b/BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Components/Preview/ActionBar.spec.lua @@ -0,0 +1,26 @@ +return function() + local Library = script.Parent.Parent.Parent + local Roact = require(Library.Parent.Parent.Roact) + + local MockWrapper = require(Library.MockWrapper) + + local ActionBar = require(Library.Components.Preview.ActionBar) + + local function createTestAsset(container, name) + local element = Roact.createElement(MockWrapper, {}, { + Roact.createElement(ActionBar, { + Size = UDim2.new(1, 0, 1, 0), + Position = UDim2.new(0, 0, 0, 0), + AnchorPoint = Vector2.new(0, 1), + Text = "foo", + }) + }) + + return Roact.mount(element, container or nil, name or "") + end + + it("should create and destroy without errors", function() + local instance = createTestAsset() + Roact.unmount(instance) + end) +end \ No newline at end of file diff --git a/BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Components/Preview/AssetDescription.lua b/BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Components/Preview/AssetDescription.lua new file mode 100644 index 0000000000..c5344e5c6e --- /dev/null +++ b/BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Components/Preview/AssetDescription.lua @@ -0,0 +1,98 @@ +--[[ + This component handles the description rows of the assetPreview component. + It's in charge of showing category name in the left and content in the right. + + Required Properties: + Position = UDim2 + LeftContent = string, the name for the category. + RightContent = string, the name of the category. + + Optional Properties: + UseBoldLine = bool, decide if we bold the underlying line or not. + HideSeparator = bool, whether or not to hide the separator after the component + LayoutOrder = num +]] + +local Library = script.Parent.Parent.Parent + +local Roact = require(Library.Parent.Parent.Roact) +local join = require(Library.join) + +local Theming = require(Library.Theming) +local withTheme = Theming.withTheme + +local AssetDescription = Roact.PureComponent:extend("AssetDescription") + +function AssetDescription:render() + return withTheme(function(theme) + local props = self.props + local position = props.Position or UDim2.new(1, 0, 1, 0) + local leftContent = props.LeftContent or "" + local rightContent = props.RightContent or "" + + local useBoldLine = props.UseBoldLine or false + local hideSeparator = props.HideSeparator or false + + local descriptionTheme = theme.assetPreview.description + + local layoutOrder = props.LayoutOrder + + local children = join({ + -- Make sure left side and right side won't be cut off. + UIPadding = Roact.createElement("UIPadding", { + PaddingBottom = UDim.new(0, 0), + PaddingLeft = UDim.new(0, 1), + PaddingRight = UDim.new(0, 1), + PaddingTop = UDim.new(0, 0), + }), + + LeftContent = Roact.createElement("TextLabel", { + Size = UDim2.new(1, 0, 1, 0), + + Text = leftContent, + Font = theme.assetPreview.font, + TextColor3 = descriptionTheme.leftTextColor, + TextSize = theme.assetPreview.textSizeLarge, + TextXAlignment = Enum.TextXAlignment.Left, + + BackgroundTransparency = 1, + + AutoLocalize = false, + }), + + RightContent = Roact.createElement("TextLabel", { + Size = UDim2.new(1, 0, 1, 0), + + Text = rightContent, + Font = theme.assetPreview.font, + TextColor3 = descriptionTheme.rightTextColor, + TextSize = theme.assetPreview.textSizeLarge, + TextXAlignment = Enum.TextXAlignment.Right, + + BackgroundTransparency = 1, + + AutoLocalize = false, + }), + + ButtonLine = not hideSeparator and Roact.createElement("Frame", { + Position = UDim2.new(0, 0, 1, 3), + Size = UDim2.new(1, 0, 0, 1), + + BorderSizePixel = useBoldLine and 1 or 0, + BackgroundColor3 = descriptionTheme.lineColor, + BorderColor3 = descriptionTheme.lineColor, + }) + }, props[Roact.Children] or {}) + + return Roact.createElement("Frame", { + Position = position, + Size = UDim2.new(1, 0, 0, theme.assetPreview.description.height), + + BackgroundTransparency = 1, + BackgroundColor3 = descriptionTheme.background, + LayoutOrder = layoutOrder, + }, children) + end) +end + +return AssetDescription diff --git a/BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Components/Preview/AssetDescription.spec.lua b/BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Components/Preview/AssetDescription.spec.lua new file mode 100644 index 0000000000..2a4153e437 --- /dev/null +++ b/BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Components/Preview/AssetDescription.spec.lua @@ -0,0 +1,28 @@ +return function() + local Library = script.Parent.Parent.Parent + local Roact = require(Library.Parent.Parent.Roact) + + local MockWrapper = require(Library.MockWrapper) + + local AssetDescription = require(Library.Components.Preview.AssetDescription) + + local function createTestAsset(container, name) + local element = Roact.createElement(MockWrapper,{},{ + AssetDescription = Roact.createElement(AssetDescription, { + Position = UDim2.new(1, 0, 1, 0), + Size = UDim2.new(1, 0, 1, 0), + LeftContent = "", + RightContent = "", + + UseBoldLine = false, + }) + }) + + return Roact.mount(element, container or nil, name or "") + end + + it("should create and destroy without errors", function() + local instance = createTestAsset() + Roact.unmount(instance) + end) +end diff --git a/BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Components/Preview/AssetPreview.lua b/BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Components/Preview/AssetPreview.lua new file mode 100644 index 0000000000..69c9fcdbc6 --- /dev/null +++ b/BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Components/Preview/AssetPreview.lua @@ -0,0 +1,512 @@ +--[[ + AssetPreview component is responsible for manageing the models will be displaying on the + ViewPortFrame. + Models, detail information regarding the asset should be coming from the parent. Asnyc reqest for getting + the asset should also be done in the parent. + + Necessary property: + Position = UDim2, the position of Asset Preview with respect to it's parent. + AnchorPoint = Vector2, used to center Asset Preview with respect to it's parent. + MaxPreviewWidth = number, the maximum width allowed for this component. + MaxPreviewHeight = number, the maximum height allowed for this component. + + AssetData = table, a table contains asset data. + CurrentPreview = asset , the current asset displayed in AssetPreview both for 3D view and Tree View. + + ActionBarText = string, the text shown in the large button of the ActionBar. + TryInsert = callback, this determines the behavior of the large button in the ActionBar. + + OnFavoritedActivated = callback, this callback is invoked when the favorites button is clicked for the asset. + FavoriteCounts = number, the number of favorites that this asset has. + Favorited = boolean, whether or not the current user has this asset favorited. + + TryCreateContextMenu = callback, that creates a context menu in the triple dot (...) of the ActionBar. + OnTreeItemClicked = callback, that determines the behavior of when an item is clicked in the TreeView + The TreeView is a part of the PreviewController. + + Optional property: + InstallDisabled = boolean, used in PluginPurchaseFlow to disable the ActionBar install button + when the plugin is already installed. + PurchaseFlow = component, component which is the start of the PluginPurhaseFlow + SuccessDialog = component, success dialog shown at the end of the PluginPurchaseFlow + ShowRobuxIcon = boolean, to determine whether or not the Robux Icon should be shown in the ActionBar. + ShowInstallationBar = boolean, determines if the installation bar should be shown, this is used in PluginPurchaseFlow + LoadingBarText = string, the text that should be displayed with the loading/installation bar. + + HasRating = boolean, determines whether or not Voting and Favorites should be displayed. + Voting = table, table of voting information structed as: + { + UpVotes = number, + DownVotes = number, + } + OnVoteUp = callback, to be invoked when the vote up button is clicked in the Vote component. + OnVoteDown = callback, to be invoked when the vote down button is clicked in the Vote component. + + SearchByCreator = callback, to search for asset in the current Marketplace category + that are created by the same creator as current asset. + + ZIndex = num, used to override the zIndex depth of the base button. +]] + +local RunService = game:GetService("RunService") +local StudioService = game:GetService("StudioService") + +local Library = script.Parent.Parent.Parent + +local Roact = require(Library.Parent.Parent.Roact) + +local Favorites = require(Library.Components.Preview.Favorites) +local PreviewController = require(Library.Components.Preview.PreviewController) +local Vote = require(Library.Components.Preview.Vote) +local ActionBar = require(Library.Components.Preview.ActionBar) +local AssetDescription = require(Library.Components.Preview.AssetDescription) +local LoadingBar = require(Library.Components.LoadingBar) +local SearchLinkText = require(Library.Components.Preview.SearchLinkText) + +local LayoutOrderIterator = require(Library.Utils.LayoutOrderIterator) +local GetTextSize = require(Library.Utils.GetTextSize) + +local Theming = require(Library.Theming) +local withTheme = Theming.withTheme + +local AssetType = require(Library.Utils.AssetType) + +local AssetPreview = Roact.PureComponent:extend("AssetPreview") + +-- TODO: Later, I will need to move all the unchanged numbers +-- into the constants. +local TITLE_HEIGHT = 18 + +local VERTICAL_PADDING = 10 +local TOP_PADDING = 12 +local BOTTOM_PADDING = 20 + +local VOTE_HEIGHT = 36 + +local ACTION_BAR_HEIGHT = 52 +local INSTALLATION_BAR_SECTION_HEIGHT = 80 +local INSTALLATION_BAR_SECTION_PADDING = 16 +local INSTALLATION_BAR_HEIGHT = 6 +local INSTALLATION_ANIMATION_TIME = 1.0 --seconds + + +-- Multiply minimum treeview width by 2 to get minimum threshold +-- When the asset preview is twice the minimum width, then we +-- can split the view in half to show the treeview on the right. +local TREEVIEW_ON_BOTTOM_WIDTH_THRESHOLD = 242 * 2 + +local function getGenreString(genreArray) + local arraySize = #genreArray + if arraySize == 0 then + return "All" + else + return tostring(genreArray[1]) + end +end + +function AssetPreview:init(props) + self.state = { + enableScroller = true, + overrideEnableVoting = false, + } + + self.assetSizeInited = false + + self.baseScrollRef = Roact.createRef() + self.baseLayouterRef = Roact.createRef() + + self.assetBaseButtonRef = Roact.createRef() + + self.onModelPreviewFrameEntered = function() + self:setState({ + enableScroller = false + }) + end + + self.onModelPreviewFrameLeft = function() + self:setState({ + enableScroller = true + }) + end + + -- For first time setting the canvas size. + self.onScrollContentSizeChange = function(rbx) + local baseScroller = self.baseScrollRef.current + local listLayouter = self.baseLayouterRef.current + local absSize = listLayouter and listLayouter.AbsoluteContentSize or Vector2.new() + if baseScroller and listLayouter then + baseScroller.CanvasSize = UDim2.new(1, 0, 0, absSize.Y + BOTTOM_PADDING) + end + + self:adjustAssetHeight() + end + + self.adjustAssetHeight = function() + -- Init the total height of asset preview component + local listLayouter = self.baseLayouterRef.current + local assetBaseButton = self.assetBaseButtonRef.current + if assetBaseButton then + local absSize = listLayouter and listLayouter.AbsoluteContentSize or Vector2.new() + local assetHeight = math.min(absSize.Y + ACTION_BAR_HEIGHT + BOTTOM_PADDING, self.props.MaxPreviewHeight) + assetBaseButton.Size = UDim2.new(0, self.props.MaxPreviewWidth, 0, assetHeight) + end + end + + self.searchByCreator = function() + local assetData = props.AssetData + local creator = assetData.Creator + local creatorName = creator.Name + if self.props.SearchByCreator then + self.props.SearchByCreator(creatorName) + end + end + if self.props.ClearPurchaseFlow then + self.props.ClearPurchaseFlow(props.AssetData.Asset.Id) + end +end + +function AssetPreview:didMount() + --[[ + FIXME (psewell) + THIS IS A HACK! ScrollingFrames can sometimes render the scroll bar in the + wrong place. Because of this, we have to hide the ScrollingFrame for a step + so that the scroll bar appears in the right place when we make it visible. + + This is a temporary fix recommended by PlayerEx. + There is a permanent fix on the way for this bug in C++. + See https://jira.rbx.com/browse/CLIPLAYEREX-2494 + We will enable the flag FFlagStudioRemoveToolboxScrollingFrameHack when the fix is done. + ]] + + local scrollingFrame = self.baseScrollRef.current + local baseButton = self.assetBaseButtonRef.current + if scrollingFrame and baseButton then + local stepConnection + stepConnection = RunService.Heartbeat:Connect(function() + scrollingFrame.Visible = true + stepConnection:Disconnect() + end) + end +end + +function AssetPreview:didUpdate() + self:adjustAssetHeight() +end + +function AssetPreview:render() + return withTheme(function(theme) + -- TODO: Time to tide up the properties passed from the asset. + local props = self.props + + local assetPreviewTheme = theme.assetPreview + + local maxPreviewWidth = props.MaxPreviewWidth + local maxPreviewHeight = props.MaxPreviewHeight + + local position = props.Position + local anchorPoint = props.AnchorPoint + + local assetData = props.AssetData + + -- Data structure from the server + local Asset = assetData.Asset + local assetId = Asset.Id + local assetName = Asset.Name or "Test Name" + local detailDescription = Asset.Description + local created = Asset.Created + local updated = Asset.Updated + local assetGenres = Asset.AssetGenres + + local creator = assetData.Creator + local creatorName = creator.Name + + local typeId = assetData.Asset.TypeId or Enum.AssetType.Model.Value + + local currentPreview = props.CurrentPreview + local previewModel = props.PreviewModel + + local assetPreviewType + if (typeId == Enum.AssetType.Plugin.Value) then + assetPreviewType = AssetType:markAsPlugin() + else + assetPreviewType = AssetType:getAssetType(currentPreview) + end + + local isPluginAsset, isPluginInstalled + isPluginAsset = AssetType:isPlugin(assetPreviewType) + isPluginInstalled = isPluginAsset and StudioService:IsPluginInstalled(assetId) + + local installDisabled = props.InstallDisabled + local showRobuxIcon = props.ShowRobuxIcon or false + local purchaseFlow = props.PurchaseFlow or nil + local successDialog = props.SuccessDialog or nil + local showInstallationBar = props.ShowInstallationBar or false + + local hasRating = props.HasRating or false + + local voting = props.Voting or {} + local upVoteRate = 0 + if voting.UpVotes and voting.DownVotes then + local totalVotes = voting.UpVotes + voting.DownVotes + if totalVotes > 0 then + upVoteRate = voting.UpVotes / totalVotes + end + end + local rating = upVoteRate * 100 + + local putTreeviewOnBottom = maxPreviewWidth <= TREEVIEW_ON_BOTTOM_WIDTH_THRESHOLD + + local assetSize = UDim2.new(0, maxPreviewWidth, 0, maxPreviewHeight) + + local zIndex = props.ZIndex or 0 + + local onTreeItemClicked = props.OnTreeItemClicked + + local tryCreateContextMenu = props.TryCreateContextMenu + + local enableScroller = self.state.enableScroller + + local detailDescriptionWidth = props.MaxPreviewWidth - 4 * assetPreviewTheme.padding - 2 + local textSize = GetTextSize(detailDescription, + assetPreviewTheme.textSizeLarge, + assetPreviewTheme.font, + Vector2.new(detailDescriptionWidth, 9000)) + local detailDescriptionHeight = textSize.y + VERTICAL_PADDING + + local layoutIndex = LayoutOrderIterator.new() + + local closeImageSize = UDim2.new(0, 28, 0, 28) + + return Roact.createElement("ImageButton", { + Position = position, + Size = assetSize, + AnchorPoint = anchorPoint, + + ZIndex = zIndex, + + BackgroundTransparency = 0, + BackgroundColor3 = assetPreviewTheme.background, + AutoButtonColor = false, + BorderSizePixel = 0, + + [Roact.Ref] = self.assetBaseButtonRef, + },{ + CloseImage = Roact.createElement("ImageLabel", { + Position = UDim2.new(1, 0, 0, 0), + Size = closeImageSize, + AnchorPoint = Vector2.new(0, 1), + + Image = assetPreviewTheme.images.deleteButton, + BackgroundTransparency = 1, + }), + + BaseScrollFrame = Roact.createElement("ScrollingFrame", { + Size = UDim2.new(1, assetPreviewTheme.padding, 1, -ACTION_BAR_HEIGHT), + Visible = true, --See comment in didMount + + ScrollBarThickness = 8, + ScrollBarImageColor3 = theme.scrollingFrame.scrollbarImageColor, + BorderSizePixel = 0, + BackgroundTransparency = 1, + TopImage = assetPreviewTheme.images.scrollbarTopImage, + MidImage = assetPreviewTheme.images.scrollbarMiddleImage, + BottomImage = assetPreviewTheme.images.scrollbarBottomImage, + ScrollingEnabled = enableScroller, + + [Roact.Ref] = self.baseScrollRef, + },{ + UIPadding = Roact.createElement("UIPadding", { + PaddingBottom = UDim.new(0, BOTTOM_PADDING), + PaddingLeft = UDim.new(0, assetPreviewTheme.padding), + PaddingRight = UDim.new(0, assetPreviewTheme.padding * 2), + PaddingTop = UDim.new(0, TOP_PADDING), + }), + + UIListLayout = Roact.createElement("UIListLayout", { + FillDirection = Enum.FillDirection.Vertical, + HorizontalAlignment = Enum.HorizontalAlignment.Center, + VerticalAlignment = Enum.VerticalAlignment.Top, + SortOrder = Enum.SortOrder.LayoutOrder, + Padding = UDim.new(0, VERTICAL_PADDING), + + [Roact.Change.AbsoluteContentSize] = self.onScrollContentSizeChange, + [Roact.Ref] = self.baseLayouterRef, + }), + + AssetName = Roact.createElement("TextLabel", { + Size = UDim2.new(1, 0, 0, TITLE_HEIGHT), + + Text = assetName, + Font = assetPreviewTheme.fontBold, + TextSize = assetPreviewTheme.textSizeTitle, + TextXAlignment = Enum.TextXAlignment.Left, + TextColor3 = assetPreviewTheme.assetNameColor, + BackgroundTransparency = 1, + TextTruncate = Enum.TextTruncate.AtEnd, + + AutoLocalize = false, + + LayoutOrder = layoutIndex:getNextOrder(), + }), + + Rating = hasRating and Roact.createElement("Frame", { + BackgroundTransparency = 1, + Size = UDim2.new(1, 0, 0, 12), + + LayoutOrder = layoutIndex:getNextOrder(), + }, { + VoteIcon = Roact.createElement("ImageLabel", { + Size = UDim2.new(0, 16, 0, 16), + BackgroundTransparency = 1, + Image = assetPreviewTheme.images.thumbUpSmall, + }), + + VoteText = Roact.createElement("TextLabel", { + Size = UDim2.new(1, 0, 1, 0), + Position = UDim2.new(0, 22, 0, 3), + BackgroundTransparency = 1, + + Text = ("%d%%"):format(rating), + TextXAlignment = Enum.TextXAlignment.Left, + TextSize = assetPreviewTheme.textSizeMedium, + Font = assetPreviewTheme.Font, + TextColor3 = theme.assetPreview.vote.textColor, + + LayoutOrder = 1, + }), + }), + + PreviewController = Roact.createElement(PreviewController, { + Width = assetPreviewTheme.padding * 2, + + CurrentPreview = currentPreview, + PreviewModel = previewModel, + AssetPreviewType = assetPreviewType, + AssetId = assetId, + PutTreeviewOnBottom = putTreeviewOnBottom, + + OnTreeItemClicked = onTreeItemClicked, + OnModelPreviewFrameEntered = self.onModelPreviewFrameEntered, + OnModelPreviewFrameLeft = self.onModelPreviewFrameLeft, + + LayoutOrder = layoutIndex:getNextOrder(), + }), + + LoadingIndicator = showInstallationBar and Roact.createElement("Frame", { + Size = UDim2.new(1, 0, 0, INSTALLATION_BAR_SECTION_HEIGHT), + BackgroundTransparency = 1, + LayoutOrder = layoutIndex:getNextOrder(), + }, { + UIPadding = Roact.createElement("UIPadding", { + PaddingBottom = UDim.new(0, 0), + PaddingLeft = UDim.new(0, INSTALLATION_BAR_SECTION_PADDING), + PaddingRight = UDim.new(0, INSTALLATION_BAR_SECTION_PADDING), + PaddingTop = UDim.new(0, (INSTALLATION_BAR_SECTION_HEIGHT * 0.5) + 10), + }), + + LoadingBar = Roact.createElement(LoadingBar, { + LoadingText = self.props.LoadingBarText, + Size = UDim2.new(1, 0, 0, INSTALLATION_BAR_HEIGHT), + HoldPercent = 0.92, + LoadingTime = INSTALLATION_ANIMATION_TIME, + InstallationFinished = isPluginInstalled, + }), + }), + + Favorites = Roact.createElement(Favorites, { + Size = UDim2.new(1, 0, 0, 20), + + FavoriteCounts = self.props.FavoriteCounts, + Favorited = self.props.Favorited, + + OnActivated = self.props.OnFavoritedActivated, + + LayoutOrder = layoutIndex:getNextOrder(), + }), + + DetailDescription = Roact.createElement("TextLabel", { + Size = UDim2.new(1, 0, 0, detailDescriptionHeight), + + BackgroundTransparency = 1, + TextWrapped = true, + + Text = detailDescription, + TextSize = assetPreviewTheme.textSizeLarge, + Font = assetPreviewTheme.font, + TextColor3 = assetPreviewTheme.descriptionTextColor, + TextXAlignment = Enum.TextXAlignment.Left, + + LayoutOrder = layoutIndex:getNextOrder(), + }), + + Vote = hasRating and Roact.createElement(Vote, { + Size = UDim2.new(1, 0, 0, VOTE_HEIGHT), + + Voting = voting, + AssetId = assetId, + + OnVoteUpButtonActivated = props.OnVoteUp, + OnVoteDownButtonActivated = props.OnVoteDown, + + LayoutOrder = layoutIndex:getNextOrder(), + }), + + Developer = Roact.createElement(AssetDescription, { + LeftContent = "Creator", + RightContent = "", + + LayoutOrder = layoutIndex:getNextOrder(), + }, { + LinkText = Roact.createElement(SearchLinkText, { + Text = creatorName, + Position = UDim2.fromScale(1, 0.5), + AnchorPoint = Vector2.new(1, 0.5), + OnClick = self.searchByCreator, + }) + }), + + Category = Roact.createElement(AssetDescription, { + LeftContent = "Genre", + RightContent = getGenreString(assetGenres), + + LayoutOrder = layoutIndex:getNextOrder(), + }), + + Created = Roact.createElement(AssetDescription, { + LeftContent = "Created", + RightContent = created, + + LayoutOrder = layoutIndex:getNextOrder(), + }), + + Updated = Roact.createElement(AssetDescription, { + LeftContent = "Last Updated", + RightContent = updated, + HideSeparator = true, + + LayoutOrder = layoutIndex:getNextOrder(), + }) + }), + + ActionBar = Roact.createElement(ActionBar, { + Text = self.props.ActionBarText, + Size = UDim2.new(1, 0, 0, ACTION_BAR_HEIGHT), + Position = UDim2.new(0, 0, 1, 0), + AnchorPoint = Vector2.new(0, 1), + AssetId = assetId, + + Asset = Asset, + TryInsert = self.props.TryInsert, + TryCreateContextMenu = tryCreateContextMenu, + InstallDisabled = installDisabled, + ShowRobuxIcon = showRobuxIcon, + }), + + PurchaseFlow = purchaseFlow, + + SuccessDialog = successDialog, + }) + end) +end + +return AssetPreview \ No newline at end of file diff --git a/BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Components/Preview/AssetPreview.spec.lua b/BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Components/Preview/AssetPreview.spec.lua new file mode 100644 index 0000000000..c684365bf8 --- /dev/null +++ b/BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Components/Preview/AssetPreview.spec.lua @@ -0,0 +1,71 @@ +return function() + local Library = script.Parent.Parent.Parent + local Roact = require(Library.Parent.Parent.Roact) + + local MockWrapper = require(Library.MockWrapper) + + local AssetPreview = require(Library.Components.Preview.AssetPreview) + + local function createTestAsset(container, name) + local testModel = Instance.new("Model") + + local assetData = { + Asset = { + Id = 123456, + Description = "This is a test asset", + Created = "", + Updated = "", + AssetGenres = {}, + }, + + Creator = { + Name = "Roblox Studio", + }, + } + + local element = Roact.createElement(MockWrapper, {}, { + AssetPreview = Roact.createElement(AssetPreview, { + Position = UDim2.new(0.5, 0, 0.5, 0), + AnchorPoint = Vector2.new(0.5, 0.5), + AssetData = assetData, + CurrentPreview = testModel, + + ShowInstallationBar = true, + InstallDisabled = false, + ShowRobuxIcon = true, + LoadingBarText = "Installing", + + FavoriteCounts = 1000, + Favorited = true, + OnFavoritedActivated = function() end, + + OnTreeItemClicked = function() end, + TryCreateContextMenu = function() end, + SearchByCreator = function() end, + + Voting = {}, + OnVoteUp = function() end, + OnVoteDown = function() end, + + ActionBarText = "Insert", + CanInsertAsset = true, + TryInsert = function() end, + + MaxPreviewWidth = 250, + MaxPreviewHeight = 400, + + ZIndex = 0, + + PurchaseFlow = Roact.createElement("Frame"), + SuccessDialog = Roact.createElement("Frame"), + }) + }) + + return Roact.mount(element, container or nil, name or "") + end + + it("should create and destroy without errors", function() + local instance = createTestAsset() + Roact.unmount(instance) + end) +end \ No newline at end of file diff --git a/BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Components/Preview/AudioControl.lua b/BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Components/Preview/AudioControl.lua new file mode 100644 index 0000000000..42617ae0a1 --- /dev/null +++ b/BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Components/Preview/AudioControl.lua @@ -0,0 +1,137 @@ +--[[ + Audio control is a piece of control panel we used in asset preview to provide play, + pause function for giving assetId. And a time label to show time left. + Start time counter when it's playing. + + Necessary properties: + UDim2 position + UDim2 size + number audioControlOffset, used to control the position of the audio control depending if we show tree view. + number timeLength, length got from the sound instance. + bool isPlaying, come from audio preview, used to change the button control. + number timePassed, audio preview know's the time length, is suited to calculate this. + + function onResume, accept an assetId. + function onPause, pause + function onPlay, This one will reset time length and time remaining. + + the sound object inside the Toolbox plugin to play. We don't want to too many sound source. +]] +local FFlagEnableToolboxVideos = game:GetFastFlag("EnableToolboxVideos") + +local Library = script.Parent.Parent.Parent +local Roact = require(Library.Parent.Parent.Roact) + +local Theming = require(Library.Theming) +local withTheme = Theming.withTheme + +local getTimeString = require(Library.Utils.getTimeString) +local RoundButton = require(Library.Components.RoundFrame) + +local LoadingIndicator = require(Library.Components.LoadingIndicator) +local TIME_LABEL_HEIGHT = 15 +local BUTTON_SIZE = 28 + +local AudioControl = Roact.PureComponent:extend("AudioControl") +if FFlagEnableToolboxVideos then + return AudioControl +end +function AudioControl:init(props) + self.state = { + init = false; + } + + self.onActivated = function() + if not self.props.isLoaded then + return + end + if self.props.isPlaying then + self.pauseASound() + else + self.startPlaying() + end + end + + self.startPlaying = function() + if self.state.init then + props.onResume() + else + -- Will update the time length and time remaining. + props.onPlay() + self:setState({ + init = true + }) + end + end + + self.pauseASound = function() + props.onPause() + end +end + +function AudioControl:render() + return withTheme(function(theme) + local props = self.props + local size = props.size + local anchorPoint = props.anchorPoint + local position = props.position + local timeLength = props.timeLength + local audioPreviewTheme = theme.assetPreview.audioPreview + local audioControlOffset = props.audioControlOffset + local isPlaying = props.isPlaying + local isLoaded = props.isLoaded + + local timePassed = props.timePassed + local timeString = getTimeString(timePassed) .. '/' .. getTimeString(timeLength) + + return Roact.createElement("Frame", { + AnchorPoint = anchorPoint, + BackgroundTransparency = 1, + Position = position, + Size = size, + }, { + Button = Roact.createElement(RoundButton, { + AnchorPoint = Vector2.new(0.5, 0), + AutoButtonColor = false, + BackgroundColor3 = isLoaded and audioPreviewTheme.buttonBackgroundColor or audioPreviewTheme.buttonDisabledBackgroundColor, + BackgroundTransparency = isLoaded and 0 or audioPreviewTheme.buttonDisabledBackgroundTransparency, + BorderSizePixel = 0, + Position = UDim2.new(0, 24, 0, 0), + Size = UDim2.new(0, BUTTON_SIZE, 0, BUTTON_SIZE), + + OnActivated = self.onActivated, + }, { + PlayOrPauseIcon = Roact.createElement("ImageLabel", { + AnchorPoint = Vector2.new(0.5, 0.5), + BackgroundTransparency = 1, + Image = isPlaying and audioPreviewTheme.pauseButton or audioPreviewTheme.playButton, + ImageColor3 = audioPreviewTheme.buttonColor, + ImageTransparency = isLoaded and 0 or audioPreviewTheme.buttonDisabledBackgroundTransparency, + Position = UDim2.new(0.5, 0, 0.5, 0), + Size = UDim2.new(1, 0, 1, 0), + }), + }), + + TimeComponent = isLoaded and Roact.createElement("TextLabel", { + AnchorPoint = Vector2.new(1, 0.5), + Position = UDim2.new(1, -audioControlOffset, 0.5, 0), + Size = UDim2.new(0, 204, 0, TIME_LABEL_HEIGHT), + BorderSizePixel = 0, + BackgroundTransparency = 1, + Text = timeString, + Font = audioPreviewTheme.font, + TextSize = audioPreviewTheme.fontSize, + TextXAlignment = Enum.TextXAlignment.Right, + TextColor3 = audioPreviewTheme.textColor, + }), + + LoadingIndicator = (not isLoaded) and Roact.createElement(LoadingIndicator, { + AnchorPoint = Vector2.new(1, 0.5), + Position = UDim2.new(1, -audioControlOffset, 0.5, 0), + Size = UDim2.new(0, 50, 0, TIME_LABEL_HEIGHT), + }), + }) + end) +end + +return AudioControl \ No newline at end of file diff --git a/BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Components/Preview/AudioControl.spec.lua b/BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Components/Preview/AudioControl.spec.lua new file mode 100644 index 0000000000..4d3945b0f9 --- /dev/null +++ b/BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Components/Preview/AudioControl.spec.lua @@ -0,0 +1,41 @@ +return function() + local FFlagEnableToolboxVideos = game:GetFastFlag("EnableToolboxVideos") + if FFlagEnableToolboxVideos then + return + end + + local Library = script.Parent.Parent.Parent + local Roact = require(Library.Parent.Parent.Roact) + + local MockWrapper = require(Library.MockWrapper) + + local AudioControl = require(Library.Components.Preview.AudioControl) + + local function createTestAsset(container, name) + local emptyFunc = function() + end + + local element = Roact.createElement(MockWrapper, {}, { + Roact.createElement(AudioControl, { + position = UDim2.new(0, 0, 0, 0), + size = UDim2.new(1, 0, 1, 0), + audioControlOffset = 30, + assetId = 0, + timeLength = 1, + isPlaying = false, + isLoaded = true, + timePassed = 0, + onResume = emptyFunc, + onPause = emptyFunc, + onPlay = emptyFunc, + }) + }) + + return Roact.mount(element, container or nil, name or "") + end + + it("should create and destroy without errors", function() + local instance = createTestAsset() + Roact.unmount(instance) + end) +end \ No newline at end of file diff --git a/BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Components/Preview/AudioPreview.lua b/BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Components/Preview/AudioPreview.lua new file mode 100644 index 0000000000..8eb1c5239b --- /dev/null +++ b/BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Components/Preview/AudioPreview.lua @@ -0,0 +1,356 @@ +--[[ + This component will be used in the asset preview for audio asset. + It will provide a still image for sound asset and a progress bar. + The progress bar will keep moving if the sound is playing. + + Necessary properties: + number SoundId, used for play and pause the sound + bool ShowTreeView, used to adjust time label component for audio control based on if we are + showing tree view button or not. + + Optional properties: + UDim2 position, default to UDim2(0, 0, 0, 0) + UDim2 size, default to UDim2(1, 0, 1, 0) + number layoutOrder, used by the layouter to change the position of the component + callBack ReportPlay, analytics events. + callback ReportPause, + + Props automatically received from wrapMedia(): + number _CurrentTime: The time in seconds that the media's TimePosition should currently be. + boolean _IsPlaying: Whether or not the Sound or VideoFrame should be currently playing. + callback _MediaPlayingUpdateSignal: Called when the media's Changed event is fired. Sets the isPlaying state. + callback _OnMediaEnded: Called when the media's Ended event is fired. Resets the currentTime & stops playing. + callback _Pause: Called when clicking the pause button. + callback _Play: Called when clicking the play button. + callBack _SetCurrentTime: Called if the currentTime has been changed, such as when moving a progressbar slider. +]] +local FFlagHideOneChildTreeviewButton = game:GetFastFlag("HideOneChildTreeviewButton") +local FFlagEnableToolboxVideos = game:GetFastFlag("EnableToolboxVideos") + +local RunService = game:GetService("RunService") +local wrapMedia = require(script.Parent.wrapMedia) + +local Library = script.Parent.Parent.Parent +local Roact = require(Library.Parent.Parent.Roact) + +local Theming = require(Library.Theming) +local withTheme = Theming.withTheme + +local PluginContext = require(Library.Plugin) +local getPlugin = PluginContext.getPlugin + +local PROGRESS_BAR_HEIGHT = 6 +local AUDIO_CONTROL_HEIGHT = 35 +local AUDIO_CONTROL_WIDTH_OFFSET_WITH_TREE = 50 +local AUDIO_CONTROL_WIDTH_OFFSET_NO_TREE = 70 + +if FFlagHideOneChildTreeviewButton then + AUDIO_CONTROL_WIDTH_OFFSET_NO_TREE = 10 +end + +local AudioControl = FFlagEnableToolboxVideos and nil or require(Library.Components.Preview.AudioControl) +local MediaControl = require(Library.Components.Preview.MediaControl) + +local AudioPreview = Roact.PureComponent:extend("AudioPreview") + +AudioPreview.defaultProps = { + size = UDim2.new(1, 0, 1, 0), +} + +function AudioPreview:init(props) + local plugin = getPlugin(self) + self.soundRef = Roact.createRef() + + self.state = { + timeLength = 0, + isPlaying = FFlagEnableToolboxVideos and nil or false, + isLoaded = false, + currentTime = FFlagEnableToolboxVideos and nil or 0, + } + + self.playSound = function(assetId) + if FFlagEnableToolboxVideos then + return + end + local soundObj = self.soundRef.current + if soundObj then + soundObj.SoundId = self.props.SoundId + plugin:PlaySound(soundObj) + end + self:setState({ + isPlaying = true, + currentTime = 0, + }) + + if self.props.reportPlay then + self.props.ReportPlay() + end + end + + self.resumeSound = function(assetId) + if FFlagEnableToolboxVideos then + return + end + local soundObj = self.soundRef.current + if soundObj then + soundObj.SoundId = self.props.SoundId + plugin:ResumeSound(soundObj) + end + + self:setState({ + isPlaying = true, + timeLength = soundObj.TimeLength, + }) + + if self.props.reportPlay then + self.props.ReportPlay() + end + end + + self.pauseSound = function(assetId) + if FFlagEnableToolboxVideos then + return + end + local soundObj = self.soundRef.current + if soundObj then + plugin:PauseSound(soundObj) + end + + self:setState({ + isPlaying = false, + }) + + if self.props.ReportPause then + self.props.ReportPause() + end + end + + self.onSoundEnded = function(soundId) + if FFlagEnableToolboxVideos then + return + end + self:setState({ + isPlaying = false, + timeLength = 0, + currentTime = 0, + }) + end + + self.dispatchMediaPlayingUpdate = function(updateType) + local soundObj = self.soundRef.current + if not soundObj or not self.isMounted then + return + end + if updateType == "PLAY" then + soundObj.SoundId = self.props.SoundId + plugin:ResumeSound(soundObj) + if self.props.reportPlay then + self.props.ReportPlay() + end + elseif updateType == "PAUSE" then + plugin:PauseSound(soundObj) + if self.props.ReportPause then + self.props.ReportPause() + end + end + end + + self.onSoundChange = function(rbx, property) + local soundObj = self.soundRef.current + if not self.isMounted then + return + end + local isLoaded = soundObj and soundObj.IsLoaded + if property == "TimeLength" then + self:setState({ + isLoaded = isLoaded, + timeLength = soundObj.TimeLength, + }) + if FFlagEnableToolboxVideos then + self.props._SetTimeLength(soundObj.TimeLength) + end + elseif isLoaded ~= self.state.isLoaded then + self:setState({ + isLoaded = isLoaded, + }) + end + end + + self.getAudioLength = function() + local soundObj = self.soundRef.current + if soundObj then + return math.max(soundObj.TimeLength, 1) + end + end +end + +function AudioPreview:didMount() + self.isMounted = true + if FFlagEnableToolboxVideos then + self.mediaPlayingUpdateConnection = self.props._MediaPlayingUpdateSignal:connect(self.dispatchMediaPlayingUpdate) + else + local soundObj = self.soundRef.current + if soundObj then + soundObj.SoundId = self.props.SoundId + end + + self.runServiceConnection = RunService.RenderStepped:Connect(function(step) + if (not self.state.isPlaying) then + return + end + local state = self.state + local newTime = self.state.currentTime + step + + if newTime >= state.timeLength then + newTime = state.timeLength + end + + if self.isMounted then + self:setState({ + currentTime = newTime + }) + end + end) + end +end + +function AudioPreview:willUnmount() + self.isMounted = false + if FFlagEnableToolboxVideos then + if self.mediaPlayingUpdateConnection then + self.mediaPlayingUpdateConnection:disconnect() + self.mediaPlayingUpdateConnection = nil + end + else + if self.runServiceConnection then + self.runServiceConnection:Disconnect() + end + end +end + +function AudioPreview:render() + return withTheme(function(theme) + local props = self.props + local state = self.state + local position = props.position + local size = props.size + local audioPreviewTheme = theme.assetPreview.audioPreview + + local layoutOrder = props.layoutOrder + local soundId = props.SoundId + + local currentTime = FFlagEnableToolboxVideos and props._CurrentTime or state.currentTime + local pause = props._Pause + local play = props._Play + local onMediaEnded = props._OnMediaEnded + + local progress + if state.timeLength ~= nil and state.timeLength ~= 0 then + progress = currentTime / state.timeLength + else + progress = 0 + end + + local showTreeView = props.ShowTreeView + local audioControlOffset = showTreeView and AUDIO_CONTROL_WIDTH_OFFSET_WITH_TREE or AUDIO_CONTROL_WIDTH_OFFSET_NO_TREE + local timeLength = self.getAudioLength() or 0 + local isLoaded = state.isLoaded + local isPlaying = FFlagEnableToolboxVideos and props._IsPlaying or state.isPlaying + + return Roact.createElement("Frame", { + BackgroundTransparency = 1, + BackgroundColor3 = audioPreviewTheme.backgroundColor, + LayoutOrder = layoutOrder, + Position = position, + Size = size, + },{ + UIListLayout = Roact.createElement("UIListLayout", { + FillDirection = Enum.FillDirection.Vertical, + HorizontalAlignment = Enum.HorizontalAlignment.Left, + VerticalAlignment = Enum.VerticalAlignment.Center, + SortOrder = Enum.SortOrder.LayoutOrder, + Padding = UDim.new(0, 10), + }), + + AudioPlayerFrame = Roact.createElement("Frame", { + Size = UDim2.new(1, 0, 1, -PROGRESS_BAR_HEIGHT- AUDIO_CONTROL_HEIGHT), + BackgroundTransparency = 1, + LayoutOrder = 1, + }, { + AudioPlayerImage = Roact.createElement("ImageLabel", { + Size = UDim2.new(1, 0, 1, 0), + Image = audioPreviewTheme.audioPlay_BG, + ScaleType = Enum.ScaleType.Fit, + BackgroundTransparency = 1, + BorderSizePixel = 0, + ImageColor3 = audioPreviewTheme.audioPlay_BG_Color, + }) + }), + + ProgressBarFrame = Roact.createElement("Frame", { + AnchorPoint = Vector2.new(0, 0.5), + Position = UDim2.new(0, 0, 0.5, 0), + Size = UDim2.new(1, 0, 0, PROGRESS_BAR_HEIGHT), + + BackgroundColor3 = audioPreviewTheme.progressBar_BG_Color, + BorderSizePixel = 0, + BackgroundTransparency = 0, + + LayoutOrder = 2, + }, { + ProgressBar = Roact.createElement("Frame", { + AnchorPoint = Vector2.new(0, 0.5), + BackgroundColor3 = audioPreviewTheme.progressBar, + BackgroundTransparency = 0, + BorderSizePixel = 0, + Position = UDim2.new(0, 0, 0.5, 0), + Size = UDim2.new(progress, 0, 0, PROGRESS_BAR_HEIGHT), + }) + }), + + MediaControl = FFlagEnableToolboxVideos and Roact.createElement(MediaControl, { + LayoutOrder = 3, + IsPlaying = isPlaying, + IsLoaded = isLoaded, + OnPause = pause, + OnPlay = play, + ShowTreeView = showTreeView, + TimeLength = timeLength, + TimePassed = currentTime, + }), + + AudioControlBase = (not FFlagEnableToolboxVideos) and Roact.createElement("Frame", { + Size = UDim2.new(1, 0, 0, AUDIO_CONTROL_HEIGHT), + BackgroundTransparency = 1, + LayoutOrder = 3, + }, { + AudioControl = Roact.createElement(AudioControl, { + position = UDim2.new(0, 0, 0, 0), + size = UDim2.new(1, 0, 0, AUDIO_CONTROL_HEIGHT), + audioControlOffset = audioControlOffset, + timeLength = timeLength, + isPlaying = isPlaying, + isLoaded = isLoaded, + timePassed = state.currentTime, + onResume = self.resumeSound, + onPause = self.pauseSound, + onPlay = self.playSound, + }), + }), + + SoundObj = Roact.createElement("Sound", { + Looped = false, + SoundId = FFlagEnableToolboxVideos and soundId or nil, + [Roact.Ref] = self.soundRef, + [Roact.Event.Changed] = self.onSoundChange, + [Roact.Event.Ended] = FFlagEnableToolboxVideos and onMediaEnded or self.onSoundEnded, + }) + }) + end) +end + +if FFlagEnableToolboxVideos then + return wrapMedia(AudioPreview) +else + return AudioPreview +end \ No newline at end of file diff --git a/BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Components/Preview/AudioPreview.spec.lua b/BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Components/Preview/AudioPreview.spec.lua new file mode 100644 index 0000000000..fc0653e7ec --- /dev/null +++ b/BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Components/Preview/AudioPreview.spec.lua @@ -0,0 +1,25 @@ +return function() + local Library = script.Parent.Parent.Parent + local Roact = require(Library.Parent.Parent.Roact) + + local MockWrapper = require(Library.MockWrapper) + + local AudioPreview = require(Library.Components.Preview.AudioPreview) + + local function createTestAsset(container, name) + local element = Roact.createElement(MockWrapper, {}, { + Roact.createElement(AudioPreview, { + SoundId = 123, + ReportPlay = function() end, + ReportPause = function() end, + }) + }) + + return Roact.mount(element, container or nil, name or "") + end + + it("should create and destroy without errors", function() + local instance = createTestAsset() + Roact.unmount(instance) + end) +end \ No newline at end of file diff --git a/BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Components/Preview/Favorites.lua b/BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Components/Preview/Favorites.lua new file mode 100644 index 0000000000..357dab03cb --- /dev/null +++ b/BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Components/Preview/Favorites.lua @@ -0,0 +1,122 @@ +--[[ + This component is designed to show the favorites counts for the assetPreview. + It will send request to fetch the data when loaded. And update accordingly. + + Necessary Properties: + Size = UDim2, + + FavoriteCounts = number, the number of favorites this asset has. + Favorited = bool, does the current user have this asset favorited. + OnActivated = callback, function to invoke when the favorited button is clicked. + + LayoutOrder = number, +]] + +local Library = script.Parent.Parent.Parent + +local Roact = require(Library.Parent.Parent.Roact) + +local Theming = require(Library.Theming) +local withTheme = Theming.withTheme + +local Favorites = Roact.PureComponent:extend("Favorites") + +-- For less then 10k, we use , to seperate the number. +-- For larger than 10k, we use xxk+ +local function getFavoritesCountString(counts) + local countsString + if counts > 10000 then + countsString = ("%dk+"):format(math.floor(counts / 1000)) + else + if counts > 1000 then + countsString = ("%d,%d"):format(counts / 1000, counts % 1000) + else + countsString = tostring(counts) + end + end + + return countsString +end + +function Favorites:init(props) + self.state = { + hovered = false + } + + self.onMouseEnter = function(rbx, x, y) + self:setState({ + hovered = true + }) + end + + self.onMouseLeave = function(rbx, x, y) + self:setState({ + hovered = false + }) + end +end + +function Favorites:render() + return withTheme(function(theme) + local props = self.props + local state = self.state + + local favoriteCounts = props.FavoriteCounts or 0 + + local favoritesTheme = theme.assetPreview.favorites + local favoritesImage = (state.hovered or props.Favorited) and favoritesTheme.favorited or favoritesTheme.unfavorited + local textContent = getFavoritesCountString(tonumber(favoriteCounts)) + local contentColor = favoritesTheme.contentColor + local size = props.Size + + local layoutOrder = props.LayoutOrder + local verticalAlignment = props.VerticalAlignment or Enum.VerticalAlignment.Center + + return Roact.createElement("Frame", { + Size = size, + + BackgroundTransparency = 1, + LayoutOrder = layoutOrder, + }, { + UIListLayout = Roact.createElement("UIListLayout", { + FillDirection = Enum.FillDirection.Horizontal, + SortOrder = Enum.SortOrder.LayoutOrder, + VerticalAlignment = verticalAlignment, + Padding = UDim.new(0, 4), + }), + + ImageContent = Roact.createElement("ImageButton", { + Size = UDim2.new(0, 20, 0, 20), + + BackgroundTransparency = 1, + + Image = favoritesImage, + + [Roact.Event.MouseEnter] = self.onMouseEnter, + [Roact.Event.MouseLeave] = self.onMouseLeave, + [Roact.Event.Activated] = self.props.OnActivated, + + LayoutOrder = 1, + }), + + TextContent = Roact.createElement("TextLabel", { + Size = UDim2.new(1, -20, 1, 0), + + Text = tostring(textContent), + TextColor3 = contentColor, + Font = theme.assetPreview.font, + TextSize = theme.assetPreview.textSizeMedium, + TextXAlignment = Enum.TextXAlignment.Left, + TextYAlignment = Enum.TextYAlignment.Center, + + BackgroundTransparency = 1, + + LayoutOrder = 2, + }) + }) + end) +end + +return Favorites + + diff --git a/BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Components/Preview/Favorites.spec.lua b/BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Components/Preview/Favorites.spec.lua new file mode 100644 index 0000000000..d0b32e3a35 --- /dev/null +++ b/BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Components/Preview/Favorites.spec.lua @@ -0,0 +1,78 @@ +return function() + local Library = script.Parent.Parent.Parent + local Roact = require(Library.Parent.Parent.Roact) + + local MockWrapper = require(Library.MockWrapper) + + local Favorites = require(Library.Components.Preview.Favorites) + + local IMAGE_FAVORITED = "rbxasset://textures/StudioToolbox/AssetPreview/star_filled.png" + local IMAGE_UNFAVORITED = "rbxasset://textures/StudioToolbox/AssetPreview/star_stroke.png" + + local function createTestFavorites(container, name, props) + local testFavoriteActivationValue = false + local favorited = true + if props then + favorited = props.Favorited + end + + local element = Roact.createElement(MockWrapper, {}, { + Favorites = Roact.createElement(Favorites, { + Size = UDim2.new(1,0,1,0), + + FavoriteCounts = 1000, + Favorited = favorited, + OnActivated = function() + testFavoriteActivationValue = not testFavoriteActivationValue + end, + + LayoutOrder = 1, + }) + }) + + return Roact.mount(element, container or nil, name or "") + end + + it("should create and destroy without errors", function() + local instance = createTestFavorites() + Roact.unmount(instance) + end) + + it("should render correctly", function() + local container = Instance.new("Folder") + local instance = createTestFavorites(container) + + local element = container:FindFirstChildOfClass("Frame") + expect(element.TextContent).to.be.ok() + expect(element.ImageContent).to.be.ok() + end) + + it("should properly set the initial favorite counts", function() + local container = Instance.new("Folder") + local instance = createTestFavorites(container) + + local element = container:FindFirstChildOfClass("Frame") + + expect(element.TextContent.Text).to.be.equal("1000") + end) + + it("should display the correct icon for a favorited asset", function() + local container = Instance.new("Folder") + local instance = createTestFavorites(container, nil, { + Favorited = true + }) + + local element = container:FindFirstChildOfClass("Frame") + expect(element.ImageContent.Image).to.be.equal(IMAGE_FAVORITED) + end) + + it("should display the correct icon for an unfavorited asset", function() + local container = Instance.new("Folder") + local instance = createTestFavorites(container, nil, { + Favorited = false + }) + + local element = container:FindFirstChildOfClass("Frame") + expect(element.ImageContent.Image).to.be.equal(IMAGE_UNFAVORITED) + end) +end \ No newline at end of file diff --git a/BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Components/Preview/ImagePreview.lua b/BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Components/Preview/ImagePreview.lua new file mode 100644 index 0000000000..ad75bcd475 --- /dev/null +++ b/BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Components/Preview/ImagePreview.lua @@ -0,0 +1,57 @@ +--[[ + This component is used to display a single image in an AssetPreview. + + Necessary properties: + Position = UDim2 + Size = UDim2 + ImageContent = String, url/rbxassetid of the image object to shown, + e.g. http://www.roblox.com/asset/?id= + rbxassetid:// + ScaleType = Enum.ScaleType.*, scaling type to use +]] + +local Library = script.Parent.Parent.Parent + +local Roact = require(Library.Parent.Parent.Roact) + +local Theming = require(Library.Theming) +local withTheme = Theming.withTheme + +local ImagePreview = Roact.PureComponent:extend("ImagePreview") + +function ImagePreview:render() + return withTheme(function(theme) + local props = self.props + local imageContent = props.ImageContent + + local position = props.Position + local size = props.Size or UDim2.new(1, 0, 1, 0) + + local imagePreviewTheme = theme.assetPreview.imagePreview + local scaleType = props.ScaleType or Enum.ScaleType.Fit + + local layoutOrder = props.LayoutOrder + + return Roact.createElement("Frame", { + Position = position, + Size = size, + + BackgroundTransparency = 0, + BackgroundColor3 = imagePreviewTheme.background, + BorderSizePixel = 0, + + LayoutOrder = layoutOrder, + },{ + ImageContent = Roact.createElement("ImageLabel", { + Size = UDim2.new(1, 0, 1, 0), + ScaleType = scaleType, + + BackgroundTransparency = 1, + + Image = imageContent, + }), + }) + end) +end + +return ImagePreview \ No newline at end of file diff --git a/BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Components/Preview/ImagePreview.spec.lua b/BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Components/Preview/ImagePreview.spec.lua new file mode 100644 index 0000000000..e3555e89b5 --- /dev/null +++ b/BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Components/Preview/ImagePreview.spec.lua @@ -0,0 +1,27 @@ +return function() + local Library = script.Parent.Parent.Parent + local Roact = require(Library.Parent.Parent.Roact) + + local MockWrapper = require(Library.MockWrapper) + + local ImagePreview = require(Library.Components.Preview.ImagePreview) + + local function createTestAsset(container, name) + local image = "rbxasset://textures/AnimationEditor/animation_editor_blue.png" + local element = Roact.createElement(MockWrapper, {}, { + Roact.createElement(ImagePreview, { + ImageContent = image, + TextContent = "ImagePreviewTest", + Position = UDim2.new(1, 0, 1, 0), + Size = UDim2.new(0, 100, 0, 20) + }) + }) + + return Roact.mount(element, container or nil, name or "") + end + + it("should create and destroy without errors", function() + local instance = createTestAsset() + Roact.unmount(instance) + end) +end \ No newline at end of file diff --git a/BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Components/Preview/InstanceTreeViewItem.lua b/BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Components/Preview/InstanceTreeViewItem.lua new file mode 100644 index 0000000000..d12b79dc1f --- /dev/null +++ b/BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Components/Preview/InstanceTreeViewItem.lua @@ -0,0 +1,161 @@ +--[[ + A single item displayed in a TreeView component. + + Required Props: + element = instance, The instance to display. + indent = number, The level of indentation this item appears at. + canExpand = boolean, Whether this item has children and can be expanded. + isExpanded = boolean, Whether this item is showing its children. + isSelected = boolean, Whether this item is the selected item. + rowIndex = number, The order in which this item appears in the list. + toggleSelected = callback, A callback when this item is clicked. +]] +local FFlagAssetManagerLuaCleanup1 = settings():GetFFlag("AssetManagerLuaCleanup1") + +local Library = script.Parent.Parent.Parent + +local Roact = require(Library.Parent.Parent.Roact) + +local GetClassIcon = require(Library.Utils.GetClassIcon) +local TooltipWrapper = require(Library.Components.Tooltip) + +local Theming = require(Library.Theming) +local withTheme = Theming.withTheme + +local ELEMENT_PADDING = 5 + +local TreeViewItem = Roact.PureComponent:extend("TreeViewItem") + +function TreeViewItem:init() + self.state = { + Hovering = false, + } + + self.mouseEnter = function() + self:setState({ + Hovering = true, + }) + end + + self.mouseLeave = function() + self:setState({ + Hovering = false, + }) + end + + self.onClick = function() + self.props.toggleSelected() + end +end + +function TreeViewItem:render(props) + return withTheme(function(theme) + local treeViewTheme = theme.instanceTreeView + local instance = self.props.element + local name = instance.Name + local iconInfo = GetClassIcon(instance) + if FFlagAssetManagerLuaCleanup1 then + if typeof(instance) == "table" and instance.Icon then + iconInfo = instance.Icon + end + end + + local indent = self.props.indent + local expandable = self.props.canExpand + local expanded = self.props.isExpanded + local selected = self.props.isSelected + local layoutOrder = self.props.rowIndex or 1 + local height = treeViewTheme.treeItemHeight + local hover = self.state.Hovering + + local selectionOffset = height + + local labelOffset = selectionOffset + ELEMENT_PADDING + + (iconInfo and (height + treeViewTheme.treeViewIndent) or 0) + + return Roact.createElement("Frame", { + Size = UDim2.new(1, 0, 0, height), + BackgroundTransparency = 1, + LayoutOrder = layoutOrder, + }, { + Padding = Roact.createElement("UIPadding", { + PaddingLeft = UDim.new(0, indent * treeViewTheme.treeViewIndent), + }), + + Layout = Roact.createElement("UIListLayout", { + SortOrder = Enum.SortOrder.LayoutOrder, + Padding = UDim.new(0, ELEMENT_PADDING), + FillDirection = Enum.FillDirection.Horizontal, + }), + + Expand = Roact.createElement("ImageButton", { + LayoutOrder = 0, + Size = UDim2.new(0, height, 0, height), + BackgroundTransparency = 1, + ImageTransparency = 1, + + [Roact.Event.Activated] = self.props.toggleExpanded, + }, { + ExpandIcon = expandable and Roact.createElement("ImageLabel", { + BackgroundTransparency = 1, + ScaleType = Enum.ScaleType.Fit, + Size = UDim2.new(0, 9, 0, 9), + AnchorPoint = Vector2.new(0.5, 0.5), + Position = UDim2.new(0.5, 0, 0.5, 1), + ImageTransparency = expandable and 0 or 1, + Image = expanded and treeViewTheme.arrowExpanded or treeViewTheme.arrowCollapsed, + ImageColor3 = treeViewTheme.arrowColor, + }), + }), + + Icon = iconInfo and Roact.createElement("ImageLabel", { + ZIndex = 2, + LayoutOrder = 1, + Size = UDim2.new(0, height, 0, height), + BackgroundTransparency = 1, + Image = iconInfo.Image, + ImageRectSize = iconInfo.ImageRectSize, + ImageRectOffset = iconInfo.ImageRectOffset, + }), + + Name = Roact.createElement("TextLabel", { + ZIndex = 2, + LayoutOrder = 2, + BackgroundTransparency = 1, + Size = UDim2.new(1, -labelOffset, 0, height), + Font = treeViewTheme.font, + TextXAlignment = Enum.TextXAlignment.Left, + TextSize = treeViewTheme.textSize, + TextTruncate = Enum.TextTruncate.AtEnd, + Text = name, + TextColor3 = selected and treeViewTheme.selectedText or treeViewTheme.textColor, + BorderSizePixel = 0, + }, { + Tooltip = Roact.createElement(TooltipWrapper, { + Text = name, + Enabled = hover, + ShowDelay = treeViewTheme.tooltipShowDelay, + }), + }), + + -- We have to create a Folder so that the hover is not affected by the UIListLayout. + HoverFolder = Roact.createElement("Folder", {}, { + Hover = Roact.createElement("ImageButton", { + Size = UDim2.new(1, -selectionOffset, 1, 4), + Position = UDim2.new(0, selectionOffset, 0.5, 0), + AnchorPoint = Vector2.new(0, 0.5), + BackgroundTransparency = (hover or selected) and 0 or 1, + BackgroundColor3 = selected and treeViewTheme.selected or treeViewTheme.hover, + BorderSizePixel = 0, + AutoButtonColor = false, + + [Roact.Event.MouseEnter] = self.mouseEnter, + [Roact.Event.MouseLeave] = self.mouseLeave, + [Roact.Event.Activated] = self.onClick, + }), + }), + }) + end) +end + +return TreeViewItem \ No newline at end of file diff --git a/BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Components/Preview/InstanceTreeViewItem.spec.lua b/BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Components/Preview/InstanceTreeViewItem.spec.lua new file mode 100644 index 0000000000..e835de7900 --- /dev/null +++ b/BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Components/Preview/InstanceTreeViewItem.spec.lua @@ -0,0 +1,44 @@ +return function() + local Library = script.Parent.Parent.Parent + local Roact = require(Library.Parent.Parent.Roact) + + local MockWrapper = require(Library.MockWrapper) + + local TreeViewItem = require(Library.Components.Preview.InstanceTreeViewItem) + + it("should create and destroy without errors", function() + local element = Roact.createElement(MockWrapper, {}, { + TreeViewItem = Roact.createElement(TreeViewItem, { + element = Instance.new("Part"), + indent = 0, + }), + }) + local instance = Roact.mount(element) + Roact.unmount(instance) + end) + + it("should render correctly", function() + local element = Roact.createElement(MockWrapper, {}, { + TreeViewItem = Roact.createElement(TreeViewItem, { + element = Instance.new("Part"), + indent = 0, + canExpand = true, + }), + }) + local container = Instance.new("Folder") + local instance = Roact.mount(element, container, "TreeViewItem") + + local treeViewItem = container.TreeViewItem + expect(treeViewItem).to.be.ok() + expect(treeViewItem.Padding).to.be.ok() + expect(treeViewItem.Layout).to.be.ok() + expect(treeViewItem.Expand).to.be.ok() + expect(treeViewItem.Expand.ExpandIcon).to.be.ok() + expect(treeViewItem.Icon).to.be.ok() + expect(treeViewItem.Name).to.be.ok() + expect(treeViewItem.HoverFolder).to.be.ok() + expect(treeViewItem.HoverFolder.Hover).to.be.ok() + + Roact.unmount(instance) + end) +end diff --git a/BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Components/Preview/MediaControl.lua b/BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Components/Preview/MediaControl.lua new file mode 100644 index 0000000000..4482705399 --- /dev/null +++ b/BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Components/Preview/MediaControl.lua @@ -0,0 +1,128 @@ +--[[ + The control panel for Sounds and VideoFrames that provides a + play/pause button, progress bar, and a time label to show time left. + + Required Props: + boolean IsPlaying: Whether or not the Sound or VideoFrame is currently playing. + boolean IsLoaded: Whether or not the Sound or VideoFrame is loaded. + callback OnPause: Called when clicking the pause button. + callback OnPlay: Called when first clicking the play button. + boolean ShowTreeView: used to control the position of the play/pause button. + number TimeLength: The total Sound/VideoFrame length. + number TimePassed: How much time has passed since playing the Sound/VideoFrame. + + Optional Props: + Vector2 AnchorPoint: The AnchorPoint of the component + UDim2 LayoutOrder: The LayoutOrder of the component + UDim2 Position: The Position of the component +]] +local FFlagHideOneChildTreeviewButton = game:GetFastFlag("HideOneChildTreeviewButton") + +local Library = script.Parent.Parent.Parent +local Roact = require(Library.Parent.Parent.Roact) + +local Theming = require(Library.Theming) +local withTheme = Theming.withTheme + +local getTimeString = require(Library.Utils.getTimeString) +local RoundButton = require(Library.Components.RoundFrame) + +local LoadingIndicator = require(Library.Components.LoadingIndicator) + +local TIME_LABEL_HEIGHT = 15 +local BUTTON_SIZE = 28 +local AUDIO_CONTROL_HEIGHT = 35 +local AUDIO_CONTROL_WIDTH_OFFSET_WITH_TREE = 50 +local AUDIO_CONTROL_WIDTH_OFFSET_NO_TREE = 70 + +if FFlagHideOneChildTreeviewButton then + AUDIO_CONTROL_WIDTH_OFFSET_NO_TREE = 10 +end + +local MediaControl = Roact.PureComponent:extend("MediaControl") + +MediaControl.defaultProps = { + TimePassed = 0, +} + +function MediaControl:init() + self.onActivated = function() + if not self.props.IsLoaded then + return + end + + if self.props.IsPlaying then + self.props.OnPause() + else + self.props.OnPlay() + end + end +end + +function MediaControl:render() + return withTheme(function(theme) + local audioPreviewTheme = theme.assetPreview.audioPreview + local props = self.props + + local anchorPoint = props.AnchorPoint + local layoutOrder = props.LayoutOrder + local isLoaded = props.IsLoaded + local isPlaying = props.IsPlaying + local position = props.Position + local showTreeView = props.ShowTreeView + local timeLength = props.TimeLength + local timePassed = props.TimePassed + + local controlOffset = showTreeView and AUDIO_CONTROL_WIDTH_OFFSET_WITH_TREE or AUDIO_CONTROL_WIDTH_OFFSET_NO_TREE + local timeString = getTimeString(timePassed) .. '/' .. getTimeString(timeLength) + + return Roact.createElement("Frame", { + AnchorPoint = anchorPoint, + BackgroundTransparency = 1, + LayoutOrder = layoutOrder, + Position = position, + Size = UDim2.new(1, 0, 0, AUDIO_CONTROL_HEIGHT), + }, { + Button = Roact.createElement(RoundButton, { + AnchorPoint = Vector2.new(0.5, 0), + AutoButtonColor = false, + BackgroundColor3 = isLoaded and audioPreviewTheme.buttonBackgroundColor or audioPreviewTheme.buttonDisabledBackgroundColor, + BackgroundTransparency = isLoaded and 0 or audioPreviewTheme.buttonDisabledBackgroundTransparency, + BorderSizePixel = 0, + OnActivated = self.onActivated, + Position = UDim2.new(0, 24, 0, 0), + Size = UDim2.new(0, BUTTON_SIZE, 0, BUTTON_SIZE), + }, { + PlayOrPauseIcon = Roact.createElement("ImageLabel", { + AnchorPoint = Vector2.new(0.5, 0.5), + BackgroundTransparency = 1, + Image = isPlaying and audioPreviewTheme.pauseButton or audioPreviewTheme.playButton, + ImageColor3 = audioPreviewTheme.buttonColor, + ImageTransparency = isLoaded and 0 or audioPreviewTheme.buttonDisabledBackgroundTransparency, + Position = UDim2.new(0.5, 0, 0.5, 0), + Size = UDim2.new(1, 0, 1, 0), + }), + }), + + TimeComponent = isLoaded and Roact.createElement("TextLabel", { + AnchorPoint = Vector2.new(1, 0.5), + BackgroundTransparency = 1, + Font = audioPreviewTheme.font, + Position = UDim2.new(1, -controlOffset, 0.5, 0), + Size = UDim2.new(0, 204, 0, TIME_LABEL_HEIGHT), + Text = timeString, + TextColor3 = audioPreviewTheme.textColor, + TextSize = audioPreviewTheme.fontSize, + TextXAlignment = Enum.TextXAlignment.Right, + }), + + LoadingIndicator = (not isLoaded) and Roact.createElement(LoadingIndicator, { + AnchorPoint = Vector2.new(1, 0.5), + Position = UDim2.new(1, -controlOffset, 0.5, 0), + Size = UDim2.new(0, 50, 0, TIME_LABEL_HEIGHT), + }), + }) + end) +end + +return MediaControl \ No newline at end of file diff --git a/BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Components/Preview/MediaControl.spec.lua b/BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Components/Preview/MediaControl.spec.lua new file mode 100644 index 0000000000..3ff5a182f1 --- /dev/null +++ b/BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Components/Preview/MediaControl.spec.lua @@ -0,0 +1,34 @@ +return function() + local Library = script.Parent.Parent.Parent + local Roact = require(Library.Parent.Parent.Roact) + + local MockWrapper = require(Library.MockWrapper) + + local MediaControl = require(Library.Components.Preview.MediaControl) + + local function createTestAsset(container, name) + local emptyFunc = function() end + + local element = Roact.createElement(MockWrapper, {}, { + Roact.createElement(MediaControl, { + AnchorPoint = Vector2.new(0, 0), + IsPlaying = false, + IsLoaded = true, + LayoutOrder = 1, + OnPause = emptyFunc, + OnPlay = emptyFunc, + Position = UDim2.new(0, 0, 0, 0), + ShowTreeView = false, + TimeLength = 1, + TimePassed = 0, + }) + }) + + return Roact.mount(element, container or nil, name or "") + end + + it("should create and destroy without errors", function() + local instance = createTestAsset() + Roact.unmount(instance) + end) +end \ No newline at end of file diff --git a/BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Components/Preview/MediaProgressBar.lua b/BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Components/Preview/MediaProgressBar.lua new file mode 100644 index 0000000000..e7abdeafa7 --- /dev/null +++ b/BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Components/Preview/MediaProgressBar.lua @@ -0,0 +1,179 @@ +--[[ + A one-knob slider + TODO: DEVTOOLS-4330 - Replace this entire component with DevFramework's one-knob slider once assetPreview is out of UILibrary + + Required Props: + number Min: Min value of the slider + number Max: Max value of the slider + number CurrentValue: Current value for the lower range handle + callback OnValuesChanged: A callback that takes in params: minValue, maxValue. The callback is called whenever the min or max value changes. + + Optional Props: + Vector2 AnchorPoint: The anchorPoint of the component + boolean Disabled: Whether to render in the enabled/disabled state + number LayoutOrder: The layoutOrder of the component + UDim2 Position: The position of the component + number SnapIncrement: Incremental points that the slider's knob will snap to. A "0" snap increment means no snapping. + number VerticalDragTolerance: A vertical pixel height for allowing a pressed mouse to drag knobs on outside the component's size. + +]] +local Library = script.Parent.Parent.Parent +local Roact = require(Library.Parent.Parent.Roact) + +local Theming = require(Library.Theming) +local withTheme = Theming.withTheme + +local PROGRESS_BAR_HEIGHT = 6 +local knobSize = Vector2.new(15, 15) + +local MediaProgressBar = Roact.PureComponent:extend("MediaProgressBar") + +local function isUserInputTypeClick(inputType) + return (inputType == Enum.UserInputType.Touch) or (inputType == Enum.UserInputType.MouseButton1) +end + +MediaProgressBar.defaultProps = { + Disabled = false, + Size = UDim2.new(1, 0, 1, 0), + SnapIncrement = 0, + VerticalDragTolerance = 300, +} + +function MediaProgressBar:init() + self.sliderFrameRef = Roact.createRef() + + self.state = { + pressed = false + } + + self.getTotalRange = function() + return self.props.Max - self.props.Min + end + + self.getSnappedValue = function(value) + local snapIncrement = self.props.SnapIncrement + local min = self.props.Min + local max = self.props.Max + + if snapIncrement > 0 then + local prevSnap = math.max(snapIncrement * math.floor(value / snapIncrement), min) + local nextSnap = math.min(prevSnap + snapIncrement, max) + return math.abs(prevSnap-value) < math.abs(nextSnap-value) and prevSnap or nextSnap + end + + return math.clamp(value, min, max) + end + + self.getMouseClickValue = function(input) + local sliderFrameRef = self.sliderFrameRef.current + local inputHorizontalOffsetNormalized = (input.Position.X - sliderFrameRef.AbsolutePosition.X) / sliderFrameRef.AbsoluteSize.X + inputHorizontalOffsetNormalized = math.clamp(inputHorizontalOffsetNormalized, 0, 1) + local valueBeforeSnapping = self.props.Min + (inputHorizontalOffsetNormalized * self.getTotalRange()) + + return self.getSnappedValue(valueBeforeSnapping) + end + + self.setValuesFromInput = function(input) + local mouseClickValue = self.getMouseClickValue(input) + local clampedValue = math.clamp(mouseClickValue, self.props.Min, self.props.Max) + + self.props.OnValuesChanged(clampedValue) + end + + self.onInputBegan = function(rbx, input) + if self.props.Disabled then + return + + elseif isUserInputTypeClick(input.UserInputType) then + self:setState({ + pressed = true, + }) + self.setValuesFromInput(input) + end + end + + self.onInputChanged = function(rbx, input) + if self.props.Disabled then + return + + elseif self.state.pressed and input.UserInputType == Enum.UserInputType.MouseMovement then + self.setValuesFromInput(input) + end + end + + self.onInputEnded = function(rbx, input) + if not self.props.Disabled and isUserInputTypeClick(input.UserInputType) then + self.props.OnInputEnded() + self:setState({ + pressed = false, + }) + end + end +end + +function MediaProgressBar:render() + return withTheme(function(theme) + local audioPreviewTheme = theme.assetPreview.audioPreview + + local anchorPoint = self.props.AnchorPoint + local isDisabled = self.props.Disabled + local currentValue = self.props.CurrentValue + local layoutOrder = self.props.LayoutOrder + local min = self.props.Min + local position = self.props.Position + local verticalDragBuffer = self.props.VerticalDragTolerance + + local lowerFillPercent = (currentValue - min) / self.getTotalRange() + + return Roact.createElement("Frame", { + AnchorPoint = anchorPoint, + BackgroundTransparency = 1, + LayoutOrder = layoutOrder, + Position = position, + Size = UDim2.new(1, 0, 0, knobSize.X), + + [Roact.Ref] = self.sliderFrameRef, + }, { + ProgressBarBackground = Roact.createElement("Frame", { + AnchorPoint = Vector2.new(0, 0.5), + BackgroundColor3 = audioPreviewTheme.progressBar_BG_Color, + BackgroundTransparency = 0, + BorderSizePixel = 0, + Position = UDim2.new(0, 0, 0.5, 0), + Size = UDim2.new(1, 0, 0, PROGRESS_BAR_HEIGHT), + }, { + ProgressBarForeground = Roact.createElement("Frame", { + BackgroundColor3 = audioPreviewTheme.progressBar, + BackgroundTransparency = 0, + BorderSizePixel = 0, + Size = UDim2.new(lowerFillPercent, 0, 1, 0), + }), + }), + + Knob = Roact.createElement("ImageButton", { + AutoButtonColor = false, + AnchorPoint = Vector2.new(0, 0.5), + BackgroundTransparency = 1, + Image = audioPreviewTheme.progressKnob, + ImageColor3 = audioPreviewTheme.progressKnobColor, + Position = UDim2.new(lowerFillPercent, 0, 0.5, 0), + Size = UDim2.new(0, knobSize.X, 0, knobSize.Y), + ZIndex = 3, + }), + + ClickHandler = (not isDisabled) and Roact.createElement("ImageButton", { + AnchorPoint = Vector2.new(0.5, 0.5), + BackgroundTransparency = 1, + Position = UDim2.new(0.5, 0, 0.5, 0), + Size = UDim2.new(1, knobSize.X, 1, self.state.pressed and verticalDragBuffer or 0), + ZIndex = 4, + + [Roact.Event.InputBegan] = self.onInputBegan, + [Roact.Event.InputChanged] = self.onInputChanged, + [Roact.Event.InputEnded] = self.onInputEnded, + }), + }) + end) +end + +return MediaProgressBar \ No newline at end of file diff --git a/BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Components/Preview/MediaProgressBar.spec.lua b/BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Components/Preview/MediaProgressBar.spec.lua new file mode 100644 index 0000000000..d83b361d03 --- /dev/null +++ b/BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Components/Preview/MediaProgressBar.spec.lua @@ -0,0 +1,30 @@ +return function() + local Library = script.Parent.Parent.Parent + local Roact = require(Library.Parent.Parent.Roact) + + local MockWrapper = require(Library.MockWrapper) + + local MediaProgressBar = require(Library.Components.Preview.MediaProgressBar) + + local function createTestAsset(container, name) + local emptyFunc = function() end + + local element = Roact.createElement(MockWrapper, {}, { + Roact.createElement(MediaProgressBar, { + CurrentValue = 0, + LayoutOrder = 2, + Min = 0, + Max = 1, + OnValuesChanged = emptyFunc, + OnInputEnded = emptyFunc, + }) + }) + + return Roact.mount(element, container or nil, name or "") + end + + it("should create and destroy without errors", function() + local instance = createTestAsset() + Roact.unmount(instance) + end) +end \ No newline at end of file diff --git a/BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Components/Preview/ModelPreview.lua b/BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Components/Preview/ModelPreview.lua new file mode 100644 index 0000000000..8073f9373f --- /dev/null +++ b/BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Components/Preview/ModelPreview.lua @@ -0,0 +1,216 @@ +--[[ + This component handles the model rendering and input support for model preview. + + Necessary properties: + Position = UDim2 + Size = UDim2 + TargetModel = Model, the model is only for previewing. So, it contains the assetInstance with all he + scripts being disabled. + + Optional properties: + OnModelPreviewFrameEntered = callBack, we use those function to make sure input will be captured if + mouse is within the area of the frame. + OnModelPreviewFrameLeft = callBack +]] + +local Library = script.Parent.Parent.Parent + +local Roact = require(Library.Parent.Parent.Roact) + +local getCamera = require(Library.Camera).getCamera + +local Theming = require(Library.Theming) +local withTheme = Theming.withTheme + +local ModelPreview = Roact.PureComponent:extend("ModelPreview") + +-- Used for the inital camera position +local INSERT_CAMERA_DIST_MULT = 0.8 +local PAN_CAMERA_DIST_MULT = 0.1 +local DOUBLE_CLICK_TIME = 0.25 + +function ModelPreview:init() + self.orbitDrag = false + self.panDrag = false + self.doubleClickTimestamp = tick() + + -- Need reference to ViewportFrame so I can set the preview model to it. + self.VFRef = Roact.createRef() + + self.modelPreviewCamera = getCamera(self) + + -- This is the model that will be displayed on the ViewportFrame. + self.VFModel = nil + + self.onInputBegan = function(_, input) + if input.UserInputType == Enum.UserInputType.MouseButton1 then + self.orbitDrag = true + end + + if input.UserInputType == Enum.UserInputType.MouseButton3 or + input.UserInputType == Enum.UserInputType.MouseButton2 then + self.panDrag = true + end + end + + self.onInputChanged = function(_, input) + if input.UserInputType == Enum.UserInputType.MouseMovement + and self.orbitDrag or self.panDrag then + local camera = self.modelPreviewCamera + local targetFocus = camera.Focus + local targetCF = targetFocus:ToObjectSpace(camera.CFrame) + + if self.orbitDrag then + targetCF = CFrame.fromAxisAngle(targetCF.RightVector, input.Delta.y * -0.01) * targetCF + targetCF = CFrame.fromAxisAngle(Vector3.new(0, 1, 0), input.Delta.x * -0.01) * targetCF + elseif self.panDrag then + local dist = (targetCF.p - targetFocus.p).magnitude + dist = dist ~= dist and 0 or dist -- NaN check + local distanceFactor = PAN_CAMERA_DIST_MULT * (dist * 0.1) + local yOffset = targetCF.upVector.Unit * input.Delta.y * distanceFactor + local xOffset = -targetCF.rightVector.Unit * input.Delta.x * distanceFactor + targetCF = targetCF + yOffset + xOffset + targetFocus = targetFocus + yOffset + xOffset + end + + camera.CFrame = camera.Focus:ToWorldSpace(targetCF) + camera.Focus = targetFocus + end + end + + self.onInputEnded = function(_, input) + if input.UserInputType == Enum.UserInputType.MouseButton1 then + self.orbitDrag = false + if tick() < self.doubleClickTimestamp + DOUBLE_CLICK_TIME then + self.centerCamera() + end + self.doubleClickTimestamp = tick() + end + + if input.UserInputType == Enum.UserInputType.MouseButton3 or + input.UserInputType == Enum.UserInputType.MouseButton2 then + self.panDrag = false + end + end + + self.zoomCamera = function(zoomIn) + local zoomFactor = zoomIn and -1 or 1 + local camera = self.modelPreviewCamera + local current = camera.CFrame + local focus = camera.Focus + + local dist = (current.p - focus.p).magnitude + dist = dist ~= dist and 0 or dist -- NaN check + + local moveAmount = math.max(dist * 0.1, 0.1) + local targetCF = current * CFrame.new(0, 0, zoomFactor * moveAmount) + + camera.CFrame = targetCF + end + + self.onMouseWheelBackward = function() + self.zoomCamera(false) + end + + self.onMouseWheelForward = function() + self.zoomCamera(true) + end + + self.centerCamera = function() + local currentPreviewCopy = self.VFModel + local camera = self.modelPreviewCamera + + -- Move the model/part in front of the camera + local success, modelCf, size = pcall(function() return currentPreviewCopy:GetBoundingBox() end) + if not success then + size = currentPreviewCopy.Size + currentPreviewCopy.CFrame = currentPreviewCopy.CFrame - currentPreviewCopy.CFrame.p + else + currentPreviewCopy:TranslateBy(-modelCf.p) + end + + local cameraDistAway = size.magnitude * INSERT_CAMERA_DIST_MULT + local dir = Vector3.new(1, 1, 1).unit + camera.Focus = CFrame.new() + camera.CFrame = CFrame.new(cameraDistAway * dir, camera.Focus.p) + end + + -- Because we are using refs and not using state, this component will not + -- re-render unless we are viewing a different model. Every call to this + -- function assumes that a different model has been selected. + self.tryRenderModel = function() + local myVRFrame = self.VFRef.current + local currentPreviewCopy = self.VFModel + myVRFrame:ClearAllChildren() + currentPreviewCopy.Parent = myVRFrame + + self.centerCamera() + end +end + +function ModelPreview:makeViewportModel() + local currentPreview = self.props.TargetModel + if currentPreview:IsA("Model") or currentPreview:IsA("BasePart") then + self.VFModel = currentPreview:Clone() + else + self.VFModel = Instance.new("Model") + currentPreview:Clone().Parent = self.VFModel + end +end + +function ModelPreview:didMount() + self:makeViewportModel() + self.tryRenderModel() +end + +function ModelPreview:didUpdate() + self.tryRenderModel() +end + +function ModelPreview:willUnmount() + if self.VFModel then + self.VFModel:Destroy() + end +end + +function ModelPreview:render() + return withTheme(function(theme) + local props = self.props + local position = props.Position + local size = props.Size or UDim2.new(1, 0, 1, 0) + local ModelPreviewCamera = self.modelPreviewCamera + + local OnModelPreviewFrameEntered = props.OnModelPreviewFrameEntered + local OnModelPreviewFrameLeft = props.OnModelPreviewFrameLeft + + local layoutOrder = props.LayoutOrder + + self:makeViewportModel() + + -- The element we return is determined by object we receive. + return Roact.createElement("ViewportFrame", { + Position = position, -- We should avoid using relative position and size. + Size = size, + + BorderSizePixel = 0, + BackgroundTransparency = 0, + BackgroundColor3 = theme.assetPreview.modelPreview.background, + + CurrentCamera = ModelPreviewCamera, + [Roact.Ref] = self.VFRef, + + [Roact.Event.MouseEnter] = OnModelPreviewFrameEntered, + [Roact.Event.MouseLeave] = OnModelPreviewFrameLeft, + [Roact.Event.MouseWheelForward] = self.onMouseWheelForward, + [Roact.Event.MouseWheelBackward] = self.onMouseWheelBackward, + [Roact.Event.InputBegan] = self.onInputBegan, + [Roact.Event.InputEnded] = self.onInputEnded, + [Roact.Event.InputChanged] = self.onInputChanged, + [Roact.Event.TouchPinch] = self.onTouchPinch, + + LayoutOrder = layoutOrder, + }) + end) +end + +return ModelPreview diff --git a/BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Components/Preview/ModelPreview.spec.lua b/BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Components/Preview/ModelPreview.spec.lua new file mode 100644 index 0000000000..6a0d95fe7d --- /dev/null +++ b/BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Components/Preview/ModelPreview.spec.lua @@ -0,0 +1,28 @@ +return function() + local Library = script.Parent.Parent.Parent + local Roact = require(Library.Parent.Parent.Roact) + + local MockWrapper = require(Library.MockWrapper) + + local ModelPreview = require(Library.Components.Preview.ModelPreview) + + local function createTestAsset(container, name) + local testModel = Instance.new("Model") + + local element = Roact.createElement(MockWrapper, {}, { + ModelPreview = Roact.createElement(ModelPreview, { + TargetModel = testModel, + + Position = UDim2.new(1, 0, 1, 0), + Size = UDim2.new(1, 0, 1, 0), + }), + }) + + return Roact.mount(element, container or nil, name or "") + end + + it("should create and destroy without errors", function() + local instance = createTestAsset() + Roact.unmount(instance) + end) +end \ No newline at end of file diff --git a/BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Components/Preview/PreviewController.lua b/BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Components/Preview/PreviewController.lua new file mode 100644 index 0000000000..1c2ac3c7ed --- /dev/null +++ b/BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Components/Preview/PreviewController.lua @@ -0,0 +1,350 @@ +--!nolint DeprecatedGlobal +--^ enables migration with FFlagPreviewControllerUseOsClock; remove with that flag. + +--[[ + This component is used to render both mainPreview and TreeView. + MainView can be modelPreview, soundPreview, scriptPreview, imagePreview, otherPreview and audioPlay. + Note soundPreview and audioPlay are different here. Sound preview is uesed to preview sound object comes with + the model, audioPlay is the audioPlayer we made for the audio asset. Which actually supports play, pause. + + Required Props: + Width = number, + + CurrentPreview = Instance, this is the instance that is currently displayed in the preview. + PreviewModel = Instance, this is the top level asset that will be displayed in the InstanceTreeView + AssetPreviewType = AssetType.TYPES, custom category that will inform which preview will be displayed. + AssetId = number, + PutTreeViewOnBottom = boolean, this determines whether the TreeView will be displayed on the right or bottom. + + OnTreeItemClicked = callback, which sets the preview to show the new Tree Item, the callback takes 1 parameter of type instance. + OnModelPreviewFrameEntered = callback, this is a callback that disables the scrollbar + OnModelPreviewFrameLeft = callback, this is a callback that re-enables teh scollbar + + LayoutOrder = number, +]] +local FFlagStudioMinorFixesForAssetPreview = settings():GetFFlag("StudioMinorFixesForAssetPreview") +local FFlagHideOneChildTreeviewButton = game:GetFastFlag("HideOneChildTreeviewButton") +local FFlagStudioFixTreeViewForFlatList = settings():GetFFlag("StudioFixTreeViewForFlatList") +local FFlagStudioAssetPreviewTreeFix2 = game:DefineFastFlag("StudioAssetPreviewTreeFix2", false) +local FFlagEnableToolboxVideos = game:GetFastFlag("EnableToolboxVideos") + +local FFlagPreviewControllerUseOsClock = game:DefineFastFlag("PreviewControllerUseOsClock", false) + +local Library = script.Parent.Parent.Parent + +local Roact = require(Library.Parent.Parent.Roact) + +local ModelPreview = require(Library.Components.Preview.ModelPreview) +local ImagePreview = require(Library.Components.Preview.ImagePreview) +local ThumbnailIconPreview = require(Library.Components.Preview.ThumbnailIconPreview) +local TreeViewButton = require(Library.Components.Preview.TreeViewButton) +local AssetType = require(Library.Utils.AssetType) +local AudioPreview = require(Library.Components.Preview.AudioPreview) +local VideoPreview = require(Library.Components.Preview.VideoPreview) + +local TreeViewItem = require(Library.Components.Preview.InstanceTreeViewItem) +local TreeView = require(Library.Components.TreeView) + +local LoadingIndicator = require(Library.Components.LoadingIndicator) + +local Theming = require(Library.Theming) +local withTheme = Theming.withTheme + +local Urls = require(Library.Utils.Urls) + +local TREEVIEW_WIDTH = 242 +local PREVIEW_HEIGHT = 242 +local TREEVIEW_BOTTOM_HEIGHT = 120 +local MAINVIEW_BUTTONS_X_OFFSET = -7 +local MAINVIEW_BUTTONS_Y_OFFSET = -7 + +local MODAL_MIN_WIDTH = 235 + +local PreviewController = Roact.PureComponent:extend("PreviewController") + +local function getImage(instance) + if FFlagStudioMinorFixesForAssetPreview then + if typeof(instance) ~= "Instance" then + return nil + end + end + + if instance:IsA("Decal") or instance:IsA("Texture") then + return instance.Texture + elseif instance:IsA("Sky") then + return instance.SkyboxFt + else + return instance.Image + end +end + +local function getImageScaleType(instance) + if FFlagStudioMinorFixesForAssetPreview then + if typeof(instance) ~= "Instance" then + return Enum.ScaleType.Fit + end + end + if instance:IsA("Sky") then + return Enum.ScaleType.Crop + else + return Enum.ScaleType.Fit + end +end + +function PreviewController:createTreeView(previewModel, size) + return withTheme(function(theme) + local onTreeviewEntered = self.onTreeviewEntered + local onTreeviewLeft = self.onTreeviewLeft + + local dataTree + if FFlagStudioAssetPreviewTreeFix2 then + dataTree = previewModel + else + dataTree = FFlagStudioFixTreeViewForFlatList and self.props.CurrentPreview or previewModel + end + + return Roact.createElement("ImageButton", { + Size = size, + + BackgroundTransparency = 0, + BackgroundColor3 = theme.instanceTreeView.background, + BorderSizePixel = 0, + AutoButtonColor = false, + + [Roact.Event.MouseEnter] = onTreeviewEntered, + [Roact.Event.MouseLeave] = onTreeviewLeft, + + LayoutOrder = 2 + },{ + TreeViewFrame = Roact.createElement(TreeView, { + dataTree = dataTree, + onSelectionChanged = self.onTreeItemClicked, + + createFlatList = FFlagStudioFixTreeViewForFlatList and true or false, + + getChildren = function(instance) + return instance:GetChildren() + end, + + renderElement = function(properties) + return Roact.createElement(TreeViewItem, properties) + end, + }) + }) + end) +end + +function PreviewController:init(props) + self.state = { + -- This controls the treeview and full screen button's status + showTreeView = false, + } + + self.inModelPreview = false + self.inTreeview = false + self.totalTimeSpent = 0 + self.cacheTime = 0 + + self.onTreeviewStatusToggle = function(newStatus) + self:setState({ + showTreeView = newStatus, + }) + end + + self.onModelPreviewFrameEntered = function(...) + self.props.OnModelPreviewFrameEntered(...) + + self.onPreviewStatusChange(true, self.inTreeview) + end + + self.onModelPreviewFrameLeft = function(...) + self.props.OnModelPreviewFrameLeft(...) + + self.onPreviewStatusChange(false, self.inTreeview) + end + + self.onTreeviewEntered = function() + self.onPreviewStatusChange(self.inModelPreview, true) + end + + self.onTreeviewLeft = function() + self.onPreviewStatusChange(self.inModelPreview, false) + end + + self.onPreviewStatusChange = function(newModelStatus, newTreeStatus) + if (not self.inModelPreview and not self.inTreeview) and + (newModelStatus or newTreeStatus) then + -- Time to start the timer + self.cacheTime = FFlagPreviewControllerUseOsClock and os.clock() or elapsedTime() + end + + if (not newModelStatus and not newTreeStatus) and + (self.inModelPreview or self.inTreeview) then + local currentTime = FFlagPreviewControllerUseOsClock and os.clock() or elapsedTime() + local newTimeSpent = currentTime - self.cacheTime + if newTimeSpent > 0 then + self.totalTimeSpent = self.totalTimeSpent + math.floor(newTimeSpent * 1000) + end + end + + self.inModelPreview = newModelStatus + self.inTreeview = newTreeStatus + end + + self.onTreeItemClicked = function(instances) + if instances[1] then + self.props.OnTreeItemClicked(instances[1]) + end + end +end + +function PreviewController:render() + local props = self.props + local state = self.state + + local currentPreview = props.CurrentPreview + local previewModel = props.PreviewModel + local assetPreviewType = props.AssetPreviewType + local assetId = props.AssetId + local putTreeviewOnBottom = props.PutTreeviewOnBottom + local width = props.Width + local layoutOrder = props.LayoutOrder + + local isShowVideoPreview = FFlagEnableToolboxVideos and AssetType:isVideo(assetPreviewType) + local videoId + if isShowVideoPreview then + videoId = currentPreview.Video + end + + local showTreeView = state.showTreeView + local previewSize + local treeViewSize + local height + if showTreeView then + height = putTreeviewOnBottom and PREVIEW_HEIGHT + TREEVIEW_BOTTOM_HEIGHT or PREVIEW_HEIGHT + previewSize = putTreeviewOnBottom and UDim2.new(1, 0, 0, PREVIEW_HEIGHT) + or UDim2.new(1, -TREEVIEW_WIDTH, 0, PREVIEW_HEIGHT) + treeViewSize = putTreeviewOnBottom and UDim2.new(1, 0, 0, TREEVIEW_BOTTOM_HEIGHT) + or UDim2.new(0, TREEVIEW_WIDTH, 0, PREVIEW_HEIGHT) + else + height = PREVIEW_HEIGHT + previewSize = UDim2.new(1, 0, 0, PREVIEW_HEIGHT) + treeViewSize = UDim2.new() + end + + local showTreeViewButton = (not AssetType:isPlugin(assetPreviewType)) + if FFlagHideOneChildTreeviewButton then + local dataTree + if FFlagStudioAssetPreviewTreeFix2 then + dataTree = previewModel + else + dataTree = FFlagStudioFixTreeViewForFlatList and self.props.CurrentPreview or previewModel + end + local hasMultiplechildren = dataTree and (#dataTree:GetChildren() > 0) or false + showTreeViewButton = showTreeViewButton and hasMultiplechildren + end + + local onModelPreviewFrameEntered = self.onModelPreviewFrameEntered + local onModelPreviewFrameLeft = self.onModelPreviewFrameLeft + + local THUMBNAIL_HEIGHT = PREVIEW_HEIGHT < MODAL_MIN_WIDTH and PREVIEW_HEIGHT or MODAL_MIN_WIDTH + local showThumbnail = AssetType:isScript(assetPreviewType) or AssetType:isOtherType(assetPreviewType) + + local soundId + if AssetType:isAudio(assetPreviewType) and currentPreview then + -- It's wrong to get SoundId from currenttPreview, it should be previewModel. + soundId = currentPreview.SoundId + end + + local reportPlay = props.reportPlay + local reportPause = props.reportPause + + local isShowAudioPreview = AssetType:isAudio(assetPreviewType) + local mainViewButtonYOffset + if isShowAudioPreview then + mainViewButtonYOffset = 3 + else + mainViewButtonYOffset = MAINVIEW_BUTTONS_Y_OFFSET + end + + return Roact.createElement("Frame", { + Size = UDim2.new(1, width, 0, height), + + BackgroundTransparency = 1, + BorderSizePixel = 0, + + LayoutOrder = layoutOrder, + }, { + UIListLayout = Roact.createElement("UIListLayout", { + FillDirection = putTreeviewOnBottom and Enum.FillDirection.Vertical or Enum.FillDirection.Horizontal, + HorizontalAlignment = Enum.HorizontalAlignment.Left, + VerticalAlignment = Enum.VerticalAlignment.Top, + SortOrder = Enum.SortOrder.LayoutOrder, + }), + + MainView = Roact.createElement("Frame", { + Size = previewSize, + + BackgroundTransparency = 1, + BorderSizePixel = 0, + + LayoutOrder = 1, + }, { + PreviewLoading = AssetType:isLoading(assetPreviewType) and Roact.createElement(LoadingIndicator,{ + AnchorPoint = Vector2.new(0.5, 0.5), + Position = UDim2.new(0.5, 0, 0.5, 0), + }), + + ModelPreview = AssetType:isModel(assetPreviewType) and Roact.createElement(ModelPreview, { + TargetModel = currentPreview, + + OnModelPreviewFrameEntered = onModelPreviewFrameEntered, + OnModelPreviewFrameLeft = onModelPreviewFrameLeft, + }), + + ImagePreview = AssetType:isImage(assetPreviewType) and Roact.createElement(ImagePreview, { + ImageContent = getImage(currentPreview), + ScaleType = getImageScaleType(currentPreview), + }), + + AudioPreview = isShowAudioPreview and Roact.createElement(AudioPreview, { + SoundId = soundId or Urls.constructAssetIdString(assetId), + AssetId = assetId, + ShowTreeView = showTreeView, + ReportPlay = reportPlay, + ReportPause = reportPause, + }), + + VideoPreview = isShowVideoPreview and Roact.createElement(VideoPreview, { + VideoId = videoId or Urls.constructAssetIdString(assetId), + ShowTreeView = showTreeView, + }), + + PluginPreview = AssetType:isPlugin(assetPreviewType) and Roact.createElement("ImageLabel", { + Image = Urls.constructAssetThumbnailUrl(assetId, 420, 420), + Size = UDim2.new(0,THUMBNAIL_HEIGHT,0,THUMBNAIL_HEIGHT), + Position = UDim2.new(0.5,0,0,0), + AnchorPoint = Vector2.new(0.5,0), + }), + + -- Let the script and other share the same component for now + ThumbnailIconPreview = showThumbnail and Roact.createElement(ThumbnailIconPreview, { + TargetInstance = currentPreview, + AssetId = assetId, + ElementName = currentPreview.Name, + }), + + TreeViewButton = showTreeViewButton and Roact.createElement(TreeViewButton, { + Position = UDim2.new(1, MAINVIEW_BUTTONS_X_OFFSET, 1, mainViewButtonYOffset), + ZIndex = 2, + + ShowTreeView = state.showTreeView, + OnTreeviewStatusToggle = self.onTreeviewStatusToggle, + }) + }), + + TreeView = showTreeView and self:createTreeView(previewModel, treeViewSize) + }) +end + +return PreviewController diff --git a/BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Components/Preview/PreviewController.spec.lua b/BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Components/Preview/PreviewController.spec.lua new file mode 100644 index 0000000000..a8d1349fde --- /dev/null +++ b/BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Components/Preview/PreviewController.spec.lua @@ -0,0 +1,36 @@ +return function() + local Library = script.Parent.Parent.Parent + local Roact = require(Library.Parent.Parent.Roact) + + local MockWrapper = require(Library.MockWrapper) + + local PreviewController = require(Library.Components.Preview.PreviewController) + + local AssetType = require(Library.Utils.AssetType) + + -- PineTree + -- rbxassetid://183435411 + local function createTestAsset(container, name) + local assetId = 183435411 + local previewModel = Instance.new("Model") + + local element = Roact.createElement(MockWrapper, {}, { + PreviewController = Roact.createElement(PreviewController, { + width = 40, + + currentPreview = previewModel, + previewModel = previewModel, + assetPreviewType = AssetType.TYPES.ModelType, + assetId = assetId, + putTreeviewOnBottom = true, + }) + }) + + return Roact.mount(element, container or nil, name or "") + end + + it("should create and destroy without errors", function() + local instance = createTestAsset() + Roact.unmount(instance) + end) +end \ No newline at end of file diff --git a/BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Components/Preview/SearchLinkText.lua b/BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Components/Preview/SearchLinkText.lua new file mode 100644 index 0000000000..5a24fb81df --- /dev/null +++ b/BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Components/Preview/SearchLinkText.lua @@ -0,0 +1,141 @@ +--[[ + A component used for a link text with an animated search icon, + that appears when the user hovers over the link text. + + Necessary Properties: + Position = UDim2, the positon of this component. + AnchorPoint = Vector2, the centering of the component relative to its parent. + + Text = string, the Creator name to be shown in the text label. + OnClick = callback, A callback for when the link is clicked. + + Optional Properties: + number TweenTime = The time in seconds to play the hover animation. +]] + +local TweenService = game:GetService("TweenService") +local DEFAULT_TWEEN_TIME = 0.2 + +local Library = script.Parent.Parent.Parent + +local Roact = require(Library.Parent.Parent.Roact) + +local GetTextSize = require(Library.Utils.GetTextSize) + +local Theming = require(Library.Theming) +local withTheme = Theming.withTheme + +local SearchLinkText = Roact.PureComponent:extend("SearchLinkText") + +function SearchLinkText:init(props) + assert(type(props.OnClick) == "function", "SearchLinkText expects an 'OnClick' function.") + self.tweenInfo = TweenInfo.new(props.TweenTime or DEFAULT_TWEEN_TIME) + + self.textRef = Roact.createRef() + self.iconRef = Roact.createRef() + self.tweens = {} + + self.mouseEnter = function() + if self.tweens.TextEnter then + self.tweens.TextEnter:Play() + end + if self.tweens.IconEnter then + self.tweens.IconEnter:Play() + end + end + + self.mouseLeave = function() + if self.tweens.TextLeave then + self.tweens.TextLeave:Play() + end + if self.tweens.IconLeave then + self.tweens.IconLeave:Play() + end + end +end + +function SearchLinkText:didMount() + local text = self.textRef:getValue() + local icon = self.iconRef:getValue() + self.tweens.TextEnter = TweenService:Create(text, self.tweenInfo, { + Position = UDim2.fromScale(0, 0.5), + AnchorPoint = Vector2.new(0, 0.5), + }) + self.tweens.TextLeave = TweenService:Create(text, self.tweenInfo, { + Position = UDim2.fromScale(1, 0.5), + AnchorPoint = Vector2.new(1, 0.5), + }) + self.tweens.IconEnter = TweenService:Create(icon, self.tweenInfo, { + ImageTransparency = 0, + AnchorPoint = Vector2.new(1, 0.5), + }) + self.tweens.IconLeave = TweenService:Create(icon, self.tweenInfo, { + ImageTransparency = 1, + AnchorPoint = Vector2.new(0, 0.5), + }) +end + +function SearchLinkText:willUnmount() + for _, tween in pairs(self.tweens) do + tween:Cancel() + tween:Destroy() + end +end + +function SearchLinkText:render() + return withTheme(function(theme) + local props = self.props + local text = props.Text + local position = props.Position + local anchorPoint = props.AnchorPoint + + local searchLinkTextTheme = theme.assetPreview.description + + local textDimensions + local textExtents = GetTextSize(text, theme.assetPreview.textSizeLarge) + textDimensions = UDim2.fromOffset(textExtents.X, textExtents.Y) + + local fullWidth = textExtents.X + searchLinkTextTheme.searchBarIconSize + + searchLinkTextTheme.padding + + return Roact.createElement("TextButton", { + Size = UDim2.new(0, fullWidth, 1, 0), + Position = position, + AnchorPoint = anchorPoint, + BackgroundTransparency = 1, + + Text = "", + [Roact.Event.Activated] = props.OnClick, + [Roact.Event.MouseEnter] = self.mouseEnter, + [Roact.Event.MouseLeave] = self.mouseLeave, + }, { + Text = Roact.createElement("TextLabel", { + Size = textDimensions, + Position = UDim2.fromScale(1, 0.5), + AnchorPoint = Vector2.new(1, 0.5), + BackgroundTransparency = 1, + + Text = text, + Font = theme.assetPreview.font, + TextColor3 = searchLinkTextTheme.rightTextColor, + TextSize = theme.assetPreview.textSizeLarge, + [Roact.Ref] = self.textRef, + }), + + SearchIcon = Roact.createElement("ImageLabel", { + Size = UDim2.new(0, searchLinkTextTheme.searchBarIconSize, + 0, searchLinkTextTheme.searchBarIconSize), + Position = UDim2.fromScale(1, 0.5), + AnchorPoint = Vector2.new(0, 0.5), + BackgroundTransparency = 1, + ImageTransparency = 1, + + ImageColor3 = searchLinkTextTheme.rightTextColor, + Image = searchLinkTextTheme.images.searchIcon, + [Roact.Ref] = self.iconRef, + }), + }) + end) +end + +return SearchLinkText diff --git a/BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Components/Preview/SearchLinkText.spec.lua b/BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Components/Preview/SearchLinkText.spec.lua new file mode 100644 index 0000000000..c257b03a3e --- /dev/null +++ b/BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Components/Preview/SearchLinkText.spec.lua @@ -0,0 +1,51 @@ +return function() + local Library = script.Parent.Parent.Parent + local Roact = require(Library.Parent.Parent.Roact) + + local MockWrapper = require(Library.MockWrapper) + + local SearchLinkText = require(Library.Components.Preview.SearchLinkText) + + it("should expect an OnClick function", function() + local element = Roact.createElement(MockWrapper, {}, { + LinkText = Roact.createElement(SearchLinkText, { + Text = "Test", + }), + }) + expect(function() + local instance = Roact.mount(element) + Roact.unmount(instance) + end).to.throw() + end) + + it("should create and destroy without errors", function() + local element = Roact.createElement(MockWrapper, {}, { + LinkText = Roact.createElement(SearchLinkText, { + Text = "Test", + OnClick = function() + end, + }), + }) + local instance = Roact.mount(element) + Roact.unmount(instance) + end) + + it("should render correctly", function() + local element = Roact.createElement(MockWrapper, {}, { + LinkText = Roact.createElement(SearchLinkText, { + Text = "Test", + OnClick = function() + end, + }), + }) + + local container = Instance.new("Folder") + local instance = Roact.mount(element, container) + + local main = container:FindFirstChildOfClass("TextButton") + expect(main.Text).to.be.ok() + expect(main.SearchIcon).to.be.ok() + + Roact.unmount(instance) + end) +end diff --git a/BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Components/Preview/ThumbnailIconPreview.lua b/BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Components/Preview/ThumbnailIconPreview.lua new file mode 100644 index 0000000000..9770397bb2 --- /dev/null +++ b/BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Components/Preview/ThumbnailIconPreview.lua @@ -0,0 +1,91 @@ +--[[ + This component is the default shown for the 3D view in asset preview. This component shows the class + icon for that instance and the name of the element. + + Necessary properties: + Postion = UDim2 + Size = UDim2, this property determines the size of the preview. + ElementName = String, the name of the asset, this will be displayed below the icon. + TargetInstance = The instance to preview. + + Optional properties: + IconSize = number, will default to 16 unless otherwise specified, + this affects the dimensions of the icon representing the asset. + TextLabelHeight = number +]] + +local Library = script.Parent.Parent.Parent + +local Roact = require(Library.Parent.Parent.Roact) + +local Theming = require(Library.Theming) +local withTheme = Theming.withTheme + +local GetClassIcon = require(Library.Utils.GetClassIcon) + +local ThumbnailIconPreview = Roact.PureComponent:extend("ThumbnailIconPreview") + +function ThumbnailIconPreview:render() + return withTheme(function(theme) + local props = self.props + + local elementName = props.ElementName or "" + local instance = props.TargetInstance + local iconInfo = GetClassIcon(instance) + + local position = props.Position + local size = props.Size or UDim2.new(1, 0, 1, 0) + local thumbnailIconPreviewTheme = theme.assetPreview.thumbnailIconPreview + local iconSize = props.IconSize or thumbnailIconPreviewTheme.iconSize + local padding = thumbnailIconPreviewTheme.textLabelPadding + local textLabelHeight = props.TextLabelHeight or thumbnailIconPreviewTheme.defaultTextLabelHeight + + local layoutOrder = props.LayoutOrder + + return Roact.createElement("Frame", { + Position = position, + Size = size, + + BackgroundTransparency = 0, + BackgroundColor3 = thumbnailIconPreviewTheme.background, + BorderSizePixel = 0, + + LayoutOrder = layoutOrder, + },{ + UIListLayout = Roact.createElement("UIListLayout", { + SortOrder = Enum.SortOrder.LayoutOrder, + FillDirection = Enum.FillDirection.Vertical, + HorizontalAlignment = Enum.HorizontalAlignment.Center, + VerticalAlignment = Enum.VerticalAlignment.Center, + }), + + ImageContent = Roact.createElement("ImageLabel", { + Size = UDim2.new(0, iconSize, 0, iconSize), + + BackgroundTransparency = 1, + + Image = iconInfo.Image, + ImageRectSize = iconInfo.ImageRectSize, + ImageRectOffset = iconInfo.ImageRectOffset, + LayoutOrder = 1, + }), + + TextContent = Roact.createElement("TextLabel", { + Size = UDim2.new(1, -2 * padding, 0, textLabelHeight), + + Text = tostring(elementName), + TextColor3 = thumbnailIconPreviewTheme.textColor, + Font = theme.assetPreview.font, + TextSize = theme.assetPreview.textSize, + TextXAlignment = Enum.TextXAlignment.Center, + + BackgroundTransparency = 1, + LayoutOrder = 2, + }) + }) + end) +end + +return ThumbnailIconPreview + + diff --git a/BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Components/Preview/ThumbnailIconPreview.spec.lua b/BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Components/Preview/ThumbnailIconPreview.spec.lua new file mode 100644 index 0000000000..bbe29f9b00 --- /dev/null +++ b/BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Components/Preview/ThumbnailIconPreview.spec.lua @@ -0,0 +1,27 @@ +return function() + local Library = script.Parent.Parent.Parent + local Roact = require(Library.Parent.Parent.Roact) + + local MockWrapper = require(Library.MockWrapper) + + local ThumbnailIconPreview = require(Library.Components.Preview.ThumbnailIconPreview) + + local function createTestAsset(container, name) + local targetInstance = Instance.new("Script") + local element = Roact.createElement(MockWrapper, {}, { + Roact.createElement(ThumbnailIconPreview, { + TargetInstance = targetInstance, + TextContent = "ThumbnailIconPreviewTest", + Position = UDim2.new(1, 0, 1, 0), + Size = UDim2.new(0, 100, 0, 20) + }) + }) + + return Roact.mount(element, container or nil, name or "") + end + + it("should create and destroy without errors", function() + local instance = createTestAsset() + Roact.unmount(instance) + end) +end \ No newline at end of file diff --git a/BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Components/Preview/TreeViewButton.lua b/BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Components/Preview/TreeViewButton.lua new file mode 100644 index 0000000000..5a5688eb8f --- /dev/null +++ b/BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Components/Preview/TreeViewButton.lua @@ -0,0 +1,143 @@ +--[[ + This is a button component styled to represent toggling a tree view for Asset Preview + + Necessary properties: + Position = UDim2 + ZIndex = number, + ShowTreeView = boolean, represents whether or not the button is selected. + OnTreeviewStatusToggle = callback, this is thefunction that should be invoked by this button. + + Optionlal properties: + Size = number, This is the length and width of the button. +]] + +local Library = script.Parent.Parent.Parent + +local Roact = require(Library.Parent.Parent.Roact) + +local RoundButton = require(Library.Components.RoundFrame) + +local Theming = require(Library.Theming) +local withTheme = Theming.withTheme + +local MainViewButtons = Roact.PureComponent:extend("MainViewButtons") + +-- Determined by how many buttons we have and the padding. +local TOTAL_WIDTH = 28 +local TOTAL_HEIGHT = 28 + +local INLINE_PADDING = 6 + +local BUTTON_STATUS = { + default = 0, + hovered = 1, + disabled = 2, +} + +function MainViewButtons:init(props) + self.state = { + treeViewButtonStatus = BUTTON_STATUS.default, + } + + self.onTreeViewButtonActivated = function() + local newTreeViewStatus = not self.props.ShowTreeView + + self.props.OnTreeviewStatusToggle(newTreeViewStatus) + end + + self.onTreeViewButtonEnter = function() + self:setState({ + treeViewButtonStatus = BUTTON_STATUS.hovered + }) + end + + self.onTreeViewButtonLeave = function() + self:setState({ + treeViewButtonStatus = BUTTON_STATUS.default + }) + end +end + +local function getButtonBGColorAndTrans(buttonsTheme, buttonStatus, toggleStatus) + local buttonBGColor + local buttonTrans + + local defaultTrans = buttonsTheme.backgroundTrans + if toggleStatus then + defaultTrans = defaultTrans + 0.3 + end + + if buttonStatus == BUTTON_STATUS.default then + buttonBGColor = buttonsTheme.backgroundColor + buttonTrans = defaultTrans + elseif buttonStatus == BUTTON_STATUS.hovered then + buttonBGColor = buttonsTheme.backgroundColor + buttonTrans = defaultTrans + 0.3 + else -- BUTTON_STATUS.disabled + buttonBGColor = buttonsTheme.backgroundDisabledColor + buttonTrans = defaultTrans + end + + return buttonBGColor, buttonTrans +end + +function MainViewButtons:render() + return withTheme(function(theme) + local props = self.props + local state = self.state + + local buttonsTheme = theme.assetPreview.treeViewButton + + local position = props.Position or UDim2.new(1, 0, 1, 0) + local treeViewButtonSize = props.TreeViewButtonSize or buttonsTheme.buttonSize + + local treeViewButtonBGColor, treeViewbButtonTrans = getButtonBGColorAndTrans(buttonsTheme, + state.treeViewButtonStatus, + props.showTreeView) + + return Roact.createElement("Frame", { + Position = position, + AnchorPoint = Vector2.new(1, 1), + Size = UDim2.new(0, TOTAL_WIDTH, 0, TOTAL_HEIGHT), + ZIndex = props.ZIndex or 1, + + BackgroundTransparency = 1, + BorderSizePixel = 0, + },{ + UIListLayout = Roact.createElement("UIListLayout", { + FillDirection = Enum.FillDirection.Horizontal, + HorizontalAlignment = Enum.HorizontalAlignment.Right, + VerticalAlignment = Enum.VerticalAlignment.Center, + + SortOrder = Enum.SortOrder.LayoutOrder, + Padding = UDim.new(0, INLINE_PADDING), + }), + + TreeViewBGButton = Roact.createElement(RoundButton, { + Size = UDim2.new(0, treeViewButtonSize, 0, treeViewButtonSize), + + BackgroundTransparency = treeViewbButtonTrans, + BackgroundColor3 = treeViewButtonBGColor, + BorderSizePixel = 0, + + LayoutOrder = 1, + AutoButtonColor = false, + + OnActivated = self.onTreeViewButtonActivated, + OnMouseEnter = self.onTreeViewButtonEnter, + OnMouseLeave = self.onTreeViewButtonLeave, + }, { + TreeViewImageLabel = Roact.createElement("ImageLabel", { + AnchorPoint = Vector2.new(0.5, 0.5), + Position = UDim2.new(0.5, 0, 0.5, 0), + Size = UDim2.new(0, 16, 0, 16), + + Image = buttonsTheme.hierarchy, + BackgroundTransparency = 1, + }) + }), + }) + end) +end + +return MainViewButtons \ No newline at end of file diff --git a/BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Components/Preview/TreeViewButton.spec.lua b/BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Components/Preview/TreeViewButton.spec.lua new file mode 100644 index 0000000000..4906ad5a24 --- /dev/null +++ b/BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Components/Preview/TreeViewButton.spec.lua @@ -0,0 +1,26 @@ +return function() + local Library = script.Parent.Parent.Parent + local Roact = require(Library.Parent.Parent.Roact) + + local MockWrapper = require(Library.MockWrapper) + + local TreeViewButton = require(Library.Components.Preview.TreeViewButton) + + local function createTestAsset(container, name) + local element = Roact.createElement(MockWrapper, {}, { + Roact.createElement(TreeViewButton, { + Position = UDim2.new(1, 0, 1, 0), + + ShowTreeView = false, + OnTreeviewStatusToggle = nil, + }) + }) + + return Roact.mount(element, container or nil, name or "") + end + + it("should create and destroy without errors", function() + local instance = createTestAsset() + Roact.unmount(instance) + end) +end \ No newline at end of file diff --git a/BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Components/Preview/VideoPreview.lua b/BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Components/Preview/VideoPreview.lua new file mode 100644 index 0000000000..c27692db58 --- /dev/null +++ b/BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Components/Preview/VideoPreview.lua @@ -0,0 +1,271 @@ +--[[ + Provides logic for a VideoFrame and it's play/pause button, progress bar, and a time label. + + Required Props: + string VideoId: the Content string for VideoFrames. Should be formatted as a Content string "rbxassetid://123456". + boolean ShowTreeView: whether or not to show the TreeView button. It is used to + adjust the position of the time label and progress bar. + + Optional Props: + UDim2 LayoutOrder: The LayoutOrder of the component + UDim2 Position: The Position of the component + UDim2 Size: The Size of the component + callback OnPlay: Optional analytics call when clicking the play button + callback OnPause: Optional analytics call when clicking the pause button + + Props automatically received from wrapDraggableMedia(): + callback OnSliderInputChanged: Called when the progressbar slider input is changed. + callback OnSliderInputEnded: Called when the progressbar slider input ends. + + Props automatically received from wrapMedia(): + number _CurrentTime: The time in seconds that the media's TimePosition should currently be. + boolean _IsPlaying: Whether or not the Sound or VideoFrame should be currently playing. + callback _MediaPlayingUpdateSignal: Called when the media's Changed event is fired. Sets the isPlaying state. + callback _OnMediaEnded: Called when the media's Ended event is fired. Resets the currentTime & stops playing. + callback _Pause: Called when clicking the pause button. + callback _Play: Called when clicking the play button. + callBack _SetCurrentTime: Called if the currentTime has been changed, such as when moving a progressbar slider. +]] +local Library = script.Parent.Parent.Parent +local Roact = require(Library.Parent.Parent.Roact) + +local Theming = require(Library.Theming) +local withTheme = Theming.withTheme + +local wrapDraggableMedia = require(script.Parent.wrapDraggableMedia) + +local MediaControl = require(Library.Components.Preview.MediaControl) +local MediaProgressBar = require(Library.Components.Preview.MediaProgressBar) + +local PROGRESS_BAR_TOTAL_HEIGHT = 30 +local AUDIO_CONTROL_HEIGHT = 35 +local ICON_SIZE = 30 + +local VideoPreview = Roact.PureComponent:extend("VideoPreview") + +VideoPreview.defaultProps = { + Size = UDim2.new(1, 0, 1, 0), +} + +function VideoPreview:init() + self.layoutRef = Roact.createRef() + self.videoRef = Roact.createRef() + self.videoContainerRef = Roact.createRef() + + self.state = { + timeLength = 0, + isLoaded = false, + showOverlayPlayIcon = true, + resolution = Vector2.new(4, 3), + } + + self.dispatchMediaPlayingUpdate = function(updateType) + local videoObj = self.videoRef.current + if videoObj then + if updateType == "PLAY" then + videoObj:Play() + if self.props._OnPlay then + self.props._OnPlay() + end + elseif updateType == "PAUSE" then + videoObj:Pause() + if self.props._OnPause then + self.props._OnPause() + end + elseif updateType == "END" then + videoObj.Playing = false + videoObj.TimePosition = 0 + end + end + end + + self.onVideoPropertyChanged = function(rbx, property) + local videoObj = self.videoRef.current + if not videoObj or not self.isMounted then + return + end + if property == "TimeLength" then + self:setState({ + isLoaded = videoObj.IsLoaded, + timeLength = videoObj.TimeLength, + }) + self.props._SetTimeLength(videoObj.TimeLength) + elseif videoObj.IsLoaded ~= self.state.isLoaded then + self:setState({ + isLoaded = videoObj.IsLoaded, + }) + elseif property == "Resolution" then + self:setState({ + resolution = videoObj.Resolution, + }) + self.onResize() + end + end + + self.onResize = function() + local currentLayout = self.layoutRef.current + local videoFrame = self.videoRef.current + local videoContainer = self.videoContainerRef.current + if not videoFrame or not currentLayout or not videoContainer then + return + end + + local resolution = self.state.resolution + local height = videoContainer.AbsoluteSize.Y + local width = height * resolution.X / resolution.Y + if (currentLayout.AbsoluteContentSize.X < width) then + width = currentLayout.AbsoluteContentSize.X + height = width * resolution.Y / resolution.X + end + videoFrame.Size = UDim2.new(UDim.new(0, width), UDim.new(0, height)) + end + + self.onSliderInputChanged = function(newValue) + local videoFrame = self.videoRef.current + videoFrame.TimePosition = newValue or 0 + videoFrame.Playing = false + self:setState({ + showOverlayPlayIcon = false, + }) + + self.props._OnSliderInputChanged(newValue) + end + + self.onSliderInputEnded = function() + local videoFrameObj = self.videoRef.current + videoFrameObj.Playing = self.props._IsPlaying + self:setState({ + showOverlayPlayIcon = true, + }) + + self.props._OnSliderInputEnded() + end + + self.togglePlay = function() + if self.props._IsPlaying then + self.props._Pause() + else + self.props._Play() + end + end +end + +function VideoPreview:didMount() + self.isMounted = true + self.onResize() + self.mediaPlayingUpdateConnection = self.props._MediaPlayingUpdateSignal:connect(self.dispatchMediaPlayingUpdate) +end + +function VideoPreview:willUnmount() + self.isMounted = false + if self.mediaPlayingUpdateConnection then + self.mediaPlayingUpdateConnection:disconnect() + self.mediaPlayingUpdateConnection = nil + end +end + +function VideoPreview:render() + return withTheme(function(theme) + local VideoPreviewTheme = theme.assetPreview.videoPreview + + local props = self.props + local state = self.state + + local isLoaded = state.isLoaded + local timeLength = state.timeLength + local showOverlayPlayIcon = state.showOverlayPlayIcon + + local layoutOrder = props.LayoutOrder + local position = props.Position + local size = props.Size + local showTreeView = props.ShowTreeView + local videoId = props.VideoId + + -- Props passed from wrapDraggableMedia() and wrapMedia() + local currentTime = props._CurrentTime + local isPlaying = props._IsPlaying + local onMediaEnded = props._OnMediaEnded + local pause = props._Pause + local play = props._Play + + return Roact.createElement("Frame", { + BackgroundColor3 = VideoPreviewTheme.backgroundColor, + BackgroundTransparency = 0, + BorderSizePixel = 0, + LayoutOrder = layoutOrder, + Position = position, + Size = size, + }, { + UIListLayout = Roact.createElement("UIListLayout", { + FillDirection = Enum.FillDirection.Vertical, + SortOrder = Enum.SortOrder.LayoutOrder, + VerticalAlignment = Enum.VerticalAlignment.Center, + Padding = UDim.new(0, 10), + + [Roact.Change.AbsoluteContentSize] = self.onResize, + [Roact.Ref] = self.layoutRef, + }), + + VideoFrameButton = Roact.createElement("TextButton", { + AutoButtonColor = false, + BackgroundTransparency = 1, + BackgroundColor3 = VideoPreviewTheme.videoBackgroundColor, + LayoutOrder = 1, + Size = UDim2.new(1, 0, 1, -PROGRESS_BAR_TOTAL_HEIGHT - AUDIO_CONTROL_HEIGHT), + Text = "", + + [Roact.Ref] = self.videoContainerRef, + [Roact.Event.Activated] = self.togglePlay, + }, { + VideoFrameObj = Roact.createElement("VideoFrame", { + AnchorPoint = Vector2.new(0.5, 0), + BackgroundTransparency = 1, + Looped = false, + Position = UDim2.new(0.5, 0, 0, 0), + Size = UDim2.new(1, 0, 1, 0), + Video = videoId, + + [Roact.Ref] = self.videoRef, + [Roact.Event.Changed] = self.onVideoPropertyChanged, + [Roact.Event.Ended] = onMediaEnded, + }, { + PauseOverlay = (not isPlaying) and Roact.createElement("Frame", { + BackgroundColor3 = VideoPreviewTheme.pauseOverlayColor, + BackgroundTransparency = VideoPreviewTheme.pauseOverlayTransparency, + Size = UDim2.new(1, 0, 1, 0), + }, { + PlayVideoIcon = showOverlayPlayIcon and Roact.createElement("ImageLabel", { + AnchorPoint = Vector2.new(0.5, 0.5), + BackgroundTransparency = 1, + Image = VideoPreviewTheme.playButton, + Position = UDim2.new(0.5, 0, 0.5, 0), + Size = UDim2.new(0, ICON_SIZE, 0, ICON_SIZE), + }), + }), + }), + }), + + ProgressBar = Roact.createElement(MediaProgressBar, { + CurrentValue = currentTime, + LayoutOrder = 2, + Min = 0, + Max = timeLength, + OnValuesChanged = self.onSliderInputChanged, + OnInputEnded = self.onSliderInputEnded, + }), + + VideoControl = Roact.createElement(MediaControl, { + LayoutOrder = 3, + ShowTreeView = showTreeView, + IsPlaying = isPlaying, + IsLoaded = isLoaded, + OnPause = pause, + OnPlay = play, + TimeLength = timeLength, + TimePassed = currentTime, + }), + }) + end) +end + +return wrapDraggableMedia(VideoPreview) \ No newline at end of file diff --git a/BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Components/Preview/VideoPreview.spec.lua b/BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Components/Preview/VideoPreview.spec.lua new file mode 100644 index 0000000000..3878a7c0e5 --- /dev/null +++ b/BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Components/Preview/VideoPreview.spec.lua @@ -0,0 +1,26 @@ +return function() + local Library = script.Parent.Parent.Parent + local Roact = require(Library.Parent.Parent.Roact) + + local MockWrapper = require(Library.MockWrapper) + + local VideoPreview = require(Library.Components.Preview.VideoPreview) + + local function createTestAsset(container, name) + local element = Roact.createElement(MockWrapper, {}, { + Roact.createElement(VideoPreview, { + VideoId = 123, + ShowTreeView = false, + OnPlay = function() end, + OnPause = function() end, + }) + }) + + return Roact.mount(element, container or nil, name or "") + end + + it("should create and destroy without errors", function() + local instance = createTestAsset() + Roact.unmount(instance) + end) +end \ No newline at end of file diff --git a/BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Components/Preview/Vote.lua b/BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Components/Preview/Vote.lua new file mode 100644 index 0000000000..2f05a24b7b --- /dev/null +++ b/BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Components/Preview/Vote.lua @@ -0,0 +1,272 @@ +--[[ + This is the Vote component for AssetPreview component. + + Necessary properties: + Voting = table, a table contains the voting data + AssetId = num + OnVoteUpButtonActivated = callback, for the behavior when the Vote Up Button is clicked. + OnVoteDownButtonActivated = callback, for the behavior when the Vote Down Button is clicked. + + Optionlal properties: + Size = UDim2, + Position = UDim2, + layoutOrder = num +]] + +local Library = script.Parent.Parent.Parent + +local Roact = require(Library.Parent.Parent.Roact) + +local RoundButton = require(Library.Components.RoundFrame) +local RoundFrame = require(Library.Components.RoundFrame) + +local Theming = require(Library.Theming) +local withTheme = Theming.withTheme + +local Vote = Roact.PureComponent:extend("Vote") + +local INLINE_PADDING = 6 + +local BUTTON_STATUS = { + default = 0, + hovered = 1, +} + +function Vote:init(props) + self.state = { + voteUpStatus = BUTTON_STATUS.default, + voteDownStatus = BUTTON_STATUS.default, + } + + self.onVoteUpButtonActivated = function() + self.props.OnVoteUpButtonActivated(self.props.AssetId, self.props.Voting) + end + + self.onVoteDownButtonActivated = function() + self.props.OnVoteDownButtonActivated(self.props.AssetId, self.props.Voting) + end + + self.onVoteUpEnter = function() + self:setState({ + voteUpStatus = BUTTON_STATUS.hovered + }) + end + + self.onVoteUpLeave = function() + self:setState({ + voteUpStatus = BUTTON_STATUS.default + }) + end + + self.onVoteDownEnter = function() + self:setState({ + voteDownStatus = BUTTON_STATUS.hovered + }) + end + + self.onVoteDownLeave = function() + self:setState({ + voteDownStatus = BUTTON_STATUS.default + }) + end +end + +function Vote:render() + return withTheme(function(theme) + local props = self.props + local state = self.state + + local position = props.Position or UDim2.new(1, 0, 1, 0) + local size = props.Size or UDim2.new(0, 100, 0, 20) + + local voting = props.Voting + local canVote = voting.CanVote + local hasVoted = voting.HasVoted + local userVote = voting.UserVote + local upVotes = voting.UpVotes or 0 + local downVotes = voting.DownVotes or 0 + local totalVotes = 0 + local upVoteRate = 0 + + local voteTheme = theme.assetPreview.vote + + local voteUpBGColor = voteTheme.button.backgroundColor + local voteDownBGColor = voteTheme.button.backgroundColor + local voteUpBGTransparancy = voteTheme.button.backgroundTrans + local voteDownBGTransparancy = voteTheme.button.backgroundTrans + local voteUpStatus = state.voteUpStatus + local voteDownStatus = state.voteDownStatus + if not canVote then + voteUpBGColor = voteTheme.button.disabledColor + voteDownBGColor = voteTheme.button.disabledColor + else + if voteUpStatus == BUTTON_STATUS.hovered then + voteUpBGTransparancy = voteUpBGTransparancy + 0.3 + end + if voteDownStatus == BUTTON_STATUS.hovered then + voteDownBGTransparancy = voteDownBGTransparancy + 0.3 + end + + if hasVoted then + if userVote then + voteUpBGColor = voteTheme.voteUp.backgroundColor + else + voteDownBGColor = voteTheme.voteDown.backgroundColor + end + end + + totalVotes = upVotes + downVotes + if totalVotes > 0 then + upVoteRate = (upVotes / totalVotes) * 100 + end + end + + local layoutOrder = props.LayoutOrder + + return Roact.createElement(RoundFrame, { + Position = position, + Size = size, + + BackgroundTransparency = voteTheme.backgroundTrans, + BackgroundColor3 = voteTheme.background, + BorderColor3 = voteTheme.boderColor, + + LayoutOrder = layoutOrder, + },{ + VoteButtons = Roact.createElement("Frame", { + Position = UDim2.new(1, -64, 0, 0), + Size = UDim2.new(0, 64, 1, 0), + + BackgroundTransparency = 1, + BorderSizePixel = 0, + }, { + UIPadding = Roact.createElement("UIPadding", { + PaddingBottom = UDim.new(0, 0), + PaddingLeft = UDim.new(0, 0), + PaddingRight = UDim.new(0, INLINE_PADDING), + PaddingTop = UDim.new(0, 0), + }), + + UIListLayout = Roact.createElement("UIListLayout", { + FillDirection = Enum.FillDirection.Horizontal, + HorizontalAlignment = Enum.HorizontalAlignment.Right, + VerticalAlignment = Enum.VerticalAlignment.Center, + + SortOrder = Enum.SortOrder.LayoutOrder, + Padding = UDim.new(0, INLINE_PADDING), + }), + + + VoteDownButton = Roact.createElement(RoundButton, { + Active = canVote, + Size = UDim2.new(0, 28, 0, 28), + + BackgroundTransparency = voteDownBGTransparancy, + BackgroundColor3 = voteDownBGColor, + BorderColor3 = voteTheme.voteDown.borderColor, + + LayoutOrder = 1, + AutoButtonColor = false, + + OnActivated = self.onVoteDownButtonActivated, + OnMouseEnter = self.onVoteDownEnter, + OnMouseLeave = self.onVoteDownLeave, + }, { + VoteDownImageLabel = Roact.createElement("ImageLabel", { + Size = UDim2.new(1, 0, 1, 0), + + Image = voteTheme.images.voteDown, + BackgroundTransparency = 1, + }) + }), + + VoteUpButton = Roact.createElement(RoundButton, { + Active = canVote, + + Size = UDim2.new(0, 28, 0, 28), + + BackgroundTransparency = voteUpBGTransparancy, + BackgroundColor3 = voteUpBGColor, + BorderColor3 = voteTheme.voteUp.borderColor, + + LayoutOrder = 2, + AutoButtonColor = false, + + OnActivated = self.onVoteUpButtonActivated, + OnMouseEnter = self.onVoteUpEnter, + OnMouseLeave = self.onVoteUpLeave, + }, { + VoteUpImageLabel = Roact.createElement("ImageLabel", { + Size = UDim2.new(1, 0, 1, 0), + + Image = voteTheme.images.voteUp, + BackgroundTransparency = 1, + }) + }), + }), + + VoteInformations = Roact.createElement("Frame", { + Position = UDim2.new(0, INLINE_PADDING, 0, 0), + Size = UDim2.new(1, -64, 1, 0), + + BackgroundTransparency = 1, + BorderSizePixel = 0, + }, { + UIPadding = Roact.createElement("UIPadding", { + PaddingLeft = UDim.new(0, 6), + }), + + UIListLayout = Roact.createElement("UIListLayout", { + FillDirection = Enum.FillDirection.Horizontal, + HorizontalAlignment = Enum.HorizontalAlignment.Left, + VerticalAlignment = Enum.VerticalAlignment.Center, + + SortOrder = Enum.SortOrder.LayoutOrder, + Padding = UDim.new(0, 8), + }), + + VoteIcon = Roact.createElement("ImageLabel", { + Size = UDim2.new(0, 17, 0, 20), + BackgroundTransparency = 1, + Image = voteTheme.images.thumbUp, + }), + + VoteRatio = Roact.createElement("TextLabel", { + Size = UDim2.new(0, 28, 1, 0), + + TextXAlignment = Enum.TextXAlignment.Left, + TextYAlignment = Enum.TextYAlignment.Center, + + BackgroundTransparency = 1, + BorderSizePixel = 0, + + Text = ("%d%%"):format(upVoteRate), + TextSize = theme.assetPreview.textSizeLarge, + Font = theme.assetPreview.font, + TextColor3 = voteTheme.textColor, + + LayoutOrder = 1, + }), + + TotalVotes = Roact.createElement("TextLabel", { + Size = UDim2.new(1, 0, 1, 0), + + TextXAlignment = Enum.TextXAlignment.Left, + TextYAlignment = Enum.TextYAlignment.Center, + + BackgroundTransparency = 1, + BorderSizePixel = 0, + + Text = ("%d VOTES"):format(tostring(totalVotes)), + TextSize = theme.assetPreview.textSize, + Font = theme.assetPreview.font, + TextColor3 = voteTheme.subTextColor, + + LayoutOrder = 2, + }), + }), + }) + end) +end + +return Vote \ No newline at end of file diff --git a/BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Components/Preview/Vote.spec.lua b/BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Components/Preview/Vote.spec.lua new file mode 100644 index 0000000000..3c6cf34a54 --- /dev/null +++ b/BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Components/Preview/Vote.spec.lua @@ -0,0 +1,32 @@ +return function() + local Library = script.Parent.Parent.Parent + local Roact = require(Library.Parent.Parent.Roact) + + local MockWrapper = require(Library.MockWrapper) + + local Vote = require(Library.Components.Preview.Vote) + + local function createTestAsset(container, name) + local voting = { + CanVote = false, + HasVoted = false, + UserVote = false, + } + + local element = Roact.createElement(MockWrapper, {}, { + Vote = Roact.createElement(Vote, { + Position = UDim2.new(1, 0, 1, 0), + Size = UDim2.new(0, 100, 0, 20), + AssetId = 183435411, + Voting = voting, + }) + }) + + return Roact.mount(element, container or nil, name or "") + end + + it("should create and destroy without errors", function() + local instance = createTestAsset() + Roact.unmount(instance) + end) +end \ No newline at end of file diff --git a/BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Components/Preview/wrapDraggableMedia.lua b/BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Components/Preview/wrapDraggableMedia.lua new file mode 100644 index 0000000000..2d4cf2dc27 --- /dev/null +++ b/BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Components/Preview/wrapDraggableMedia.lua @@ -0,0 +1,67 @@ +--[[ + Wraps a component with logic required for a media's time interactable progressbar, play/pause button, + and time countdown. The wrapped component passes all props that wrapMedia in addition to interactable slider logic. + + Props automatically received from wrapMedia(): + boolean IsPlaying: Whether or not the Sound or VideoFrame is currently playing. + callback Pause: Called when clicking the pause button. + callback Play: Called when clicking the play button. + callBack SetCurrentTime: Called if the currentTime has been changed, such as when moving a progressbar slider. + + Returns: + callback _OnSliderInputChanged: Called when the progressbar slider input is changed. + callback _OnSliderInputEnded: Called when the progressbar slider input ends. + + number _CurrentTime: The time in seconds that the media's TimePosition should currently be. + boolean _IsPlaying: Whether or not the Sound or VideoFrame should be currently playing. + callback _MediaPlayingUpdateSignal: Called when the media's Changed event is fired. Sets the isPlaying state. + callback _OnMediaEnded: Called when the media's Ended event is fired. Resets the currentTime & stops playing. + callback _Pause: Called when clicking the pause button. + callback _Play: Called when clicking the play button. + callBack _SetCurrentTime: Called if the currentTime has been changed, such as when moving a progressbar slider. +]] +local Library = script.Parent.Parent.Parent +local Roact = require(Library.Parent.Parent.Roact) +local wrapMedia = require(script.Parent.wrapMedia) +local Immutable = require(Library.Utils.Immutable) + +local function wrapDraggableMedia(wrappedComponent) + local componentName = wrappedComponent and wrappedComponent.component and tostring(wrappedComponent.component) or "" + local DraggableMediaWrapper = Roact.PureComponent:extend(("DraggableMediaWrapper(%s)"):format(componentName)) + + function DraggableMediaWrapper:init() + self.isPlayingBeforeDrag = nil + + self.onSliderInputChanged = function(newValue) + local isPlaying = self.props._IsPlaying + if self.isPlayingBeforeDrag == nil then + self.isPlayingBeforeDrag = isPlaying + end + + if isPlaying then + self.props._Pause() + end + + self.props._SetCurrentTime(newValue) + end + + self.onSliderInputEnded = function() + if self.isPlayingBeforeDrag then + self.props._Play() + end + self.isPlayingBeforeDrag = nil + end + end + + function DraggableMediaWrapper:render() + local props = Immutable.JoinDictionaries(self.props, { + _OnSliderInputChanged = self.onSliderInputChanged, + _OnSliderInputEnded = self.onSliderInputEnded, + }) + return Roact.createElement(wrappedComponent, props) + end + + return wrapMedia(DraggableMediaWrapper) +end + +return wrapDraggableMedia \ No newline at end of file diff --git a/BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Components/Preview/wrapDraggableMedia.spec.lua b/BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Components/Preview/wrapDraggableMedia.spec.lua new file mode 100644 index 0000000000..af394c3897 --- /dev/null +++ b/BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Components/Preview/wrapDraggableMedia.spec.lua @@ -0,0 +1,38 @@ +return function() + local Library = script.Parent.Parent.Parent + local Roact = require(Library.Parent.Parent.Roact) + + local wrapDraggableMedia = require(script.Parent.wrapDraggableMedia) + + local function createTestComponent() + local TEST_WRAPPER = Roact.PureComponent:extend("TEST_WRAPPER") + function TEST_WRAPPER:render() + return Roact.createElement("Frame") + end + return TEST_WRAPPER + end + + it("should create and destroy without errors", function() + local element = wrapDraggableMedia(createTestComponent()) + local component = Roact.createElement(element, {}, {}) + local instance = Roact.mount(component) + + Roact.unmount(instance) + end) + + it("should add props into the component parameter", function() + local hasNewProps + local testComponent = createTestComponent() + function testComponent:init() + hasNewProps = (self.props._OnSliderInputChanged ~= nil) + hasNewProps = hasNewProps and (self.props._OnSliderInputEnded ~= nil) + end + + local element = wrapDraggableMedia(testComponent) + local component = Roact.createElement(element, {}, {}) + local instance = Roact.mount(component) + + Roact.unmount(instance) + expect(hasNewProps).to.be.equal(true) + end) +end \ No newline at end of file diff --git a/BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Components/Preview/wrapMedia.lua b/BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Components/Preview/wrapMedia.lua new file mode 100644 index 0000000000..78426ecd6c --- /dev/null +++ b/BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Components/Preview/wrapMedia.lua @@ -0,0 +1,116 @@ +--[[ + Wraps a component with logic required for a media's time progressbar, play/pause button, and time countdown. + + Returns: + number _CurrentTime: The time in seconds that the media's TimePosition should currently be. + boolean _IsPlaying: Whether or not the Sound or VideoFrame should be currently playing. + callback _MediaPlayingUpdateSignal: Should be called when the media's Changed event is fired. Sets the isPlaying state. + callback _OnMediaEnded: Should be called when the media's Ended event is fired. Resets the currentTime & stops playing. + callback _Pause: Should be called when clicking the pause button. + callback _Play: Should be called when clicking the play button. + callBack _SetCurrentTime: Should be called if the currentTime has been changed, such as when moving a progressbar slider. + callBack _SetTimeLength: Should be called if the timeLnegth has been changed, such as when a new audio or video is loaded. +]] +local RunService = game:GetService("RunService") + +local Library = script.Parent.Parent.Parent +local Roact = require(Library.Parent.Parent.Roact) + +local Signal = require(Library.Utils.Signal) +local Immutable = require(Library.Utils.Immutable) + +local function wrapMedia(wrappedComponent) + local MediaWrapper = Roact.PureComponent:extend(("MediaWrapper(%s)"):format(tostring(wrappedComponent))) + + function MediaWrapper:init() + self.state = { + currentTime = 0, + isPlaying = false, + timeLength = 0, + } + + self.mediaPlayingUpdateSignal = Signal.new() + + self.play = function() + self.mediaPlayingUpdateSignal:fire("PLAY") + self:setState({ + isPlaying = true, + }) + end + + self.pause = function() + self.mediaPlayingUpdateSignal:fire("PAUSE") + self:setState({ + isPlaying = false, + }) + end + + self.onMediaEnded = function() + self:setState({ + currentTime = 0, + isPlaying = false, + }) + end + + self.setCurrentTime = function(currentTime) + self:setState({ + currentTime = currentTime, + }) + end + + self.setTimeLength = function(timeLength) + self:setState({ + timeLength = timeLength, + }) + end + + self.onRenderStepped = function(deltaTime) + if not self.isMounted or not self.state.isPlaying then + return + end + + local newTime = self.state.currentTime + deltaTime + + if newTime >= self.state.timeLength then + self.onMediaEnded() + self.mediaPlayingUpdateSignal:fire("END") + else + self:setState({ + currentTime = newTime, + }) + end + end + end + + function MediaWrapper:didMount() + self.isMounted = true + self.runServiceConnection = RunService.RenderStepped:Connect(self.onRenderStepped) + end + + function MediaWrapper:willUnmount() + self.isMounted = false + if self.runServiceConnection then + self.runServiceConnection:Disconnect() + self.runServiceConnection = nil + end + end + + function MediaWrapper:render() + local props = Immutable.JoinDictionaries(self.props, { + _CurrentTime = self.state.currentTime, + _IsPlaying = self.state.isPlaying, + _MediaPlayingUpdateSignal = self.mediaPlayingUpdateSignal, + _OnMediaEnded = self.onMediaEnded, + _Play = self.play, + _Pause = self.pause, + _SetCurrentTime = self.setCurrentTime, + _SetTimeLength = self.setTimeLength, + }) + + return Roact.createElement(wrappedComponent, props) + end + + return MediaWrapper +end + +return wrapMedia \ No newline at end of file diff --git a/BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Components/Preview/wrapMedia.spec.lua b/BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Components/Preview/wrapMedia.spec.lua new file mode 100644 index 0000000000..374ec29804 --- /dev/null +++ b/BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Components/Preview/wrapMedia.spec.lua @@ -0,0 +1,49 @@ +return function() + local Library = script.Parent.Parent.Parent + local Roact = require(Library.Parent.Parent.Roact) + + local wrapMedia = require(script.Parent.wrapMedia) + + local function createTestComponent() + local TEST_WRAPPER = Roact.PureComponent:extend("TEST_WRAPPER") + function TEST_WRAPPER:render() + return Roact.createElement("Frame") + end + return TEST_WRAPPER + end + + it("should create and destroy without errors", function() + local element = wrapMedia(createTestComponent()) + local component = Roact.createElement(element, {}, {}) + local instance = Roact.mount(component) + + Roact.unmount(instance) + end) + + it("should add props into the component parameter", function() + local hasNewProps + local testComponent = createTestComponent() + function testComponent:init() + local expectedProps = { + "_CurrentTime", + "_IsPlaying", + "_MediaPlayingUpdateSignal", + "_OnMediaEnded", + "_Pause", + "_Play", + "_SetCurrentTime", + } + hasNewProps = (self.props._CurrentTime ~= nil) + for _,value in pairs(expectedProps) do + hasNewProps = hasNewProps and (self.props[value] ~= nil) + end + end + + local element = wrapMedia(testComponent) + local component = Roact.createElement(element, {}, {}) + local instance = Roact.mount(component) + + Roact.unmount(instance) + expect(hasNewProps).to.be.equal(true) + end) +end \ No newline at end of file diff --git a/BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Components/RadioButtons.lua b/BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Components/RadioButtons.lua new file mode 100644 index 0000000000..bb2cfa5907 --- /dev/null +++ b/BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Components/RadioButtons.lua @@ -0,0 +1,140 @@ +--[[ + A set of an arbitrary number of Radio buttons. Automatically scales to fit + the number of buttons contained in this component. + + Props: + Table Buttons: A table of buttons to use. + Format: {{Key = "Key1", Text = "Text1"}, ...} + string Selected = The current button that is selected. + Enum.FillDirection FillDirection = if the buttons should be in a vertical or horizontal layout + int LayoutOrder = The layout order of the frame, if in a Layout. + + function onButtonClicked(string key) = A callback for when a user selects a button. +]] + +local NO_WRAP = Vector2.new(1000000, 50) +local BUTTON_HEIGHT_SCALE = 0.4 + +local TextService = game:GetService("TextService") + +local Library = script.Parent.Parent + +local Roact = require(Library.Parent.Parent.Roact) + +local createFitToContent = require(Library.Components.createFitToContent) + +local Theming = require(Library.Theming) +local withTheme = Theming.withTheme + +local RadioButtons = Roact.PureComponent:extend("RadioButtons") + +function RadioButtons:init() + self.layoutRef = Roact.createRef() + self.containerRef = Roact.createRef() + self.currentLayout = 0 + + self.onButtonClicked = function(key, index) + if self.props.onButtonClicked then + self.props.onButtonClicked(key, index) + end + end +end + +function RadioButtons:createButton(key, text, index, selected, theme) + local textWidth = TextService:GetTextSize(text, theme.radioButton.textSize, theme.radioButton.font, NO_WRAP).X + local buttonHeight = theme.radioButton.buttonHeight + + local buttonSize = UDim2.new(1, 0, 0, buttonHeight) + if self.props.FillDirection == Enum.FillDirection.Horizontal then + buttonSize = UDim2.new(0, textWidth + buttonHeight, 0, buttonHeight) + end + + return Roact.createElement("Frame", { + LayoutOrder = self:nextLayout(), + BackgroundTransparency = 1, + Size = buttonSize, + }, { + UIListLayout = Roact.createElement("UIListLayout", { + FillDirection = Enum.FillDirection.Horizontal, + VerticalAlignment = Enum.VerticalAlignment.Center, + SortOrder = Enum.SortOrder.LayoutOrder, + Padding = UDim.new(0, theme.radioButton.buttonPadding), + }), + + Background = Roact.createElement("ImageButton", { + LayoutOrder = 1, + Size = UDim2.new(0, buttonHeight, 0, buttonHeight), + BackgroundTransparency = 1, + ImageColor3 = theme.radioButton.radioButtonColor, + Image = theme.radioButton.radioButtonBackground, + + [Roact.Event.Activated] = function() + self.onButtonClicked(key, index) + end, + }, { + Highlight = selected and Roact.createElement("ImageLabel", { + Size = UDim2.new(BUTTON_HEIGHT_SCALE, 0, BUTTON_HEIGHT_SCALE, 0), + AnchorPoint = Vector2.new(0.5, 0.5), + Position = UDim2.new(0.5, 0, 0.5, 0), + BackgroundTransparency = 1, + Image = theme.radioButton.radioButtonSelected, + }), + }), + + Text = Roact.createElement("TextButton", { + LayoutOrder = 2, + Text = text, + Size = UDim2.new(0, textWidth, 1, 0), + BackgroundTransparency = 1, + Font = theme.radioButton.font, + TextSize = theme.radioButton.textSize, + TextColor3 = theme.radioButton.textColor, + TextXAlignment = Enum.TextXAlignment.Left, + + [Roact.Event.Activated] = function() + self.onButtonClicked(key, index) + end, + }), + }) +end + +function RadioButtons:resetLayout() + self.currentLayout = 0 +end + +function RadioButtons:nextLayout() + self.currentLayout = self.currentLayout + 1 + return self.currentLayout +end + +function RadioButtons:render() + return withTheme(function(theme) + local props = self.props + + local buttons = props.Buttons + local layoutOrder = props.LayoutOrder + local selected = props.Selected + local fillDirection = props.FillDirection + + local fitToContent = createFitToContent("Frame", "UIListLayout", { + FillDirection = fillDirection or Enum.FillDirection.Vertical, + Padding = UDim.new(0, theme.radioButton.contentPadding), + SortOrder = Enum.SortOrder.LayoutOrder, + }) + + self:resetLayout() + + local children = {} + for index, button in ipairs(buttons) do + children[button.Key] = self:createButton(button.Key, button.Text, index, + selected == button.Key, theme) + end + + return Roact.createElement(fitToContent, { + BackgroundTransparency = 1, + LayoutOrder = layoutOrder + }, children) + end) +end + +return RadioButtons diff --git a/BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Components/RadioButtons.spec.lua b/BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Components/RadioButtons.spec.lua new file mode 100644 index 0000000000..eb1f95c71e --- /dev/null +++ b/BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Components/RadioButtons.spec.lua @@ -0,0 +1,47 @@ +return function() + local Library = script.Parent.Parent + local Roact = require(Library.Parent.Parent.Roact) + + local MockWrapper = require(Library.MockWrapper) + + local RadioButtons = require(script.Parent.RadioButtons) + + it("should create and destroy without errors", function() + local element = Roact.createElement(MockWrapper, {}, { + Buttons = Roact.createElement(RadioButtons, { + Buttons = { + {Key = "Button1", Text = "Button 1"}, + {Key = "Button2", Text = "Button 2"}, + }, + Selected = "Button1", + }), + }) + local instance = Roact.mount(element) + Roact.unmount(instance) + end) + + it("should render correctly", function() + local element = Roact.createElement(MockWrapper, {}, { + Buttons = Roact.createElement(RadioButtons, { + Buttons = { + {Key = "Button1", Text = "Button 1"}, + {Key = "Button2", Text = "Button 2"}, + }, + Selected = "Button1", + }), + }) + + local container = Instance.new("Folder") + local instance = Roact.mount(element, container, "Buttons") + + local buttons = container.Buttons + expect(buttons.Layout).to.be.ok() + expect(buttons.Button1).to.be.ok() + expect(buttons.Button1.UIListLayout).to.be.ok() + expect(buttons.Button1.Background).to.be.ok() + expect(buttons.Button1.Background.Highlight).to.be.ok() + expect(buttons.Button1.Text).to.be.ok() + + Roact.unmount(instance) + end) +end diff --git a/BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Components/RoundFrame.lua b/BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Components/RoundFrame.lua new file mode 100644 index 0000000000..b781c5ab08 --- /dev/null +++ b/BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Components/RoundFrame.lua @@ -0,0 +1,107 @@ +--[[ + An element with rounded corners. + Designed to function almost identically to a standard roblox Frame. + + Props: + Color3 BackgroundColor3 = The background color of the frame. + float BackgroundTransparency = The transparency of the frame's background and border. + Color3 BorderColor = The border color of the frame. + int BorderSizePixel: + If == 0, the border will not render. If ~= 0, the border will render. + + UDim2 Size = The size of the frame. + UDim2 Position = The position of the frame. + Vector2 AnchorPoint = The center point of this frame. + int LayoutOrder = The layout order of the frame, if in a Layout. + int ZIndex = The draw index of the frame. + + function OnActivated = A callback fired when the user clicks the frame. + function OnMouseEnter = A callback fired when the mouse enters the frame. + function OnMouseLeave = A callback fired when the mouse leaves the frame. + + [Roact.Change.AbsoluteSize] = An event that fires when the frame's AbsoluteSize changes + [Roact.Change.AbsolutePosition] = An event that fires when the frame's AbsolutePosition changes +]] + +local Library = script.Parent.Parent + +local Roact = require(Library.Parent.Parent.Roact) + +local Theming = require(Library.Theming) +local withTheme = Theming.withTheme + +local ROUNDED_FRAME_SLICE = Rect.new(3, 3, 13, 13) +local DEFAULT_BORDER_COLOR = Color3.fromRGB(27, 42, 53) +local DEFAULT_SIZE = UDim2.new(0, 100, 0, 100) + +local RoundFrame = Roact.PureComponent:extend("RoundFrame") + +function RoundFrame:init(initialProps) + local isButton = initialProps.OnActivated ~= nil + self.elementType = isButton and "ImageButton" or "ImageLabel" +end + +function RoundFrame:render() + return withTheme(function(theme) + local props = self.props + local roundFrameTheme = theme.roundFrame + + local backgroundColor = props.BackgroundColor3 + local backgroundTransparency = props.BackgroundTransparency + local borderColor = props.BorderColor3 or DEFAULT_BORDER_COLOR + local borderSize = props.BorderSizePixel or 1 + local size = props.Size or DEFAULT_SIZE + local position = props.Position + local anchorPoint = props.AnchorPoint + local layoutOrder = props.LayoutOrder + local zindex = props.ZIndex + local activatedCallback = props.OnActivated + local mouseEnterCallback = props.OnMouseEnter + local mouseLeaveCallback = props.OnMouseLeave + + local borderTransparency + if borderSize == 0 then + borderTransparency = 1 + else + borderTransparency = backgroundTransparency + end + + return Roact.createElement(self.elementType, { + Size = size, + Position = position, + AnchorPoint = anchorPoint, + LayoutOrder = layoutOrder, + ZIndex = zindex, + + BackgroundTransparency = 1, + ImageColor3 = backgroundColor, + ImageTransparency = backgroundTransparency, + + Image = roundFrameTheme.backgroundImage, + ScaleType = Enum.ScaleType.Slice, + SliceCenter = ROUNDED_FRAME_SLICE, + + [Roact.Event.MouseEnter] = mouseEnterCallback, + [Roact.Event.MouseLeave] = mouseLeaveCallback, + [Roact.Event.Activated] = activatedCallback, + + [Roact.Change.AbsoluteSize] = props[Roact.Change.AbsoluteSize], + [Roact.Change.AbsolutePosition] = props[Roact.Change.AbsolutePosition], + }, { + Border = Roact.createElement("ImageLabel", { + Size = UDim2.new(1, 0, 1, 0), + + BackgroundTransparency = 1, + ImageColor3 = borderColor, + ImageTransparency = borderTransparency, + + Image = roundFrameTheme.borderImage, + ScaleType = Enum.ScaleType.Slice, + SliceCenter = ROUNDED_FRAME_SLICE, + SliceScale = borderSize, + }, props[Roact.Children]) + }) + end) +end + +return RoundFrame diff --git a/BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Components/RoundFrame.spec.lua b/BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Components/RoundFrame.spec.lua new file mode 100644 index 0000000000..b7c5babed6 --- /dev/null +++ b/BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Components/RoundFrame.spec.lua @@ -0,0 +1,79 @@ +return function() + local Library = script.Parent.Parent + local Roact = require(Library.Parent.Parent.Roact) + + local MockWrapper = require(Library.MockWrapper) + + local RoundFrame = require(script.Parent.RoundFrame) + + local function createTestRoundFrame(props, children) + return Roact.createElement(MockWrapper, {}, { + RoundFrame = Roact.createElement(RoundFrame, props, children) + }) + end + + it("should create and destroy without errors", function() + local element = createTestRoundFrame() + local instance = Roact.mount(element) + Roact.unmount(instance) + end) + + it("should render correctly", function () + local container = Instance.new("Folder") + local instance = Roact.mount(createTestRoundFrame(), container) + local frame = container:FindFirstChildOfClass("ImageLabel") + + expect(frame).to.be.ok() + expect(frame.Border).to.be.ok() + + Roact.unmount(instance) + end) + + it("should hide its border if BorderSizePixel == 0", function () + local container = Instance.new("Folder") + local instance = Roact.mount(createTestRoundFrame({ + BorderSizePixel = 0, + }), container) + local frame = container:FindFirstChildOfClass("ImageLabel") + + expect(frame.Border.ImageTransparency).to.equal(1) + + Roact.unmount(instance) + end) + + it("should be an ImageLabel if OnActivated is undefined", function () + local container = Instance.new("Folder") + local instance = Roact.mount(createTestRoundFrame(), container) + local frame = container:FindFirstChildOfClass("ImageLabel") + + expect(frame:IsA("ImageLabel")).to.equal(true) + + Roact.unmount(instance) + end) + + it("should be an ImageButton if OnActivated is defined", function () + local container = Instance.new("Folder") + local instance = Roact.mount(createTestRoundFrame({ + OnActivated = function() + end, + }), container) + local frame = container:FindFirstChildOfClass("ImageButton") + + expect(frame:IsA("ImageButton")).to.equal(true) + + Roact.unmount(instance) + end) + + it("should accept children", function () + local container = Instance.new("Folder") + local instance = Roact.mount(createTestRoundFrame({}, { + Child = Roact.createElement("Frame"), + }), container) + local frame = container:FindFirstChildOfClass("ImageLabel") + + expect(frame.Border).to.be.ok() + expect(frame.Border.Child).to.be.ok() + + Roact.unmount(instance) + end) +end \ No newline at end of file diff --git a/BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Components/RoundTextBox.lua b/BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Components/RoundTextBox.lua new file mode 100644 index 0000000000..2150859fde --- /dev/null +++ b/BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Components/RoundTextBox.lua @@ -0,0 +1,194 @@ +--[[ + A TextBox with rounded corners that allows single-line or multiline entry, + maximum character count, and error messages. + + Props: + bool Active = Whether this component can be interacted with. + int MaxLength = The maximum number of characters allowed in the TextBox. + bool Multiline = Whether this TextBox allows a single line of text or multiple. + int Height = The vertical size of this TextBox, in pixels. + int WidthOffset = the horizontal offset size of this TextBox, in pixels. + int LayoutOrder = The sort order of this component in a UIListLayout. + int TextSize = The size of text + + boolean ErrorBorder = puts red border around text box + string ErrorMessage = A general override message used to display an error. A non-nil ErrorMessage will border the TextBox in red. + + string Text = The text to display in the TextBox + string PlaceholderText = text to display when TextBox is empty/in default state + boolean ShowToolTip = do we want to show anything beneath the rounded text box (defaults to true) + boolean ShowErrors = do we want to show any error text beneath the rounded text box, or change the border to indicate an error (defaults to true) + function SetText(text) = Callback to tell parent that text has changed + function FocusChanged(focused) = Callback when this TextBox is focused. + function HoverChanged(hovering) = Callback when the mouse enters or leaves this TextBox. +]] + +local StudioUILibraryRoundTextBoxNoTooltip = settings():GetFFlag("StudioUILibraryRoundTextBoxNoTooltip") + +local DEFAULT_HEIGHT = 42 +local PADDING = UDim.new(0, 10) + +local Library = script.Parent.Parent +local Roact = require(Library.Parent.Parent.Roact) +local Theming = require(Library.Theming) +local withTheme = Theming.withTheme + +local TextEntry = require(Library.Components.TextEntry) +local MultilineTextEntry = require(Library.Components.MultilineTextEntry) + +local RoundTextBox = Roact.PureComponent:extend("RoundTextBox") + +function RoundTextBox:init() + self.state = { + Focused = false, + } + + self.focusChanged = function(focused) + if self.props.Active then + if self.props.FocusChanged then + self.props.FocusChanged(focused) + end + self:setState({ + Focused = focused, + }) + end + end + + self.mouseHoverChanged = function(hovering) + if self.props.Active then + if self.state.Focused and self.props.HoverChanged then + self.props.HoverChanged(hovering) + end + end + end +end + +function RoundTextBox:render() + return withTheme(function(theme) + local active = self.props.Active + local focused = self.state.Focused + local multiline = self.props.Multiline + local textLength = utf8.len(self.props.Text) + local pastMaxLength = self.props.MaxLength and textLength > self.props.MaxLength + local errorState = self.props.ErrorMessage + or pastMaxLength + + if StudioUILibraryRoundTextBoxNoTooltip then + errorState = errorState or self.props.ErrorBorder + end + + local size = self.props.Size or UDim2.new(1, self.props.WidthOffset or 0, 0, self.props.Height or DEFAULT_HEIGHT) + + local backgroundProps = { + -- Necessary to make the rounded background + BackgroundTransparency = 1, + Image = theme.roundFrame.backgroundImage, + ImageTransparency = 0, + ImageColor3 = active and theme.textBox.background or theme.textBox.disabled, + ScaleType = Enum.ScaleType.Slice, + SliceCenter = theme.roundFrame.slice, + + Position = UDim2.new(0, 0, 0, 0), + Size = size, + + LayoutOrder = self.props.LayoutOrder or 1, + } + + local showToolTip = true + if StudioUILibraryRoundTextBoxNoTooltip then + if nil ~= self.props.ShowToolTip then + showToolTip = self.props.ShowToolTip + end + end + + local tooltipText + if active then + if StudioUILibraryRoundTextBoxNoTooltip then + if showToolTip then + if errorState and self.props.ErrorMessage then + tooltipText = self.props.ErrorMessage + else + tooltipText = textLength .. "/" .. self.props.MaxLength + end + else + tooltipText = "" + end + else + if errorState and self.props.ErrorMessage then + tooltipText = self.props.ErrorMessage + else + tooltipText = textLength .. "/" .. self.props.MaxLength + end + end + else + tooltipText = "" + end + + local borderColor + if active then + if errorState then + borderColor = theme.textBox.error + elseif focused then + borderColor = theme.textBox.borderHover + else + borderColor = theme.textBox.borderDefault + end + else + borderColor = theme.textBox.borderDefault + end + + local textEntryProps = { + Visible = self.props.Active, + Text = self.props.Text, + PlaceholderText = self.props.PlaceholderText, + FocusChanged = self.focusChanged, + HoverChanged = self.mouseHoverChanged, + SetText = self.props.SetText, + TextColor3 = theme.textBox.text, + Font = theme.textBox.font, + TextSize = self.props.TextSize, + } + + local textEntry + if multiline then + textEntry = Roact.createElement(MultilineTextEntry, textEntryProps) + else + textEntry = Roact.createElement(TextEntry, textEntryProps) + end + + return Roact.createElement("ImageLabel", backgroundProps, { + Tooltip = Roact.createElement("TextLabel", { + BackgroundTransparency = 1, + Position = UDim2.new(0, 2, 1, 2), + Size = UDim2.new(1, 0, 0, 10), + + Font = Enum.Font.SourceSans, + TextSize = 16, + TextXAlignment = Enum.TextXAlignment.Left, + TextYAlignment = Enum.TextYAlignment.Top, + TextColor3 = (active and errorState and theme.textBox.error) or theme.textBox.tooltip, + Text = tooltipText, + Visible = (not StudioUILibraryRoundTextBoxNoTooltip) or showToolTip + }), + + Border = Roact.createElement("ImageLabel", { + Size = UDim2.new(1, 0, 1, 0), + BackgroundTransparency = 1, + Image = theme.roundFrame.borderImage, + ImageColor3 = borderColor, + ScaleType = Enum.ScaleType.Slice, + SliceCenter = theme.roundFrame.slice, + }, { + Padding = Roact.createElement("UIPadding", { + PaddingLeft = PADDING, + PaddingRight = PADDING, + PaddingTop = PADDING, + PaddingBottom = PADDING, + }), + Text = textEntry, + }), + }) + end) +end + +return RoundTextBox diff --git a/BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Components/RoundTextBox.spec.lua b/BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Components/RoundTextBox.spec.lua new file mode 100644 index 0000000000..356cfa3221 --- /dev/null +++ b/BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Components/RoundTextBox.spec.lua @@ -0,0 +1,71 @@ +return function() + local Library = script.Parent.Parent + local Roact = require(Library.Parent.Parent.Roact) + + local MockWrapper = require(Library.MockWrapper) + + local RoundTextBox = require(script.Parent.RoundTextBox) + + local function createTestRoundTextBox(active, errorMessage) + return Roact.createElement(MockWrapper, {}, { + RoundTextbox = Roact.createElement(RoundTextBox, { + Active = active, + MaxLength = 50, + Multiline = false, + Text = "Text", + ErrorMessage = errorMessage, + }), + }) + end + + it("should create and destroy without errors", function() + local element = createTestRoundTextBox(true) + local instance = Roact.mount(element) + Roact.unmount(instance) + end) + + it("should render correctly", function () + local container = Instance.new("Folder") + local instance = Roact.mount(createTestRoundTextBox(true), container) + local background = container:FindFirstChildOfClass("ImageLabel") + + expect(background.Border).to.be.ok() + expect(background.Border.Padding).to.be.ok() + expect(background.Border.Text).to.be.ok() + expect(background.Tooltip).to.be.ok() + + Roact.unmount(instance) + end) + + describe("Tooltip", function() + it("should show the correct length of the text", function () + local container = Instance.new("Folder") + local instance = Roact.mount(createTestRoundTextBox(true), container) + local background = container:FindFirstChildOfClass("ImageLabel") + + expect(background.Tooltip.Text).to.equal("4/50") + + Roact.unmount(instance) + end) + + it("should show an error message if one exists", function () + local container = Instance.new("Folder") + local instance = Roact.mount(createTestRoundTextBox(true, "Error"), container) + local background = container:FindFirstChildOfClass("ImageLabel") + + expect(background.Tooltip.Text).to.equal("Error") + + Roact.unmount(instance) + end) + + it("should be empty if component is inactive", function () + local container = Instance.new("Folder") + local instance = Roact.mount(createTestRoundTextBox(false), container) + local background = container:FindFirstChildOfClass("ImageLabel") + + expect(background.Tooltip.Text).to.equal("") + + Roact.unmount(instance) + end) + end) +end \ No newline at end of file diff --git a/BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Components/RoundTextButton.lua b/BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Components/RoundTextButton.lua new file mode 100644 index 0000000000..ccc20fade6 --- /dev/null +++ b/BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Components/RoundTextButton.lua @@ -0,0 +1,118 @@ +--[[ + A button with rounded corners. + + Supports one of two styles: + "Blue": A blue button with white text and no border. + "White": A white button with black text and a black border. + + Props: + bool Active = Whether or not this button can be clicked. + UDim2 Size = UDim2.new(0, Constants.BUTTON_WIDTH, 0, Constants.BUTTON_HEIGHT) + int LayoutOrder = The order this RoundTextButton will sort to when placed in a UIListLayout. + string Name = The text to display in this Button. + function OnClicked = The function that will be called when this button is clicked. + variant Value = Data that can be accessed from the OnClicked callback. + int TextSize = The size of text + table Style = { + ButtonColor, + ButtonColor_Hover, + ButtonColor_Disabled, + TextColor, + TextColor_Disabled, + BorderColor, + } +]] + +local Library = script.Parent.Parent +local Roact = require(Library.Parent.Parent.Roact) +local Theming = require(Library.Theming) +local withTheme = Theming.withTheme + +local BUTTON_WIDTH = 125 +local BUTTON_HEIGHT = 35 + +local RoundTextButton = Roact.PureComponent:extend("RoundTextButton") + +function RoundTextButton:init() + self.state = { + Hovering = false, + } + + self.mouseEnter = function() + self:setState({ + Hovering = true, + }) + end + + self.mouseLeave = function() + self:setState({ + Hovering = false, + }) + end +end + +function RoundTextButton:render() + return withTheme(function(theme) + local active = self.props.Active + local hovering = self.state.Hovering + local style = self.props.Style + local match = self.props.BorderMatchesBackground + local textSize = self.props.TextSize + + local backgroundProps = { + -- Necessary to make the rounded background + BackgroundTransparency = 1, + Image = theme.roundFrame.backgroundImage, + ImageTransparency = 0, + ScaleType = Enum.ScaleType.Slice, + SliceCenter = theme.roundFrame.slice, + + Position = self.props.Position or UDim2.new(0, 0, 0, 0), + Size = self.props.Size or UDim2.new(0, BUTTON_WIDTH, 0, BUTTON_HEIGHT), + AnchorPoint = self.props.AnchorPoint or Vector2.new(0, 0), + + LayoutOrder = self.props.LayoutOrder or 1, + ZIndex = self.props.ZIndex or 1, + + [Roact.Event.MouseEnter] = self.mouseEnter, + [Roact.Event.MouseLeave] = self.mouseLeave, + + [Roact.Event.Activated] = function() + if active then + self.props.OnClicked(self.props.Value) + end + end, + } + + if active then + backgroundProps.ImageColor3 = hovering and style.ButtonColor_Hover or style.ButtonColor + else + backgroundProps.ImageColor3 = style.ButtonColor_Disabled + end + + return Roact.createElement("ImageButton", backgroundProps, { + Border = Roact.createElement("ImageLabel", { + Size = UDim2.new(1, 0, 1, 0), + BackgroundTransparency = 1, + Image = theme.roundFrame.borderImage, + ImageColor3 = match and backgroundProps.ImageColor3 or style.BorderColor, + ScaleType = Enum.ScaleType.Slice, + SliceCenter = theme.roundFrame.slice, + ZIndex = self.props.ZIndex or 1, + }), + + Text = Roact.createElement("TextLabel", { + Size = UDim2.new(1, 0, 1, 0), + BackgroundTransparency = 1, + BorderSizePixel = 0, + Font = theme.textButton.font, + TextColor3 = active and style.TextColor or style.TextColor_Disabled, + TextSize = textSize, + Text = self.props.Name, + ZIndex = self.props.ZIndex or 1, + }), + }) + end) +end + +return RoundTextButton diff --git a/BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Components/RoundTextButton.spec.lua b/BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Components/RoundTextButton.spec.lua new file mode 100644 index 0000000000..cdd2496bb8 --- /dev/null +++ b/BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Components/RoundTextButton.spec.lua @@ -0,0 +1,35 @@ +return function() + local Library = script.Parent.Parent + local Roact = require(Library.Parent.Parent.Roact) + + local MockWrapper = require(Library.MockWrapper) + + local RoundTextButton = require(script.Parent.RoundTextButton) + + local function createTestRoundTextButton() + return Roact.createElement(MockWrapper, {}, { + RoundTextButton = Roact.createElement(RoundTextButton, { + Active = true, + Style = {}, + Name = "Name", + }), + }) + end + + it("should create and destroy without errors", function() + local element = createTestRoundTextButton() + local instance = Roact.mount(element) + Roact.unmount(instance) + end) + + it("should render correctly", function () + local container = Instance.new("Folder") + local instance = Roact.mount(createTestRoundTextButton(), container) + local button = container:FindFirstChildOfClass("ImageButton") + + expect(button.Border).to.be.ok() + expect(button.Text).to.be.ok() + + Roact.unmount(instance) + end) +end \ No newline at end of file diff --git a/BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Components/SearchBar.lua b/BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Components/SearchBar.lua new file mode 100644 index 0000000000..5f2673e5ce --- /dev/null +++ b/BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Components/SearchBar.lua @@ -0,0 +1,483 @@ +--[[ + Search Bar Component + + Implements a search bar component with a text box that dynamically moves as you type, and searches after a delay. + + Props: + UDim2 Size: size of the searchBar + number LayoutOrder = 0 : optional layout order for UI layouts + number TextSearchDelay : optional delay when text changes before requesting search, in ms + string DefaultText : default text to show in the empty search bar. + bool Enabled : searchbar is enabled or not + bool Rounded : searchbar has rounded corners + bool EnableFocus : if the searchbar borders becomes dark when it is selected + + callback OnSearchRequested(string searchTerm) : callback for when the user presses the enter key + or clicks the search button or types if search is live +]] +local FFlagAssetManagerLuaCleanup1 = settings():GetFFlag("AssetManagerLuaCleanup1") +local FFlagUXImprovementAddSearchBar = settings():GetFFlag("UXImprovementAddSearchBar") + +local Library = script.Parent.Parent +local Roact = require(Library.Parent.Parent.Roact) +local Theming = require(Library.Theming) +local withTheme = Theming.withTheme + +local LayoutOrderIterator = require(Library.Utils.LayoutOrderIterator) + +local TextService = game:GetService("TextService") + +local TEXT_SEARCH_DELAY = 500 + +local SearchBar = Roact.PureComponent:extend("SearchBar") + +local RoundFrame = require(Library.Components.RoundFrame) + +local function stripSearchTerm(searchTerm) + return searchTerm and searchTerm:gsub("\n", " ") or "" +end + +function SearchBar:init() + self.state = { + text = "", + + isFocused = false, + isContainerHovered = false, + isClearButtonHovered = false, + } + + self.textBoxRef = Roact.createRef() + + self.requestSearch = function() + if self.props.Enabled then + self.props.OnSearchRequested(self.state.text) + end + end + + self.onContainerHovered = function() + if self.props.Enabled then + self:setState({ + isContainerHovered = true, + }) + end + end + + self.onContainerHoverEnded = function() + if self.props.Enabled then + self:setState({ + isContainerHovered = false, + }) + end + end + + self.onTextChanged = function(rbx) + if self.props.Enabled then + local text = stripSearchTerm(rbx.Text) + local textBox = self.textBoxRef.current + if FFlagUXImprovementAddSearchBar then + if self.state.text ~= text and textBox ~= nil then + self:setState({ + text = text, + }) + + local textSearchDelay = self.props.TextSearchDelay or TEXT_SEARCH_DELAY + delay(textSearchDelay / 1000, function() + self.requestSearch() + end) + + local textBound = TextService:GetTextSize(text, textBox.TextSize, textBox.Font, Vector2.new(math.huge, math.huge)) + if textBound.x > textBox.AbsoluteSize.x then + textBox.TextXAlignment = Enum.TextXAlignment.Right + else + textBox.TextXAlignment = Enum.TextXAlignment.Left + end + end + else + if self.state.text ~= text then + self:setState({ + text = text, + }) + + local textSearchDelay = self.props.TextSearchDelay or TEXT_SEARCH_DELAY + delay(textSearchDelay / 1000, function() + self.requestSearch() + end) + + local textBound = TextService:GetTextSize(text, textBox.TextSize, textBox.Font, Vector2.new(math.huge, math.huge)) + if textBound.x > textBox.AbsoluteSize.x then + textBox.TextXAlignment = Enum.TextXAlignment.Right + else + textBox.TextXAlignment = Enum.TextXAlignment.Left + end + end + end + end + end + + self.onTextBoxFocused = function(rbx) + if self.props.Enabled then + self:setState({ + isFocused = true, + }) + end + end + + self.onTextBoxFocusLost = function(rbx, enterPressed, inputObject) + if self.props.Enabled then + self:setState({ + isFocused = false, + isContainerHovered = false, + }) + end + end + + self.onClearButtonHovered = function() + if self.props.Enabled then + self:setState({ + isClearButtonHovered = true, + }) + end + end + + self.onClearButtonHoverEnded = function() + if self.props.Enabled then + self:setState({ + isClearButtonHovered = false, + }) + end + end + + self.onClearButtonClicked = function() + if self.props.Enabled then + local textBox = self.textBoxRef.current + self:setState({ + isFocused = true, + isClearButtonHovered = false, + }) + + textBox.Text = "" + self.props.OnSearchRequested("") + textBox:CaptureFocus() + textBox.TextXAlignment = Enum.TextXAlignment.Left + end + end +end + +function SearchBar:render() + return withTheme(function(theme) + local props = self.props + local state = self.state + + local size = props.Size + local layoutOrder = props.LayoutOrder or 0 + local defaultText = props.DefaultText + local enabled = props.Enabled + local focusDisabled = props.FocusDisabled + + local onSearchRequested = props.OnSearchRequested + + local rounded = props.Rounded + + assert(size ~= nil, "Searchbar requires a size.") + assert(onSearchRequested ~= nil and type(onSearchRequested) == "function", + "Searchbar requires a OnSearchRequested function.") + + local text = state.text + + local isFocused = (FFlagUXImprovementAddSearchBar and not focusDisabled and state.isFocused) or (not FFlagUXImprovementAddSearchBar and state.isFocused) + local isContainerHovered = state.isContainerHovered + local isClearButtonHovered = state.isClearButtonHovered + + local showClearButton = #text > 0 + + --[[ + By default, TextBoxes let you keep typing infinitely and it will just go out of the bounds + (unless you set properties like ClipDescendants, TextWrapped) + Elsewhere, text boxes shift their contents to the left as you're typing past the bounds + So what you're typing is on the screen + + This is implemented here by: + - Set ClipsDescendants = true on the container + - Get the width of the container, subtracting any padding and the width of the button on the right + - Get the width of the text being rendered (this is calculated in the Roact.Change.Text event) + - If the text is shorter than the parent, then: + - Anchor the text label to the left side of the parent + - Set its width = container width + - Else + - Anchor the text label to the right side of the parent + - Sets its width = text width (with AnchorPoint = (1, 0), this grows to the left) + ]] + local searchBarTheme = theme.searchBar + + local buttonSize = searchBarTheme.buttons.size + + local textBoxOffset = #text > 0 and -buttonSize * 2 or -buttonSize + + local borderColor + if isFocused then + borderColor = searchBarTheme.border.selected.color + elseif isContainerHovered then + borderColor = searchBarTheme.border.hovered.color + else + borderColor = searchBarTheme.border.color + end + + local clearButtonImage = isClearButtonHovered and searchBarTheme.images.clear.hovered.image or searchBarTheme.images.clear.image + + local layoutIndex = LayoutOrderIterator.new() + + local Contents = Roact.createElement("Frame", { + Size = size, + BackgroundColor3 = searchBarTheme.backgroundColor, + BorderColor3 = borderColor, + BorderSizePixel = 1, + LayoutOrder = layoutOrder, + + [Roact.Event.MouseEnter] = self.onContainerHovered, + [Roact.Event.MouseLeave] = self.onContainerHoverEnded, + }, { + Background = Roact.createElement("Frame", { + Size = UDim2.new(1, 0, 1, 0), + BackgroundTransparency = 1, + BorderSizePixel = 0, + }, { + SearchBarLayout = Roact.createElement("UIListLayout", { + SortOrder = Enum.SortOrder.LayoutOrder, + Padding = UDim.new(0, 0), + FillDirection = Enum.FillDirection.Horizontal, + }), + + SearchImageFrame = Roact.createElement("Frame", { + BackgroundTransparency = 1, + Size = UDim2.new(0, buttonSize, 0, buttonSize), + LayoutOrder = layoutIndex:getNextOrder(), + } , { + SearchImage = Roact.createElement("ImageLabel", { + AnchorPoint = Vector2.new(0.5, 0.5), + Position = UDim2.new(0.5, 0, 0.5, 0), + Size = UDim2.new(0, searchBarTheme.buttons.iconSize, + 0, searchBarTheme.buttons.iconSize), + BackgroundTransparency = 1, + Image = searchBarTheme.images.search.image, + ImageColor3 = searchBarTheme.buttons.search.color, + }), + }), + + TextBox = Roact.createElement("TextBox", { + Size = UDim2.new(1, textBoxOffset, 0, buttonSize), + LayoutOrder = layoutIndex:getNextOrder(), + + BackgroundTransparency = 1, + ClipsDescendants = true, + + ClearTextOnFocus = false, + Font = searchBarTheme.font, + TextSize = searchBarTheme.textSize, + TextXAlignment = Enum.TextXAlignment.Left, + TextColor3 = searchBarTheme.text.color, + Text = text, + TextEditable = enabled, + + PlaceholderText = defaultText, + PlaceholderColor3 = searchBarTheme.text.placeholder.color, + + -- Get a reference to the text box so that clicking on the container can call :CaptureFocus() + [Roact.Ref] = self.textBoxRef, + + [Roact.Change.Text] = self.onTextChanged, + [Roact.Event.Focused] = self.onTextBoxFocused, + [Roact.Event.FocusLost] = self.onTextBoxFocusLost, + }), + + ClearButton = showClearButton and Roact.createElement("ImageButton", { + Size = UDim2.new(0, buttonSize, 0, buttonSize), + LayoutOrder = layoutIndex:getNextOrder(), + + BackgroundTransparency = 1, + + [Roact.Event.MouseEnter] = self.onClearButtonHovered, + [Roact.Event.MouseLeave] = self.onClearButtonHoverEnded, + [Roact.Event.MouseButton1Down] = self.onClearButtonClicked, + }, { + ClearImage = Roact.createElement("ImageLabel", { + AnchorPoint = Vector2.new(0.5, 0.5), + Position = UDim2.new(0.5, 0, 0.5, 0), + Size = UDim2.new(0, searchBarTheme.buttons.iconSize, + 0, searchBarTheme.buttons.iconSize), + BackgroundTransparency = 1, + Image = clearButtonImage, + ImageColor3 = searchBarTheme.buttons.clear.color, + }), + }), + }), + }) + + if FFlagAssetManagerLuaCleanup1 then + Contents = Roact.createElement("Frame", { + Size = size, + BackgroundColor3 = searchBarTheme.backgroundColor, + BorderColor3 = borderColor, + BorderSizePixel = 1, + LayoutOrder = layoutOrder, + + [Roact.Event.MouseEnter] = self.onContainerHovered, + [Roact.Event.MouseLeave] = self.onContainerHoverEnded, + }, { + SearchBarLayout = Roact.createElement("UIListLayout", { + SortOrder = Enum.SortOrder.LayoutOrder, + Padding = UDim.new(0, 0), + FillDirection = Enum.FillDirection.Horizontal, + }), + + SearchImageFrame = Roact.createElement("Frame", { + BackgroundTransparency = 1, + Size = UDim2.new(0, buttonSize, 0, buttonSize), + LayoutOrder = layoutIndex:getNextOrder(), + } , { + SearchImage = Roact.createElement("ImageLabel", { + AnchorPoint = Vector2.new(0.5, 0.5), + Position = UDim2.new(0.5, 0, 0.5, 0), + Size = UDim2.new(0, searchBarTheme.buttons.iconSize, + 0, searchBarTheme.buttons.iconSize), + BackgroundTransparency = 1, + Image = searchBarTheme.images.search.image, + ImageColor3 = searchBarTheme.buttons.search.color, + }), + }), + + TextBox = Roact.createElement("TextBox", { + Size = UDim2.new(1, textBoxOffset, 0, buttonSize), + LayoutOrder = layoutIndex:getNextOrder(), + + BackgroundTransparency = 1, + ClipsDescendants = true, + + ClearTextOnFocus = false, + Font = searchBarTheme.font, + TextSize = searchBarTheme.textSize, + TextXAlignment = Enum.TextXAlignment.Left, + TextColor3 = searchBarTheme.text.color, + Text = text, + TextEditable = enabled, + + PlaceholderText = defaultText, + PlaceholderColor3 = searchBarTheme.text.placeholder.color, + + -- Get a reference to the text box so that clicking on the container can call :CaptureFocus() + [Roact.Ref] = self.textBoxRef, + + [Roact.Change.Text] = self.onTextChanged, + [Roact.Event.Focused] = self.onTextBoxFocused, + [Roact.Event.FocusLost] = self.onTextBoxFocusLost, + }), + + ClearButton = showClearButton and Roact.createElement("ImageButton", { + Size = UDim2.new(0, buttonSize, 0, buttonSize), + LayoutOrder = layoutIndex:getNextOrder(), + + BackgroundTransparency = 1, + + [Roact.Event.MouseEnter] = self.onClearButtonHovered, + [Roact.Event.MouseLeave] = self.onClearButtonHoverEnded, + [Roact.Event.MouseButton1Down] = self.onClearButtonClicked, + }, { + ClearImage = Roact.createElement("ImageLabel", { + AnchorPoint = Vector2.new(0.5, 0.5), + Position = UDim2.new(0.5, 0, 0.5, 0), + Size = UDim2.new(0, searchBarTheme.buttons.iconSize, + 0, searchBarTheme.buttons.iconSize), + BackgroundTransparency = 1, + Image = clearButtonImage, + ImageColor3 = searchBarTheme.buttons.clear.color, + }), + }), + }) + end + + if FFlagUXImprovementAddSearchBar and FFlagAssetManagerLuaCleanup1 then + Contents = Roact.createElement(rounded and RoundFrame or "Frame", { + Size = size, + BackgroundColor3 = searchBarTheme.backgroundColor, + BorderColor3 = borderColor, + BorderSizePixel = 1, + LayoutOrder = layoutOrder, + + [Roact.Event.MouseEnter] = self.onContainerHovered, + [Roact.Event.MouseLeave] = self.onContainerHoverEnded, + }, { + SearchBarLayout = Roact.createElement("UIListLayout", { + SortOrder = Enum.SortOrder.LayoutOrder, + Padding = UDim.new(0, 0), + FillDirection = Enum.FillDirection.Horizontal, + }), + + SearchImageFrame = Roact.createElement("Frame", { + BackgroundTransparency = 1, + Size = UDim2.new(0, buttonSize, 1, 0), + LayoutOrder = layoutIndex:getNextOrder(), + } , { + SearchImage = Roact.createElement("ImageLabel", { + AnchorPoint = Vector2.new(0.5, 0.5), + Position = UDim2.new(0.5, 0, 0.5, 0), + Size = UDim2.new(0, searchBarTheme.buttons.iconSize, + 0, searchBarTheme.buttons.iconSize), + BackgroundTransparency = 1, + Image = searchBarTheme.images.search.image, + ImageColor3 = searchBarTheme.buttons.search.color, + }), + }), + + TextBox = Roact.createElement("TextBox", { + Size = UDim2.new(1, textBoxOffset, 1, 0), + LayoutOrder = layoutIndex:getNextOrder(), + + BackgroundTransparency = 1, + ClipsDescendants = true, + + ClearTextOnFocus = false, + Font = searchBarTheme.font, + TextSize = searchBarTheme.textSize, + TextXAlignment = Enum.TextXAlignment.Left, + TextColor3 = searchBarTheme.text.color, + Text = text, + TextEditable = enabled, + + PlaceholderText = defaultText, + PlaceholderColor3 = searchBarTheme.text.placeholder.color, + + -- Get a reference to the text box so that clicking on the container can call :CaptureFocus() + [Roact.Ref] = self.textBoxRef, + + [Roact.Change.Text] = self.onTextChanged, + [Roact.Event.Focused] = self.onTextBoxFocused, + [Roact.Event.FocusLost] = self.onTextBoxFocusLost, + }), + + ClearButton = showClearButton and Roact.createElement("ImageButton", { + Size = UDim2.new(0, buttonSize, 1, 0), + LayoutOrder = layoutIndex:getNextOrder(), + + BackgroundTransparency = 1, + + [Roact.Event.MouseEnter] = self.onClearButtonHovered, + [Roact.Event.MouseLeave] = self.onClearButtonHoverEnded, + [Roact.Event.MouseButton1Down] = self.onClearButtonClicked, + }, { + ClearImage = Roact.createElement("ImageLabel", { + AnchorPoint = Vector2.new(0.5, 0.5), + Position = UDim2.new(0.5, 0, 0.5, 0), + Size = UDim2.new(0, searchBarTheme.buttons.iconSize, + 0, searchBarTheme.buttons.iconSize), + BackgroundTransparency = 1, + Image = clearButtonImage, + ImageColor3 = searchBarTheme.buttons.clear.color, + }), + }), + }) + end + + return Contents + end) +end + +return SearchBar diff --git a/BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Components/SearchBar.spec.lua b/BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Components/SearchBar.spec.lua new file mode 100644 index 0000000000..eb00ad0348 --- /dev/null +++ b/BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Components/SearchBar.spec.lua @@ -0,0 +1,59 @@ +local FFlagAssetManagerLuaCleanup1 = settings():GetFFlag("AssetManagerLuaCleanup1") + +return function() + local Library = script.Parent.Parent + local Roact = require(Library.Parent.Parent.Roact) + + local MockWrapper = require(Library.MockWrapper) + + local SearchBar = require(script.Parent.SearchBar) + + it("should create and destroy without errors", function() + local element = Roact.createElement(MockWrapper, {}, { + SearchBar = Roact.createElement(SearchBar, { + Size = UDim2.new(0, 100, 0, 20), + OnSearchRequested = function() end, + }), + }) + local instance = Roact.mount(element) + Roact.unmount(instance) + end) + + describe("the textbox", function() + it("should move as text is typed", function() + local width = 200 + local element = Roact.createElement(MockWrapper, {}, { + SearchBar = Roact.createElement(SearchBar, { + Size = UDim2.new(0, width, 0, 20), + Enabled = true, + OnSearchRequested = function() end, + }), + }) + local container = Instance.new("Folder") + local instance = Roact.mount(element, container, "SearchBar") + local searchBar = container.SearchBar + local textBox + if FFlagAssetManagerLuaCleanup1 then + textBox =searchBar.TextBox + else + textBox = searchBar.Background.TextBox + end + + local str = ("abcdefghijklmnopqrstuvwxyz"):rep(2) + + textBox.Text = str:sub(1, 1) + local previousWidth = textBox.AbsoluteSize.x + + for i = 1, #str, 1 do + local text = str:sub(1, i) + textBox.Text = text + + local width = textBox.AbsoluteSize.x + expect(width >= previousWidth).to.equal(true) + previousWidth = width + end + + Roact.unmount(instance) + end) + end) +end diff --git a/BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Components/Separator.lua b/BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Components/Separator.lua new file mode 100644 index 0000000000..f0ecae653d --- /dev/null +++ b/BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Components/Separator.lua @@ -0,0 +1,51 @@ +--[[ + A simple border to separate elements. + + Props: + Enum DominantAxis = Specifies whether the separator fills the + space horizontally or vertically. Width will make the separator + fill the horizontal space, and Height will make the separator + fill the vertical space. + Weight = The thickness of the separator line. + Padding = The padding in pixels to subtract from either side of + the separator's dominant axis. + + Position = The position of the center of the separator. + LayoutOrder = The order in which the separator appears in a UILayout. + ZIndex = The render order of the separator. +]] + +local Library = script.Parent.Parent +local Roact = require(Library.Parent.Parent.Roact) +local Theming = require(Library.Theming) +local withTheme = Theming.withTheme + +local function Separator(props) + local dominantAxis = props.DominantAxis or Enum.DominantAxis.Width + local position = props.Position + local weight = props.Weight or 1 + local padding = props.Padding or 0 + local zIndex = props.ZIndex + local layoutOrder = props.LayoutOrder + + local size + if dominantAxis == Enum.DominantAxis.Width then + size = UDim2.new(1, -padding * 2, 0, weight) + else + size = UDim2.new(0, weight, 1, -padding * 2) + end + + return withTheme(function(theme) + return Roact.createElement("Frame", { + Size = size, + Position = position, + AnchorPoint = Vector2.new(0.5, 0.5), + BackgroundColor3 = theme.separator.lineColor, + BorderSizePixel = 0, + ZIndex = zIndex, + LayoutOrder = layoutOrder, + }) + end) +end + +return Separator \ No newline at end of file diff --git a/BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Components/Separator.spec.lua b/BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Components/Separator.spec.lua new file mode 100644 index 0000000000..300dda2c74 --- /dev/null +++ b/BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Components/Separator.spec.lua @@ -0,0 +1,30 @@ +return function() + local Library = script.Parent.Parent + local Roact = require(Library.Parent.Parent.Roact) + + local MockWrapper = require(Library.MockWrapper) + + local Separator = require(script.Parent.Separator) + + local function createTestSeparator(props) + return Roact.createElement(MockWrapper, {}, { + Separator = Roact.createElement(Separator, props) + }) + end + + it("should create and destroy without errors", function() + local element = createTestSeparator() + local instance = Roact.mount(element) + Roact.unmount(instance) + end) + + it("should render correctly", function () + local container = Instance.new("Folder") + local instance = Roact.mount(createTestSeparator(), container) + local separator = container:FindFirstChildOfClass("Frame") + + expect(separator).to.be.ok() + + Roact.unmount(instance) + end) +end \ No newline at end of file diff --git a/BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Components/StyledDialog.lua b/BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Components/StyledDialog.lua new file mode 100644 index 0000000000..200abb3ce9 --- /dev/null +++ b/BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Components/StyledDialog.lua @@ -0,0 +1,105 @@ +--[[ + An implementation of BaseDialog that adds UILibrary Buttons to the bottom. + To use the component, the consumer supplies an array of buttons, optionally + defining a Style for each button if it should display differently. + + Props: + array Buttons = An array of items used to render the buttons for this dialog. + { + {Key = "Cancel", Text = "SomeLocalizedTextForCancel"}, + {Key = "Save", Text = "SomeLocalizedTextForSave", Style = "Primary"}, + } + function OnButtonClicked(key) = A callback for when the user clicked + a button in the dialog. Accepts the Key of the button that was clicked. + function OnClose = A callback for when the user closed the dialog by + clicking the X in the corner of the window. + + Vector2 Size = The starting size of the dialog. + Vector2 MinSize = The minimum size of the dialog, if it is resizable. + bool Resizable = Whether the dialog can be resized. + int BorderPadding = The padding to add around the edges of the dialog. + int ButtonPadding = The padding to add between buttons. + int ButtonHeight = The height of the buttons in the dialog, in pixels. + int ButtonWidth = The width of each button in the dialog, in pixels. + string Title = The title to display at the top of the window. +]] + +local Library = script.Parent.Parent +local Roact = require(Library.Parent.Parent.Roact) + +local Theming = require(Library.Theming) +local withTheme = Theming.withTheme + +local BaseDialog = require(Library.Components.BaseDialog) +local Button = require(Library.Components.Button) + +local StyledDialog = Roact.PureComponent:extend("StyledDialog") + +function StyledDialog:init() + self.enabledChanged = function(enabled) + if not enabled and self.props.OnClose then + self.props.OnClose() + end + end + + self.buttonClicked = function(button) + if self.props.OnButtonClicked then + self.props.OnButtonClicked(button.Key) + end + end +end + +function StyledDialog:render() + return withTheme(function(theme) + local props = self.props + local title = props.Title + local size = props.Size + local minSize = props.MinSize + local resizable = props.Resizable + local borderPadding = props.BorderPadding + local textSize = props.TextSize + + local buttons = props.Buttons + local buttonPadding = props.ButtonPadding + local buttonHeight = props.ButtonHeight + local buttonWidth = props.ButtonWidth + + return Roact.createElement(BaseDialog, { + Title = title, + Size = size, + MinSize = minSize, + Resizable = resizable, + Buttons = buttons, + ButtonHeight = buttonHeight, + BorderPadding = borderPadding, + ButtonPadding = buttonPadding, + + RenderButton = function(button, index, activated) + return Roact.createElement(Button, { + Size = UDim2.new(0, buttonWidth, 0, buttonHeight), + LayoutOrder = index, + Style = button.Style, + + OnClick = activated, + RenderContents = function(buttonTheme) + return { + Text = Roact.createElement("TextLabel", { + Size = UDim2.new(1, 0, 1, 0), + BackgroundTransparency = 1, + Font = buttonTheme.font, + Text = button.Text, + TextSize = textSize, + TextColor3 = buttonTheme.textColor, + }) + } + end, + }) + end, + + OnButtonClicked = self.buttonClicked, + OnClose = props.OnClose, + }, self.props[Roact.Children]) + end) +end + +return StyledDialog diff --git a/BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Components/StyledDialog.spec.lua b/BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Components/StyledDialog.spec.lua new file mode 100644 index 0000000000..4a4289e0ac --- /dev/null +++ b/BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Components/StyledDialog.spec.lua @@ -0,0 +1,94 @@ +return function() + local Library = script.Parent.Parent + local Roact = require(Library.Parent.Parent.Roact) + + local MockWrapper = require(Library.MockWrapper) + + local StyledDialog = require(script.Parent.StyledDialog) + + local function createTestStyledDialog(props, children, container) + return Roact.createElement(MockWrapper, { + Container = container, + }, { + StyledDialog = Roact.createElement(StyledDialog, props, children) + }) + end + + it("should create and destroy without errors", function() + local element = createTestStyledDialog({ + Buttons = {}, + }) + local instance = Roact.mount(element) + Roact.unmount(instance) + end) + + it("should render correctly", function() + local container = Instance.new("Folder") + + local element = createTestStyledDialog({ + Buttons = {}, + }, {}, container) + + local instance = Roact.mount(element, container) + + local gui = container:FindFirstChildOfClass("BillboardGui") + expect(gui).to.be.ok() + expect(gui.FocusProvider).to.be.ok() + expect(gui.FocusProvider.Padding).to.be.ok() + expect(gui.FocusProvider.Content).to.be.ok() + expect(gui.FocusProvider.Buttons).to.be.ok() + + Roact.unmount(instance) + end) + + it("should require a Buttons table", function() + local element = createTestStyledDialog() + expect(function() + Roact.mount(element) + end).to.throw() + + element = createTestStyledDialog({ + Buttons = true, + }) + expect(function() + Roact.mount(element) + end).to.throw() + end) + + it("should create a Button for each button", function() + local container = Instance.new("Folder") + + local element = createTestStyledDialog({ + Buttons = { + {Key = "Test", Text = "TestText"}, + {Key = "Test2", Text = "TestText2"}, + }, + }, {}, container) + + local instance = Roact.mount(element, container) + + local gui = container:FindFirstChildOfClass("BillboardGui") + expect(gui.FocusProvider.Buttons).to.be.ok() + expect(gui.FocusProvider.Buttons[1]).to.be.ok() + expect(gui.FocusProvider.Buttons[2]).to.be.ok() + + Roact.unmount(instance) + end) + + it("should render its children", function() + local container = Instance.new("Folder") + + local element = createTestStyledDialog({ + Buttons = {}, + }, { + Frame = Roact.createElement("Frame"), + }, container) + + local instance = Roact.mount(element, container) + + local gui = container:FindFirstChildOfClass("BillboardGui") + expect(gui.FocusProvider.Content.Frame).to.be.ok() + + Roact.unmount(instance) + end) +end \ No newline at end of file diff --git a/BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Components/StyledDropdown.lua b/BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Components/StyledDropdown.lua new file mode 100644 index 0000000000..56830dda86 --- /dev/null +++ b/BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Components/StyledDropdown.lua @@ -0,0 +1,281 @@ +--[[ + A dropdown menu styled to match the Roblox Studio start page. + Consists of a button used to open the dropdown as well as the menu itself. + Note that the logic for opening and closing the menu is contained within this component, + but the consumer is responsible for showing the current value in the button. + + Required Props: + UDim2 Size = The size of the button that opens the dropdown. + UDim2 Position = The position of the button that opens the dropdown. + int TextSize = The size of the text in the dropdown and button. + int ItemHeight = The height of each entry in the dropdown, in pixels. + string ButtonText = The text to display in the button that opens the dropdown. + Usually should be set to the currently selected dropdown entry. + array Items = An ordered array of each item that should appear in the dropdown. + The array is formatted like this: + { + {Key = "Item1", Text = "SomeLocalizedTextForItem1"}, + {Key = "Item2", Text = "SomeLocalizedTextForItem2"}, + {Key = "Item3", Text = "SomeLocalizedTextForItem3"}, + } + Key is how the item will be referenced in code. Text is what will appear to the user. + function OnItemClicked(item) = A callback when the user selects an item in the dropdown. + Returns the item as it was defined in the Items array. + + Optional Props: + int MaxItems = The maximum number of entries that can display at a time. + If this is less than the number of entries in the dropdown, a scrollbar will appear. + bool ShowRibbon = Whether to show a colored ribbon next to the currently + hovered dropdown entry. Usually should be enabled for Light theme only. + int TextPadding = The amount of padding, in pixels, around the text elements. + int IconSize = The size of the arrow icon in the button. + int IconPadding = The distance from the right side of the arrow icon to the button edge. + int ScrollBarPadding = The padding which appears on either side of the scrollbar. + int ScrollBarThickness = The horizontal width of the scrollbar. +]] +local FFlagStudioFixUILibDropdownStyle = game:GetFastFlag("StudioFixUILibDropdownStyle") +local FFlagStudioFixUILibDropdownText = game:GetFastFlag("StudioFixUILibDropdownText") + +-- Defaults +local TEXT_PADDING = 8 +local ICON_SIZE = 12 +local ICON_PADDING = 4 + +local RIBBON_WIDTH = 5 +local VERTICAL_OFFSET = 2 + +local TextService = game:GetService("TextService") + +local Library = script.Parent.Parent +local Roact = require(Library.Parent.Parent.Roact) + +local Theming = require(Library.Theming) +local withTheme = Theming.withTheme + +local DropdownMenu = require(Library.Components.DropdownMenu) +local RoundFrame = require(Library.Components.RoundFrame) + +local StyledDropdown = Roact.PureComponent:extend("StyledDropdown") + +function StyledDropdown:init() + self.state = { + showDropdown = false, + isButtonHovered = false, + hoveredKey = nil, + } + self.buttonRef = Roact.createRef() + + self.onItemClicked = function(key) + self.props.OnItemClicked(key) + self.hideDropdown() + end + + self.showDropdown = function() + self:setState({ + showDropdown = true, + }) + end + + self.hideDropdown = function() + self:setState({ + showDropdown = false, + }) + end + + self.onKeyMouseEnter = function(key) + self:setState({ + hoveredKey = key, + }) + end + + self.onKeyMouseLeave = function(key) + if self.state.hoveredKey == key then + self:setState({ + hoveredKey = Roact.None, + }) + end + end + + self.onMouseEnter = function() + self:setState({ + isButtonHovered = true, + }) + end + + self.onMouseLeave = function() + self:setState({ + isButtonHovered = false, + }) + end +end + +function StyledDropdown:createLabel(key, displayText, textSize, textPadding, font, textColor) + return Roact.createElement("TextLabel", { + Size = UDim2.new(1, 0, 1, 0), + Font = font, + TextSize = textSize, + Text = displayText, + TextXAlignment = Enum.TextXAlignment.Left, + TextColor3 = textColor, + BackgroundTransparency = 1, + + [Roact.Event.MouseEnter] = function() + self.onKeyMouseEnter(key) + end, + [Roact.Event.MouseLeave] = function() + self.onKeyMouseLeave(key) + end, + }, { + Padding = Roact.createElement("UIPadding", { + PaddingLeft = UDim.new(0, textPadding), + }), + }) +end + +function StyledDropdown:render() + return withTheme(function(theme) + local props = self.props + local state = self.state + local dropdownTheme = theme.styledDropdown + local listTheme = dropdownTheme.listTheme or dropdownTheme + local showDropdown = state.showDropdown + local buttonRef = self.buttonRef and self.buttonRef.current + local buttonExtents + if buttonRef then + local buttonMin = buttonRef.AbsolutePosition + local buttonSize = buttonRef.AbsoluteSize + local buttonMax = buttonMin + buttonSize + buttonExtents = Rect.new(buttonMin.X, buttonMin.Y, buttonMax.X, buttonMax.Y) + end + local listWidth = props.ListWidth or 0 + local items = props.Items or {} + local size = props.Size + local position = props.Position + local textSize = props.TextSize + local itemHeight = props.ItemHeight + local maxItems = props.MaxItems + local showRibbon = props.ShowRibbon + + local textPadding = props.TextPadding or TEXT_PADDING + local iconSize = props.IconSize or ICON_SIZE + local iconPadding = props.IconPadding or ICON_PADDING + local scrollBarPadding = props.ScrollBarPadding + local scrollBarThickness = props.ScrollBarThickness + + local hoveredKey = state.hoveredKey + local selectedItem = props.SelectedItem + local isButtonHovered = state.isButtonHovered + local buttonText = props.ButtonText + + local maxWidth = 0 + local maxHeight = maxItems and (maxItems * itemHeight) or nil + local LayoutOrder = props.LayoutOrder or 0 + + for _, data in ipairs(items) do + local textBound = TextService:GetTextSize(data.Text, + textSize, dropdownTheme.font, Vector2.new(9000, 100)) + + local itemWidth = textBound.X + textPadding * 2 + maxWidth = math.max(maxWidth, itemWidth) + end + + if FFlagStudioFixUILibDropdownStyle then + maxWidth = math.max(maxWidth, listWidth) + end + + local buttonTheme = (showDropdown or isButtonHovered) and dropdownTheme.selected + or dropdownTheme + + return Roact.createElement("ImageButton", { + Size = size, + Position = position, + BackgroundTransparency = 1, + ImageTransparency = 1, + + [Roact.Ref] = self.buttonRef, + + [Roact.Event.Activated] = self.showDropdown, + [Roact.Event.MouseEnter] = self.onMouseEnter, + [Roact.Event.MouseLeave] = self.onMouseLeave, + + LayoutOrder = LayoutOrder, + }, { + RoundFrame = Roact.createElement(RoundFrame, { + Size = UDim2.new(1, 0, 1, 0), + BackgroundColor3 = buttonTheme.backgroundColor, + BorderColor3 = buttonTheme.borderColor, + }), + + ArrowIcon = Roact.createElement("ImageLabel", { + Size = UDim2.new(0, iconSize, 0, iconSize), + Position = UDim2.new(1, -iconPadding, 0.5, 0), + AnchorPoint = Vector2.new(1, 0.5), + BackgroundTransparency = 1, + ImageColor3 = buttonTheme.textColor, + Image = dropdownTheme.arrowImage, + }), + + TextLabel = Roact.createElement("TextLabel", { + Size = UDim2.new(1, FFlagStudioFixUILibDropdownText and -iconSize or 0, 1, 0), + BackgroundTransparency = 1, + Font = dropdownTheme.font, + TextColor3 = buttonTheme.textColor, + TextSize = textSize, + Text = buttonText, + TextXAlignment = Enum.TextXAlignment.Left, + TextTruncate = FFlagStudioFixUILibDropdownText and Enum.TextTruncate.AtEnd or nil, + }, { + Padding = Roact.createElement("UIPadding", { + PaddingLeft = UDim.new(0, textPadding), + }), + }), + + Dropdown = showDropdown and buttonRef and Roact.createElement(DropdownMenu, { + OnItemClicked = self.onItemClicked, + OnFocusLost = self.hideDropdown, + SourceExtents = buttonExtents, + Offset = Vector2.new(0, VERTICAL_OFFSET), + MaxHeight = maxHeight, + ShowBorder = true, + ScrollBarPadding = scrollBarPadding, + ScrollBarThickness = scrollBarThickness, + ListWidth = FFlagStudioFixUILibDropdownStyle and maxWidth or listWidth, + Items = items, + RenderItem = function(item, index, activated) + local key = item.Key + local selected = key == selectedItem + local displayText = item.Text + local isHovered = hoveredKey == key + local textColor = (selected or isHovered) and dropdownTheme.hovered.textColor + or dropdownTheme.textColor + local itemColor = listTheme.backgroundColor + if selected then + itemColor = listTheme.selected.backgroundColor + elseif isHovered then + itemColor = listTheme.hovered.backgroundColor + end + + return Roact.createElement("ImageButton", { + Size = UDim2.new(0, FFlagStudioFixUILibDropdownStyle and maxWidth or math.max(listWidth, maxWidth), 0, itemHeight), + BackgroundColor3 = itemColor, + BorderSizePixel = 0, + LayoutOrder = index, + AutoButtonColor = false, + [Roact.Event.Activated] = activated, + }, { + Ribbon = isHovered and showRibbon and Roact.createElement("Frame", { + Size = UDim2.new(0, RIBBON_WIDTH, 1, 0), + BackgroundColor3 = listTheme.selected.backgroundColor, + BorderSizePixel = 0, + }), + + Label = self:createLabel(key, displayText, textSize, + textPadding, dropdownTheme.font, textColor), + }) + end, + }) + }) + end) +end + +return StyledDropdown diff --git a/BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Components/StyledDropdown.spec.lua b/BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Components/StyledDropdown.spec.lua new file mode 100644 index 0000000000..2e01d21f4d --- /dev/null +++ b/BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Components/StyledDropdown.spec.lua @@ -0,0 +1,34 @@ +return function() + local Library = script.Parent.Parent + local Roact = require(Library.Parent.Parent.Roact) + + local MockWrapper = require(Library.MockWrapper) + + local StyledDropdown = require(script.Parent.StyledDropdown) + + local function createTestStyledDropdown(props, children) + return Roact.createElement(MockWrapper, {}, { + StyledDropdown = Roact.createElement(StyledDropdown, props, children) + }) + end + + it("should create and destroy without errors", function() + local element = createTestStyledDropdown() + local instance = Roact.mount(element) + Roact.unmount(instance) + end) + + it("should render correctly", function() + local container = Instance.new("Folder") + local instance = Roact.mount(createTestStyledDropdown(), container) + local button = container:FindFirstChildOfClass("ImageButton") + + expect(button).to.be.ok() + expect(button.RoundFrame).to.be.ok() + expect(button.ArrowIcon).to.be.ok() + expect(button.TextLabel).to.be.ok() + expect(button.TextLabel.Padding).to.be.ok() + + Roact.unmount(instance) + end) +end \ No newline at end of file diff --git a/BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Components/StyledScrollingFrame.lua b/BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Components/StyledScrollingFrame.lua new file mode 100644 index 0000000000..7e54ef796e --- /dev/null +++ b/BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Components/StyledScrollingFrame.lua @@ -0,0 +1,98 @@ +--[[ + A scrolling frame with a colored background, providing a consistent look + with the Studio native start page. + + Props: + UDim2 Position = The position of the scrolling frame. + UDim2 Size = The size of the scrolling frame. + UDim2 CanvasSize = The size of the scrolling frame's canvas. + + int LayoutOrder = The order this component will display in a UILayout. + int ZIndex = The draw index of the frame. + + bool ScrollingEnabled = Whether scrolling in this frame will change the CanvasPosition. + int ScrollBarPadding = The padding which appears on either side of the scrollbar. + int ScrollBarThickness = The horizontal width of the scrollbar. + + function OnScroll(Vector2 CanvasPosition) = A callback for when the CanvasPosition changes. +]] + +local DEFAULT_SCROLLBAR_THICKNESS = 8 +local DEFAULT_SCROLLBAR_PADDING = 2 + +local Library = script.Parent.Parent +local Roact = require(Library.Parent.Parent.Roact) + +local Theming = require(Library.Theming) +local withTheme = Theming.withTheme + +local StyledScrollingFrame = Roact.PureComponent:extend("StyledScrollingFrame") + +function StyledScrollingFrame:init() + self.onScroll = function(rbx) + if self.props.OnScroll then + self.props.OnScroll(rbx.CanvasPosition) + end + end +end + +function StyledScrollingFrame:render() + return withTheme(function(theme) + local props = self.props + local scrollTheme = theme.scrollingFrame + + local position = props.Position + local size = props.Size + local canvasSize = props.CanvasSize + local layoutOrder = props.LayoutOrder + local zindex = props.ZIndex + local scrollingEnabled = props.ScrollingEnabled + local padding = props.ScrollBarPadding or DEFAULT_SCROLLBAR_PADDING + local scrollBarThickness = props.ScrollBarThickness or DEFAULT_SCROLLBAR_THICKNESS + + local backgroundThickness = scrollBarThickness + (padding * 2) + + local ref = props[Roact.Ref] + local children = props[Roact.Children] + + return Roact.createElement("Frame", { + Position = position, + Size = size, + LayoutOrder = layoutOrder, + ZIndex = zindex, + BackgroundTransparency = 1, + }, { + ScrollBarBackground = Roact.createElement("Frame", { + Position = UDim2.new(1, 0, 0, 0), + Size = UDim2.new(0, backgroundThickness, 1, 0), + AnchorPoint = Vector2.new(1, 0), + BorderSizePixel = 0, + BackgroundColor3 = scrollTheme.backgroundColor, + ZIndex = 2, + }), + + ScrollingFrame = Roact.createElement("ScrollingFrame", { + Size = UDim2.new(1, -padding, 1, 0), + CanvasSize = canvasSize, + BackgroundTransparency = 1, + BorderSizePixel = 0, + ScrollBarThickness = scrollBarThickness, + ZIndex = 2, + + TopImage = scrollTheme.topImage, + MidImage = scrollTheme.midImage, + BottomImage = scrollTheme.bottomImage, + + ScrollBarImageColor3 = scrollTheme.scrollbarColor, + + ScrollingEnabled = scrollingEnabled, + ScrollingDirection = Enum.ScrollingDirection.Y, + + [Roact.Change.CanvasPosition] = self.onScroll, + [Roact.Ref] = ref, + }, children), + }) + end) +end + +return StyledScrollingFrame diff --git a/BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Components/StyledScrollingFrame.spec.lua b/BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Components/StyledScrollingFrame.spec.lua new file mode 100644 index 0000000000..6ecb742b68 --- /dev/null +++ b/BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Components/StyledScrollingFrame.spec.lua @@ -0,0 +1,62 @@ +return function() + local Library = script.Parent.Parent + local Roact = require(Library.Parent.Parent.Roact) + + local MockWrapper = require(Library.MockWrapper) + + local StyledScrollingFrame = require(script.Parent.StyledScrollingFrame) + + local function createTestScrollingFrame(props, children) + return Roact.createElement(MockWrapper, {}, { + ScrollingFrame = Roact.createElement(StyledScrollingFrame, props, children) + }) + end + + it("should create and destroy without errors", function() + local element = createTestScrollingFrame() + local instance = Roact.mount(element) + Roact.unmount(instance) + end) + + it("should render correctly", function() + local container = Instance.new("Folder") + local instance = Roact.mount(createTestScrollingFrame(), container) + local frame = container:FindFirstChildOfClass("Frame") + + expect(frame).to.be.ok() + expect(frame.ScrollBarBackground).to.be.ok() + expect(frame.ScrollingFrame).to.be.ok() + + Roact.unmount(instance) + end) + + it("should render its children in the ScrollingFrame", function() + local container = Instance.new("Folder") + local instance = Roact.mount(createTestScrollingFrame({}, { + ChildFrame = Roact.createElement("Frame"), + }), container) + local frame = container:FindFirstChildOfClass("Frame") + + expect(frame).to.be.ok() + expect(frame.ScrollBarBackground).to.be.ok() + expect(frame.ScrollingFrame).to.be.ok() + expect(frame.ScrollingFrame.ChildFrame).to.be.ok() + + Roact.unmount(instance) + end) + + it("should add padding to both sides of the ScrollBar", function() + local container = Instance.new("Folder") + local instance = Roact.mount(createTestScrollingFrame({ + ScrollBarPadding = 2, + ScrollBarThickness = 8, + }), container) + local frame = container:FindFirstChildOfClass("Frame") + + expect(frame).to.be.ok() + expect(frame.ScrollBarBackground).to.be.ok() + expect(frame.ScrollBarBackground.Size.X.Offset).to.equal(12) + + Roact.unmount(instance) + end) +end \ No newline at end of file diff --git a/BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Components/StyledTooltip.lua b/BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Components/StyledTooltip.lua new file mode 100644 index 0000000000..91bffdcb3e --- /dev/null +++ b/BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Components/StyledTooltip.lua @@ -0,0 +1,196 @@ +--[[ + An element which can be added to a component to give that component a tooltip. + When the user hovers the mouse over the component, the tooltip will appear + after a short delay. + + Required Props: + table Elements = The table containing roact elements to display in the tooltip. + Vector2 TooltipExtents = vector containing tooltip size + bool Enabled = Whether the tooltip will display on hover. + + Optional Props: + float ShowDelay = The time in seconds before the tooltip appears + after the user stops moving the mouse over the element. Defaults to 0.5. + int Priority = The display order of this element, compared to other focused + elements or elements that show on top. +]] + +local SHOW_DELAY_DEFAULT = 0.5 + +local RunService = game:GetService("RunService") + +local Library = script.Parent.Parent +local Roact = require(Library.Parent.Parent.Roact) + +local Theming = require(Library.Theming) +local withTheme = Theming.withTheme + +local Focus = require(Library.Focus) +local ShowOnTop = Focus.ShowOnTop +local withFocus = Focus.withFocus + +local DropShadow = require(Library.Components.DropShadow) + +local Tooltip = Roact.PureComponent:extend("Tooltip") + +function Tooltip:init(props) + self.state = { + showToolTip = false, + } + + self.isElementHovered = false + self.isTooltipHovered = false + self.mousePos = nil + + self.connectHover = function() + self.hoverConnection = RunService.Heartbeat:Connect(function() + if self.isElementHovered or self.isTooltipHovered then + if tick() >= self.targetTime then + self.disconnectHover() + self:setState({ + showToolTip = true, + }) + end + end + end) + end + + self.disconnectHover = function() + if self.hoverConnection then + self.hoverConnection:Disconnect() + end + end + + self.elementMouseEnter = function(rbx, xpos, ypos) + local showDelay = props.ShowDelay or SHOW_DELAY_DEFAULT + self.isElementHovered = true + self.targetTime = tick() + showDelay + if not self.mousePos then + self.mousePos = Vector2.new(xpos, ypos) + end + self.connectHover() + end + + self.elementMouseMoved = function(rbx, xpos, ypos) + local showDelay = props.ShowDelay or SHOW_DELAY_DEFAULT + self.targetTime = tick() + showDelay + end + + self.elementMouseLeave = function() + self.isElementHovered = false + local hovered = self.isElementHovered or self.isTooltipHovered + self:setState({ + showToolTip = hovered, + }) + if not hovered then + self.targetTime = 0 + self.mousePos = nil + self.disconnectHover() + end + end + + self.tooltipMouseEnter = function(rbx, xpos, ypos) + self.isTooltipHovered = true + end + + self.tooltipMouseMoved = function(rbx, xpos, ypos) + local showDelay = props.ShowDelay or SHOW_DELAY_DEFAULT + self.targetTime = tick() + showDelay + end + + self.tooltipMouseLeave = function() + self.isTooltipHovered = false + local hovered = self.isElementHovered or self.isTooltipHovered + self:setState({ + showToolTip = hovered, + }) + if not hovered then + self.targetTime = 0 + self.mousePos = nil + self.disconnectHover() + end + end +end + +function Tooltip:willUnmount() + self.disconnectHover() +end + +function Tooltip:render() + return withTheme(function(theme) + return withFocus(function(pluginGui) + local props = self.props + local state = self.state + + local styledTooltipTheme = theme.styledTooltip + + local enabled = props.Enabled + local priority = props.Priority or 0 + + local mousePos = self.mousePos + + local elements = props.Elements + local tooltipWidth = props.TooltipExtents and props.TooltipExtents.X + local tooltipHeight = props.TooltipExtents and props.TooltipExtents.Y + + local content = {} + + if state.showToolTip and mousePos and enabled and pluginGui then + local targetX = mousePos.X + local targetY = mousePos.Y + + local targetWidth = pluginGui.AbsoluteSize.X + local targetHeight = pluginGui.AbsoluteSize.Y + + + if targetX + tooltipWidth >= targetWidth then + targetX = targetWidth - tooltipWidth + end + + if targetY + tooltipHeight >= targetHeight then + targetY = targetHeight - tooltipHeight + end + + content.TooltipContainer = Roact.createElement(ShowOnTop, { + Priority = priority, + }, { + Tooltip = Roact.createElement("Frame", { + Position = UDim2.new(0, targetX, 0, targetY), + Size = UDim2.new(0, tooltipWidth, 0, tooltipHeight), + BackgroundTransparency = 1, + BorderSizePixel = 0, + + [Roact.Event.MouseEnter] = self.tooltipMouseEnter, + [Roact.Event.MouseMoved] = self.tooltipMouseMoved, + [Roact.Event.MouseLeave] = self.tooltipMouseLeave, + }, { + DropShadow = Roact.createElement(DropShadow, { + Transparency = styledTooltipTheme.shadowTransparency, + Color = styledTooltipTheme.shadowColor, + Offset = styledTooltipTheme.shadowOffset, + ZIndex = 1, + }), + + ContentFrame = Roact.createElement("Frame", { + Size = UDim2.new(1, 0, 1, 0), + BackgroundColor3 = styledTooltipTheme.backgroundColor, + BorderSizePixel = 0, + ZIndex = 2, + }, elements), + }) + }) + end + + return Roact.createElement("Frame",{ + Size = UDim2.new(1, 0, 1, 0), + BackgroundTransparency = 1, + + [Roact.Event.MouseEnter] = self.elementMouseEnter, + [Roact.Event.MouseMoved] = self.elementMouseMoved, + [Roact.Event.MouseLeave] = self.elementMouseLeave, + }, content) + end) + end) +end + +return Tooltip diff --git a/BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Components/StyledTooltip.spec.lua b/BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Components/StyledTooltip.spec.lua new file mode 100644 index 0000000000..c2291b26f9 --- /dev/null +++ b/BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Components/StyledTooltip.spec.lua @@ -0,0 +1,30 @@ +return function() + local Library = script.Parent.Parent + local Roact = require(Library.Parent.Parent.Roact) + + local MockWrapper = require(Library.MockWrapper) + + local StyledTooltip = require(script.Parent.StyledTooltip) + + local function createTestTooltip(props) + return Roact.createElement(MockWrapper, {}, { + Tooltip = Roact.createElement(StyledTooltip, props) + }) + end + + it("should create and destroy without errors", function() + local element = createTestTooltip() + local instance = Roact.mount(element) + Roact.unmount(instance) + end) + + it("should render correctly", function () + local container = Instance.new("Folder") + local instance = Roact.mount(createTestTooltip(), container) + local frame = container:FindFirstChildOfClass("Frame") + + expect(frame).to.be.ok() + + Roact.unmount(instance) + end) +end \ No newline at end of file diff --git a/BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Components/TextEntry.lua b/BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Components/TextEntry.lua new file mode 100644 index 0000000000..04040995a1 --- /dev/null +++ b/BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Components/TextEntry.lua @@ -0,0 +1,137 @@ +--[[ + A text entry that is only one line. + Used in a RoundTextBox when Multiline is false. + + Props: + string Text = The text to display + string PlaceholderText = text to display when box is empty/in default state + bool Visible = Whether to display this component + function SetText(text) = Callback to tell parent that text has changed + function FocusChanged(focus) = Callback to tell parent that this component has focus +]] + +local Library = script.Parent.Parent +local Roact = require(Library.Parent.Parent.Roact) + +local Theming = require(Library.Theming) +local withTheme = Theming.withTheme + +local TextEntry = Roact.PureComponent:extend("TextEntry") +local FFlagAllowTextEntryToTakeSizeAndPositionProp = game:DefineFastFlag("AllowTextEntryToTakeSizeAndPositionProp", false) +local FFlagGameSettingsFixNameWhitespace = game:DefineFastFlag("GameSettingsFixNameWhitespace", false) +local FFlagFixTextChangedFromEmptyForTextEntry = game:DefineFastFlag("FixTextChangedFromEmptyForTextEntry", false) + +function TextEntry:init() + self.textBoxRef = Roact.createRef() + self.onTextChanged = function(rbx) + if rbx.TextFits then + rbx.TextXAlignment = Enum.TextXAlignment.Left + else + rbx.TextXAlignment = Enum.TextXAlignment.Right + end + if FFlagFixTextChangedFromEmptyForTextEntry then + if FFlagGameSettingsFixNameWhitespace then + local processed = string.gsub(rbx.Text, "[\n\r]", " ") + self.props.SetText(processed) + else + self.props.SetText(rbx.Text) + end + else + if rbx.Text ~= self.props.Text then + if FFlagGameSettingsFixNameWhitespace then + local processed = string.gsub(rbx.Text, "[\n\r]", " ") + self.props.SetText(processed) + else + self.props.SetText(rbx.Text) + end + end + end + end + + self.mouseEnter = function() + self.props.HoverChanged(true) + end + self.mouseLeave = function() + self.props.HoverChanged(false) + end +end + +function TextEntry:render() + return withTheme(function(theme) + local textSize = self.props.TextSize + local font = self.props.Font + + local textEntryTheme = theme.textEntry + + local size + local position + local textTransparency + local enabled + if FFlagAllowTextEntryToTakeSizeAndPositionProp then + size = self.props.Size and self.props.Size or UDim2.new(1, 0, 1, 0) + position = self.props.Position and self.props.Position or nil + enabled = (self.props.Enabled == nil) and true or self.props.Enabled + textTransparency = enabled and textEntryTheme.textTransparency.enabled or textEntryTheme.textTransparency.disabled + else + size = UDim2.new(1, 0, 1, 0) + position = nil + enabled = nil + textTransparency = nil + end + + return Roact.createElement("Frame", { + Size = size, + BackgroundTransparency = 1, + ClipsDescendants = true, + }, { + Text = Roact.createElement("TextBox", { + Visible = self.props.Visible, + + Size = UDim2.new(1, 0, 1, 0), + Position = position, + BackgroundTransparency = 1, + BorderSizePixel = 0, + + PlaceholderText = self.props.PlaceholderText, + PlaceholderColor3 = self.props.TextColor3, + ClearTextOnFocus = false, + Font = font, + TextSize = textSize, + TextColor3 = self.props.TextColor3, + Text = self.props.Text, + TextXAlignment = Enum.TextXAlignment.Left, + TextTransparency = textTransparency, + TextEditable = enabled, + + [Roact.Ref] = self.textBoxRef, + + [Roact.Event.MouseEnter] = self.mouseEnter, + [Roact.Event.MouseLeave] = self.mouseLeave, + + [Roact.Event.Focused] = function() + self.props.FocusChanged(true) + end, + + [Roact.Event.FocusLost] = function() + -- workaround because we do not disconnect events before we start unmounting host components. + -- see https://github.com/Roblox/roact/issues/235 for more info + if not self.textBoxRef.current then return end + + local textBox = self.textBoxRef.current + textBox.TextXAlignment = Enum.TextXAlignment.Left + self.props.FocusChanged(false) + end, + + [Roact.Change.Text] = function(rbx) + -- workaround because we do not disconnect events before we start unmounting host components. + -- see https://github.com/Roblox/roact/issues/235 for more info + if not self.textBoxRef.current then return end + + self.onTextChanged(rbx) + end + }), + }) + end) +end + +return TextEntry diff --git a/BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Components/TextEntry.spec.lua b/BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Components/TextEntry.spec.lua new file mode 100644 index 0000000000..254be99b19 --- /dev/null +++ b/BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Components/TextEntry.spec.lua @@ -0,0 +1,44 @@ +return function() + local Library = script.Parent.Parent + local Roact = require(Library.Parent.Parent.Roact) + + local MockWrapper = require(Library.MockWrapper) + + local TextEntry = require(script.Parent.TextEntry) + + local function createTestTextEntry(text, visible) + return Roact.createElement(MockWrapper, {}, { + TextEntry = Roact.createElement(TextEntry, { + Text = text, + Visible = visible, + TextSize = 22, + }), + }) + end + + it("should create and destroy without errors", function() + local element = createTestTextEntry() + local instance = Roact.mount(element) + Roact.unmount(instance) + end) + + it("should render correctly", function () + local container = Instance.new("Folder") + local instance = Roact.mount(createTestTextEntry("", true), container) + local frame = container:FindFirstChildOfClass("Frame") + + expect(frame.Text).to.be.ok() + + Roact.unmount(instance) + end) + + it("should hide its text when not visible", function () + local container = Instance.new("Folder") + local instance = Roact.mount(createTestTextEntry("", false), container) + local frame = container:FindFirstChildOfClass("Frame") + + expect(frame.Text.Visible).to.equal(false) + + Roact.unmount(instance) + end) +end \ No newline at end of file diff --git a/BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Components/Timeline/Keyframe.lua b/BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Components/Timeline/Keyframe.lua new file mode 100644 index 0000000000..fe02fb2ad6 --- /dev/null +++ b/BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Components/Timeline/Keyframe.lua @@ -0,0 +1,74 @@ +-- TODO: Delete file when FFlagRemoveUILibraryTimeline is retired +--[[ + A single keyframe which can be displayed on a media timeline. + + Props: + int Width = The size in pixels of the keyframe item. Determines both width and height. + UDim2 Position = The position of the keyframe. + int ZIndex = The display order of the keyframe. + int BorderSizePixel = The size of the keyframe's border highlight. + string Style = A style key for coloring this keyframe. Indexed into the keyframe theme. + + bool Selected = Whether this keyframe is currently selected. Changes the appearance. + + function OnActivated = A callback for when the user clicks on this keyframe. + function OnRightClick = A callback for when the user right-clicks on this keyframe. + function OnInputBegan = A callback for when the user starts interacting with the keyframe. + function OnInputEnded = A callback for when the user stops interacting with the keyframe. +]] + +local Library = script.Parent.Parent.Parent +local Roact = require(Library.Parent.Parent.Roact) +local Theming = require(Library.Theming) +local withTheme = Theming.withTheme + +local DEFAULT_WIDTH = 10 +local DEFAULT_BORDER_SIZE = 2 + +local Keyframe = Roact.PureComponent:extend("Keyframe") + +function Keyframe:render() + return withTheme(function(theme) + local props = self.props + + local style = props.Style + local selected = props.Selected + + local themeBase = style and theme.keyframe[style] or theme.keyframe.Default + local keyframeTheme = selected and themeBase.selected or themeBase + + local position = props.Position + local borderSize = props.BorderSizePixel or DEFAULT_BORDER_SIZE + local width = props.Width or DEFAULT_WIDTH + local zindex = props.ZIndex + + local onActivated = props.OnActivated + local onRightClick = props.OnRightClick + local onInputBegan = props.OnInputBegan + local onInputEnded = props.OnInputEnded + + return Roact.createElement("ImageButton", { + Size = UDim2.new(0, width, 0, width), + AnchorPoint = Vector2.new(0.5, 0.5), + Position = position, + Rotation = 45, + ZIndex = zindex, + + ImageTransparency = 1, + BackgroundTransparency = 0, + AutoButtonColor = false, + + BorderSizePixel = borderSize, + BorderColor3 = keyframeTheme.borderColor, + BackgroundColor3 = keyframeTheme.backgroundColor, + + [Roact.Event.Activated] = onActivated, + [Roact.Event.MouseButton2Click] = onRightClick, + + [Roact.Event.InputBegan] = onInputBegan, + [Roact.Event.InputEnded] = onInputEnded, + }, props[Roact.Children]) + end) +end + +return Keyframe diff --git a/BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Components/Timeline/Keyframe.spec.lua b/BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Components/Timeline/Keyframe.spec.lua new file mode 100644 index 0000000000..57826636bf --- /dev/null +++ b/BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Components/Timeline/Keyframe.spec.lua @@ -0,0 +1,31 @@ +-- TODO: Delete file when FFlagRemoveUILibraryTimeline is retired +return function() + local Library = script.Parent.Parent.Parent + local Roact = require(Library.Parent.Parent.Roact) + + local MockWrapper = require(Library.MockWrapper) + + local Keyframe = require(script.Parent.Keyframe) + + local function createTestKeyframe(enabled, selected) + return Roact.createElement(MockWrapper, {}, { + keyframe = Roact.createElement(Keyframe), + }) + end + + it("should create and destroy without errors", function() + local element = createTestKeyframe() + local instance = Roact.mount(element) + Roact.unmount(instance) + end) + + it("should render correctly", function () + local container = Instance.new("Folder") + local instance = Roact.mount(createTestKeyframe(), container) + local frame = container:FindFirstChildOfClass("ImageButton") + + expect(frame).to.be.ok() + + Roact.unmount(instance) + end) +end \ No newline at end of file diff --git a/BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Components/Timeline/Scrubber.lua b/BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Components/Timeline/Scrubber.lua new file mode 100644 index 0000000000..8ef1706dad --- /dev/null +++ b/BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Components/Timeline/Scrubber.lua @@ -0,0 +1,66 @@ +-- TODO: Delete file when FFlagRemoveUILibraryTimeline is retired +--[[ + Generic implementation of a Scrubber for a Timeline + + Properties: + UDim2 Position = position of the scrubber head + UDim2 HeadSize = size of the scrubber head + float Height = length of the scrubber line + bool ShowHead = whether or not the scrubber head is visible + Vector2 AnchorPoint = anchor point for the Scrubber component + int ZIndex = display order of the scrubber component + int thickness = pixel width of the scrubber line +]] + +local Library = script.Parent.Parent.Parent +local Roact = require(Library.Parent.Parent.Roact) +local Theming = require(Library.Theming) +local withTheme = Theming.withTheme + +local Scrubber = Roact.PureComponent:extend("Scrubber") + +function Scrubber:render() + return withTheme(function(theme) + local props = self.props + + local position = props.Position + local headSize = props.HeadSize + local height = props.Height + local showHead = props.ShowHead + local anchorPoint = props.AnchorPoint + local zIndex = props.ZIndex + local thickness = props.Thickness + + local children = props[Roact.Children] + if not children then + children = {} + end + if showHead then + table.insert(children, Roact.createElement("ImageLabel", { + Image = theme.scrubber.image, + ImageColor3 = theme.scrubber.backgroundColor, + BackgroundTransparency = 1, + Size = UDim2.new(1, 0, 1, 0), + })) + end + + table.insert(children, Roact.createElement("Frame", { + Position = UDim2.new(0.5, 0, 0, 0), + Size = UDim2.new(0, thickness, 0, height), + BackgroundColor3 = theme.scrubber.backgroundColor, + AnchorPoint = Vector2.new(0.5, 0), + BorderSizePixel = 0, + })) + + return Roact.createElement("Frame", { + BackgroundTransparency = 1, + Position = position, + Size = headSize, + ZIndex = zIndex, + AnchorPoint = anchorPoint, + [Roact.Event.InputBegan] = self.onDragBegan, + }, children) + end) +end + +return Scrubber \ No newline at end of file diff --git a/BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Components/Timeline/Scrubber.spec.lua b/BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Components/Timeline/Scrubber.spec.lua new file mode 100644 index 0000000000..4efff8bc32 --- /dev/null +++ b/BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Components/Timeline/Scrubber.spec.lua @@ -0,0 +1,54 @@ +-- TODO: Delete file when FFlagRemoveUILibraryTimeline is retired +return function() + local Library = script.Parent.Parent.Parent + local Roact = require(Library.Parent.Parent.Roact) + + local MockWrapper = require(Library.MockWrapper) + + local Scrubber = require(script.Parent.Scrubber) + + local function createTestScrubber(showHead) + return Roact.createElement(MockWrapper, {}, { + Scrubber = Roact.createElement(Scrubber, { + Height = 1000, + HeadSize = UDim2.new(0, 48, 0, 48), + ShowHead = showHead, + AnchorPoint = Vector2.new(0.5, 0), + Thickness = 1, + }) + }) + end + + it("should create and destroy without errors", function() + local element = createTestScrubber(true) + local instance = Roact.mount(element) + Roact.unmount(instance) + end) + + + describe("should render correctly", function() + it("should render with head correctly", function() + local container = Instance.new("Folder") + local instance = Roact.mount(createTestScrubber(true), container) + local frame = container:FindFirstChildOfClass("Frame") + + expect(frame).to.be.ok() + expect(frame["1"]).to.be.ok() + expect(frame["2"]).to.be.ok() + + Roact.unmount(instance) + end) + + it("should render without head correctly", function() + local container = Instance.new("Folder") + local instance = Roact.mount(createTestScrubber(false), container) + local frame = container:FindFirstChildOfClass("Frame") + + expect(frame).to.be.ok() + expect(#frame:GetChildren()).to.be.equal(1) + expect(frame["1"]).to.be.ok() + + Roact.unmount(instance) + end) + end) +end \ No newline at end of file diff --git a/BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Components/TitledFrame.lua b/BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Components/TitledFrame.lua new file mode 100644 index 0000000000..02a8431675 --- /dev/null +++ b/BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Components/TitledFrame.lua @@ -0,0 +1,57 @@ +--[[ + A frame with a title offset to the left side. + Used as a distinct vertical entry on a SettingsPage. + + Props: + string Title = The text to display in this TitledFrame's left-hand title. + int MaxHeight = The maximum height of this TitledFrame in pixels. Defaults to 100. + int LayoutOrder = The order which this TitledFrame will sort to in a UIListLayout. + int TextSize = The size of text +]] + +local Library = script.Parent.Parent +local Roact = require(Library.Parent.Parent.Roact) +local Theming = require(Library.Theming) +local withTheme = Theming.withTheme + +local CENTER_GUTTER = 180 + +local function TitledFrame(props) + return withTheme(function(theme) + local textSize = props.TextSize + local centerGutter = props.CenterGutter or CENTER_GUTTER + + return Roact.createElement("Frame", { + BackgroundTransparency = 1, + BorderSizePixel = 0, + ZIndex = props.ZIndex or 1, + Size = UDim2.new(1, 0, 0, props.MaxHeight or 100), + LayoutOrder = props.LayoutOrder or 1, + }, { + Title = Roact.createElement("TextLabel", { + BackgroundTransparency = 1, + BorderSizePixel = 0, + Size = UDim2.new(0, centerGutter, 1, 0), + + TextColor3 = theme.titledFrame.text, + Font = theme.titledFrame.font, + TextSize = textSize, + TextXAlignment = Enum.TextXAlignment.Left, + TextYAlignment = Enum.TextYAlignment.Top, + Text = props.Title, + TextWrapped = true, + }), + + Content = Roact.createElement("Frame", { + BackgroundTransparency = 1, + BorderSizePixel = 0, + + Size = UDim2.new(1, -centerGutter, 1, 0), + AnchorPoint = Vector2.new(1, 0), + Position = UDim2.new(1, 0, 0, 0), + }, props[Roact.Children]), + }) + end) +end + +return TitledFrame \ No newline at end of file diff --git a/BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Components/TitledFrame.spec.lua b/BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Components/TitledFrame.spec.lua new file mode 100644 index 0000000000..048a470fbc --- /dev/null +++ b/BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Components/TitledFrame.spec.lua @@ -0,0 +1,34 @@ +return function() + local Library = script.Parent.Parent + local Roact = require(Library.Parent.Parent.Roact) + + local MockWrapper = require(Library.MockWrapper) + + local TitledFrame = require(script.Parent.TitledFrame) + + + local function createTestTitledFrame() + return Roact.createElement(MockWrapper, {}, { + TitledFrame = Roact.createElement(TitledFrame, { + Title = "Title", + }), + }) + end + + it("should create and destroy without errors", function() + local element = createTestTitledFrame() + local instance = Roact.mount(element) + Roact.unmount(instance) + end) + + it("should render correctly", function () + local container = Instance.new("Folder") + local instance = Roact.mount(createTestTitledFrame(), container) + local titledFrame = container:FindFirstChildOfClass("Frame") + + expect(titledFrame.Title).to.be.ok() + expect(titledFrame.Content).to.be.ok() + + Roact.unmount(instance) + end) +end \ No newline at end of file diff --git a/BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Components/ToggleButton.lua b/BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Components/ToggleButton.lua new file mode 100644 index 0000000000..69ac76ab8a --- /dev/null +++ b/BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Components/ToggleButton.lua @@ -0,0 +1,61 @@ +--[[ + A toggle button with on and off state. + + Necessary props: + Position = explicit position, if not placed in UIListLayout + + bool Enabled = Whether or not this button can be clicked. + bool IsOn = whether the button should be on or off + + function onToggle = The function that will be called when this button is clicked to turn on and off + + Optional pros: + int LayoutOrder = The order this ToggleButton will sort to when placed in a UIListLayout +]] + +local Library = script.Parent.Parent +local Roact = require(Library.Parent.Parent.Roact) + +local Theming = require(Library.Theming) +local withTheme = Theming.withTheme + +local ToggleButton = Roact.PureComponent:extend("ToggleButton") + +function ToggleButton:init(props) + self.onToggle = function() + self.props.onToggle(not self.props.IsOn) + end +end + +function ToggleButton:render() + return withTheme(function(theme) + local props = self.props + + local toogleButtonTheme = theme.toggleButton + + local backgroundImage + if props.Enabled then + if props.IsOn then + backgroundImage = toogleButtonTheme.onImage + else + backgroundImage = toogleButtonTheme.offImage + end + else + backgroundImage = toogleButtonTheme.disabledImage + end + + return Roact.createElement("ImageButton", { + BackgroundTransparency = 1, -- Necessary to make the rounded background + Image = backgroundImage, + + Position = props.Position, + Size = props.Size or UDim2.new(0, toogleButtonTheme.defaultWidth, 0, toogleButtonTheme.defaultHeight), + + LayoutOrder = props.LayoutOrder or 1, + + [Roact.Event.Activated] = self.onToggle, + }) + end) +end + +return ToggleButton diff --git a/BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Components/ToggleButton.spec.lua b/BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Components/ToggleButton.spec.lua new file mode 100644 index 0000000000..56fd52742a --- /dev/null +++ b/BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Components/ToggleButton.spec.lua @@ -0,0 +1,30 @@ +return function() + local Library = script.Parent.Parent + local Roact = require(Library.Parent.Parent.Roact) + + local MockWrapper = require(Library.MockWrapper) + + local ToggleButton = require(script.Parent.ToggleButton) + + local function createTestToggleButton() + return Roact.createElement(MockWrapper, {}, { + ToggleButton = Roact.createElement(ToggleButton, { + Size = UDim2.new(0, 20, 0, 20), + Enabled = true, + IsOn = true, + + OnClickedOn = function() + end, + + OnClickedOff = function() + end, + }), + }) + end + + it("should create and destroy without errors", function() + local element = createTestToggleButton() + local instance = Roact.mount(element) + Roact.unmount(instance) + end) +end \ No newline at end of file diff --git a/BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Components/Tooltip.lua b/BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Components/Tooltip.lua new file mode 100644 index 0000000000..dd7f6e7fa3 --- /dev/null +++ b/BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Components/Tooltip.lua @@ -0,0 +1,193 @@ +--[[ + An element which can be added to a component to give that component a tooltip. + When the user hovers the mouse over the component, the tooltip will appear + after a short delay. + + Props: + string Text = The text to display in the tooltip. + float ShowDelay = The time in seconds before the tooltip appears + after the user stops moving the mouse over the element. Defaults to 0.5. + bool Enabled = Whether the tooltip will display on hover. + int Priority = The display order of this element, compared to other focused + elements or elements that show on top. +]] + +local PADDING = 3 +local SHADOW_OFFSET = Vector2.new(3, 3) +local OFFSET = Vector2.new(10, 5) +local SHOW_DELAY_DEFAULT = 0.5 + +local RunService = game:GetService("RunService") +local TextService = game:GetService("TextService") + +local Library = script.Parent.Parent +local Roact = require(Library.Parent.Parent.Roact) + +local Theming = require(Library.Theming) +local withTheme = Theming.withTheme + +local Focus = require(Library.Focus) +local ShowOnTop = Focus.ShowOnTop +local withFocus = Focus.withFocus + +local DropShadow = require(Library.Components.DropShadow) + +local Tooltip = Roact.PureComponent:extend("Tooltip") + +function Tooltip:init(props) + local showDelay = props.ShowDelay or SHOW_DELAY_DEFAULT + + self.state = { + showToolTip = false, + } + + self.isHovered = false + self.mousePos = nil + + self.connectHover = function() + self.hoverConnection = RunService.Heartbeat:Connect(function() + if self.isHovered then + if tick() >= self.targetTime then + self.disconnectHover() + self:setState({ + showToolTip = true, + }) + end + end + end) + end + + self.disconnectHover = function() + if self.hoverConnection then + self.hoverConnection:Disconnect() + end + end + + self.mouseEnter = function(rbx, xpos, ypos) + self.isHovered = true + self.targetTime = tick() + showDelay + self.mousePos = Vector2.new(xpos, ypos) + self.connectHover() + end + + self.mouseMoved = function(rbx, xpos, ypos) + self.mousePos = Vector2.new(xpos, ypos) + self.targetTime = tick() + showDelay + end + + self.mouseLeave = function() + self.isHovered = false + self.targetTime = 0 + self.mousePos = nil + self.disconnectHover() + self:setState({ + showToolTip = false, + }) + end +end + +function Tooltip:willUnmount() + self.disconnectHover() +end + +function Tooltip:render() + return withTheme(function(theme) + return withFocus(function(pluginGui) + local props = self.props + local state = self.state + + local tooltipTheme = theme.tooltip + local textSize = tooltipTheme.textSize + + local text = props.Text + local enabled = props.Enabled + local priority = props.Priority or 0 + + local mousePos = self.mousePos + + local content = {} + + if state.showToolTip and mousePos and enabled and pluginGui then + local targetX = mousePos.X + OFFSET.X + local targetY = mousePos.Y + OFFSET.Y + + local targetWidth = pluginGui.AbsoluteSize.X + local targetHeight = pluginGui.AbsoluteSize.Y + + local textBound = TextService:GetTextSize(text, + textSize, tooltipTheme.font, Vector2.new(100, 9000)) + + local tooltipTargetWidth = textBound.X + 2 * PADDING + local tooltipTargetHeight = textBound.Y + 2 * PADDING + + if targetX + tooltipTargetWidth >= targetWidth then + targetX = targetWidth - tooltipTargetWidth + end + + if targetY + tooltipTargetHeight >= targetHeight then + targetY = targetHeight - tooltipTargetHeight + end + + content.TooltipContainer = Roact.createElement(ShowOnTop, { + Priority = priority, + }, { + Tooltip = Roact.createElement("Frame", { + Position = UDim2.new(0, targetX, 0, targetY), + Size = UDim2.new(0, tooltipTargetWidth, 0, tooltipTargetHeight), + BackgroundTransparency = 1, + ZIndex = 10, + }, { + DropShadow = Roact.createElement(DropShadow, { + Transparency = tooltipTheme.shadowTransparency, + Color = tooltipTheme.shadowColor, + Offset = SHADOW_OFFSET, + ZIndex = 1, + }), + + ContentFrame = Roact.createElement("Frame", { + Size = UDim2.new(1, 0, 1, 0), + ZIndex = 2, + + BackgroundColor3 = tooltipTheme.backgroundColor, + BorderColor3 = tooltipTheme.borderColor, + }, { + UIPadding = Roact.createElement("UIPadding", { + PaddingBottom = UDim.new(0, PADDING), + PaddingLeft = UDim.new(0, PADDING), + PaddingRight = UDim.new(0, PADDING), + PaddingTop = UDim.new(0, PADDING), + }), + + Label = Roact.createElement("TextLabel", { + Size = UDim2.new(1, 0, 1, 0), + BackgroundTransparency = 1, + BorderSizePixel = 0, + Text = text, + + TextColor3 = tooltipTheme.textColor, + + Font = tooltipTheme.font, + TextSize = textSize, + TextXAlignment = Enum.TextXAlignment.Left, + TextYAlignment = Enum.TextYAlignment.Top, + TextWrapped = true, + ZIndex = 3, + }), + }) + }) + }) + end + + return Roact.createElement("Frame",{ + Size = UDim2.new(1, 0, 1, 0), + BackgroundTransparency = 1, + + [Roact.Event.MouseEnter] = self.mouseEnter, + [Roact.Event.MouseMoved] = self.mouseMoved, + [Roact.Event.MouseLeave] = self.mouseLeave, + }, content) + end) + end) +end + +return Tooltip diff --git a/BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Components/Tooltip.spec.lua b/BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Components/Tooltip.spec.lua new file mode 100644 index 0000000000..28c7d6b015 --- /dev/null +++ b/BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Components/Tooltip.spec.lua @@ -0,0 +1,30 @@ +return function() + local Library = script.Parent.Parent + local Roact = require(Library.Parent.Parent.Roact) + + local MockWrapper = require(Library.MockWrapper) + + local Tooltip = require(script.Parent.Tooltip) + + local function createTestTooltip(props) + return Roact.createElement(MockWrapper, {}, { + Tooltip = Roact.createElement(Tooltip, props) + }) + end + + it("should create and destroy without errors", function() + local element = createTestTooltip() + local instance = Roact.mount(element) + Roact.unmount(instance) + end) + + it("should render correctly", function () + local container = Instance.new("Folder") + local instance = Roact.mount(createTestTooltip(), container) + local frame = container:FindFirstChildOfClass("Frame") + + expect(frame).to.be.ok() + + Roact.unmount(instance) + end) +end \ No newline at end of file diff --git a/BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Components/TreeView.lua b/BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Components/TreeView.lua new file mode 100644 index 0000000000..14751f8704 --- /dev/null +++ b/BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Components/TreeView.lua @@ -0,0 +1,524 @@ +--[[ + Displays a heirarchical data set. + + For these notes, assume that : + - DataNode = the type of the data you have passed into the TreeView + + Props: + (required) + dataTree : (DataNode) the first element in some heirarchical data set + getChildren : (function>(DataNode)) gets a list of the node's children + renderElement : (function(table)) renders the current element given a table of props + + (optional) + sortChildren : (function(DataNode, DataNode)) a comparator function passed to table.sort() + onSelectionChanged : (function(list)) a callback for observing when items are selected + expandAll: (bool) a check to determine if the entire tree should be initially expanded. + expandRoot: (bool) a check to determine if the root of the tree should be initially expanded. + createFlatList: (bool) a check to determine if a flat list or node/child structure should be used. + + Elements rendered by the TreeView are given the following props : + -- data information + element : (DataNode) + parent : (DataNode) + + -- styling information + rowIndex : (int) the current row of the element + indent : (int) the current depth of the element + canExpand : (bool) true if the element contains children + isExpanded : (bool) true if the element is currently showing its children + isSelected : (bool) true if the element has been selected, + + -- function callbacks + toggleExpanded : (function()) a function that tells the treeview to expand or collapse this row + toggleSelected : (function(bool)) a function that tells the treeview to select this row +]] +local FFlagStudioFixTreeViewForSquish = settings():GetFFlag("StudioFixTreeViewForSquish") +-- Related Ticket https://jira.rbx.com/browse/CLISTUDIO-21831 +local FFlagStudioFixTreeViewForFlatList = settings():GetFFlag("StudioFixTreeViewForFlatList") +local FFlagFixTreeViewFlatListDefault = game:DefineFastFlag("FixTreeViewFlatListDefault", false) + +local Library = script.Parent.Parent + +local Roact = require(Library.Parent.Parent.Roact) +local Theming = require(Library.Theming) +local withTheme = Theming.withTheme + +local TreeView = Roact.PureComponent:extend("TreeView") + +function TreeView:init() + assert(self.props.dataTree ~= nil, "TreeView expected a dataTree, but none was provided") + assert(type(self.props.getChildren) == "function", "TreeView expected getChildren() to be defined") + if self.props.sortChildren then + assert(type(self.props.sortChildren) == "function", "TreeView expected sortChildren()") + end + + local dataTreeRoot = self.props.dataTree + local getChildrenFunc = self.props.getChildren + local sortChildrenFunc = self.props.sortChildren + local expandAll = self.props.expandAll + local expandRoot = self.props.expandRoot + + self.layoutRef = Roact.createRef() + self.contentRef = Roact.createRef() + self.scrollbarResizeSignalToken = nil + + self.previousNodesArray = {} + self.nodesArray = {} + + self.state = { + expandedItems = {}, + selectedItems = {}, + } + + --[[ + resizeScrollContent : a callback for when the content in the treeview resizes. Ensures that all content can be seen + ]] + self.resizeScrollContent = function() + if not FFlagStudioFixTreeViewForSquish then + return + end + + -- keep the canvas size equal to the size of the content in it + local absoluteContentSize = self.layoutRef.current.AbsoluteContentSize + self.contentRef.current.CanvasSize = UDim2.new(0, absoluteContentSize.X, 0, absoluteContentSize.Y) + end + + --[[ + onTreeUpdated: fires a callback (if provided) for whenever the tree is created/updated. Provides the nodes of the tree + in a list format. + ]] + self.onTreeUpdated = function() + local function treeChanged() + if #self.previousNodesArray ~= #self.nodesArray then + return true + end + + for index, node in ipairs(self.previousNodesArray) do + if self.nodesArray[index] ~= node then + return true + end + end + + return false + end + if self.props.onTreeUpdated and treeChanged() then + self.props.onTreeUpdated(self.nodesArray) + end + end + + --[[ + toggleStateValue : a helper function for changing state values. + + Since Roact only does partial merges of keys, this function is to help ensure that keys get removed + when they are assigned nil. + + Args : + tableName : (string) a table key defined in self.state + element : (DataNode) + copyOldState : (boolean) when true, preserves the old state. When false, removes old state entirely + ]] + self.toggleStateValue = function(tableName, element, copyOldState) + assert(tableName ~= nil, "Expected a table name, but found none") + assert(self.state[tableName] ~= nil, string.format("%s does not exist in the state table", tostring(tableName))) + assert(element ~= nil, string.format("Expected an element to add to %s but found none", tableName)) + assert(type(copyOldState) == "boolean", "Expected copyOldState to be a boolean") + + local newValue = true + if self.state[tableName][element] then + newValue = nil + end + + -- copy the old table and update with the new value + local newState = { + [element] = newValue + } + if copyOldState then + for k, v in pairs(self.state[tableName]) do + if k ~= element then + newState[k] = v + end + end + end + + self:setState({ + [tableName] = newState + }) + + return newState + end + + self.toggleExpandedElement = function(element) + return function() + if element == nil then + return + end + + self.toggleStateValue("expandedItems", element, true) + end + end + + self.toggleSelectedElement = function(element) + -- shouldSelectAlso : (bool) if true, does not clear the previous selection + return function(shouldSelectAlso) + if shouldSelectAlso == nil then + shouldSelectAlso = false + end + assert(type(shouldSelectAlso) == "boolean", "Expected shouldSelectAlso to be a boolean") + + local onSelectionChanged = self.props.onSelectionChanged + local newState = self.toggleStateValue("selectedItems", element, shouldSelectAlso) + + if onSelectionChanged then + -- return a list of selected elements + local selectedData = {} + for k, isSelected in pairs(newState) do + if isSelected then + table.insert(selectedData, k) + end + end + onSelectionChanged(selectedData) + end + end + end + + --[[ + createNode : provides a bunch of props to the consumer's renderElement callback + + Args : + element : (DataNode) + parent : (DataNode) + rowIndex : (int) the current row of the element + indent : (int) the current depth of the element + ]] + -- Remove with FFlagStudioFixTreeViewForSquish + self.DEPRECATED_createNode = function(element, parent, rowIndex, indent) + local expandedItems = self.state.expandedItems + local selectedItems = self.state.selectedItems + local renderElement = self.props.renderElement + local getChildren = self.props.getChildren + + local canExpand = next(getChildren(element)) ~= nil + + local props = { + -- data information + element = element, + parent = parent, + + -- styling information + rowIndex = rowIndex, + indent = indent, + canExpand = canExpand, + isExpanded = expandedItems[element] == true, + isSelected = selectedItems[element] == true, + + -- function callbacks + toggleExpanded = self.toggleExpandedElement(canExpand and element or nil), + toggleSelected = self.toggleSelectedElement(element), + } + + return renderElement(props) + end + + self.createNode = function(element, rowIndex, indent, children) + local expandedItems = self.state.expandedItems + local selectedItems = self.state.selectedItems + local renderElement = self.props.renderElement + local getChildren = self.props.getChildren + + local canExpand = next(getChildren(element)) ~= nil + + local props = { + -- data information + element = element, + + -- styling information + rowIndex = rowIndex, + indent = indent, + canExpand = canExpand, + isExpanded = expandedItems[element] == true, + isSelected = selectedItems[element] == true, + + -- function callbacks + toggleExpanded = self.toggleExpandedElement(canExpand and element or nil), + toggleSelected = self.toggleSelectedElement(element), + + children = children, + } + + return renderElement(props) + end + + --[[ + traverseDepthFirst : visits all of the children in a tree structure, assuming they have been expanded + + Args: + parent : (DataNode) the element to search inside for children + depth : (int) a counter for indenting purposes + handlers : (table) all of the callbacks to properly traverse the tree + - onNodeVisited : (function(DataNode, int, DataNode)) + - decideShouldContinue : (function(DataNode, int, DataNode)) decides if it should continue + - getChildrenOfElement : (function>(DataNode)) gets a list of children from a parent + - sortChildren : (optional, function(DataNode, DataNode) a comparator function to sort the children + ]] + + -- Remove with FFlagStudioFixTreeViewForSquish + self.DEPRECATED_traverseDepthFirst = function(parent, depth, handlers) + local children = handlers.getChildrenOfElement(parent) + + if handlers.sortChildren then + table.sort(children, handlers.sortChildren) + end + + for _, child in pairs(children) do + -- alert any listeners that we've visited this node + handlers.onNodeVisited(child, depth + 1, parent) + + -- check if there are any children of this node we should traverse + local shouldContinue = handlers.decideShouldContinue(child, depth + 1, parent) + if shouldContinue then + self.DEPRECATED_traverseDepthFirst(child, depth + 1, handlers) + end + end + end + + self.traverseDepthFirst = function(current, depth, handlers) + if not handlers.decideShouldContinue(current) then + return handlers.onNodeVisited(current, depth, {}) + end + + local children = handlers.getChildrenOfElement(current) + + local childComponents = {} + + local createFlatList + if FFlagFixTreeViewFlatListDefault then + if self.props.createFlatList == nil then + createFlatList = true + else + createFlatList = self.props.createFlatList + end + else + createFlatList = FFlagStudioFixTreeViewForFlatList and self.props.createFlatList + end + + if handlers.sortChildren then + table.sort(children, handlers.sortChildren) + end + + if createFlatList then + handlers.onNodeVisited(current, depth, {}) + for _, child in pairs(children) do + self.traverseDepthFirst(child, depth + 1, handlers) + end + else + for _, child in pairs(children) do + local childComponent = self.traverseDepthFirst(child, depth + 1, handlers) + table.insert(childComponents, childComponent) + end + + return handlers.onNodeVisited(current, depth, childComponents) + end + end + --[[ + getVisibleNodes : returns a map of the elements to render into the tree, including the root + ]] + self.getVisibleNodes = function() + self.previousNodesArray = self.nodesArray + self.nodesArray = {} + + local expandedItems = self.state.expandedItems + + local root = self.props.dataTree + local getChildren = self.props.getChildren + local sortChildren = self.props.sortChildren + local createFlatList + if FFlagFixTreeViewFlatListDefault then + if self.props.createFlatList == nil then + createFlatList = true + else + createFlatList = self.props.createFlatList + end + else + createFlatList = FFlagStudioFixTreeViewForFlatList and self.props.createFlatList + end + + local numNodes = 1 + local treeNodes + if not FFlagStudioFixTreeViewForSquish then + treeNodes = { + Root = self.DEPRECATED_createNode(root, nil, 0, 0), + } + else + treeNodes = {} + end + + if expandedItems[root] then + if FFlagStudioFixTreeViewForSquish then + treeNodes.Root = self.traverseDepthFirst(root, 0, { + -- upon visiting a node, add it to the map of elements to display + onNodeVisited = function(child, depth, children) + local node = self.createNode(child, numNodes, depth, children) + + if createFlatList then + if node then + local nodeName = string.format("Node-%d", numNodes) + treeNodes[nodeName] = node + numNodes = numNodes + 1 + end + + if FFlagFixTreeViewFlatListDefault then + table.insert(self.nodesArray, child) + end + else + numNodes = numNodes + 1 + return node + end + end, + + -- when deciding whether to continue traversing the child elements, check if it is expanded + decideShouldContinue = function(child) + return expandedItems[child] == true + end, + + -- allow the consumer to figure out how to get the children of each element + getChildrenOfElement = getChildren, + sortChildren = sortChildren }) + else + self.DEPRECATED_traverseDepthFirst(root, 0, { + -- upon visiting a node, add it to the map of elements to display + onNodeVisited = function(child, depth, parent) + local nodeName = string.format("Node-%d", numNodes) + treeNodes[nodeName] = self.DEPRECATED_createNode(child, parent, numNodes, depth) + + numNodes = numNodes + 1 + table.insert(self.nodesArray, child) + end, + + -- when deciding whether to continue traversing the child elements, check if it is expanded + decideShouldContinue = function(child, depth, parent) + return expandedItems[child] == true + end, + + -- allow the consumer to figure out how to get the children of each element + getChildrenOfElement = getChildren, + sortChildren = sortChildren }) + end + elseif FFlagStudioFixTreeViewForSquish then + treeNodes.Root = self.createNode(root, 0 , 0, {}) + end + + return treeNodes + end + + -- if the tree is marked as expandAll, then show all the nodes by default + if expandAll then + local expandedItems = { + [dataTreeRoot] = true, + } + if FFlagStudioFixTreeViewForSquish then + self.traverseDepthFirst(dataTreeRoot, 0, { + onNodeVisited = function(child) + expandedItems[child] = true + end, + decideShouldContinue = function() + return true + end, + getChildrenOfElement = getChildrenFunc, + sortChildren = sortChildrenFunc, + }) + else + self.DEPRECATED_traverseDepthFirst(dataTreeRoot, 0, { + onNodeVisited = function(child) + expandedItems[child] = true + end, + decideShouldContinue = function() + return true + end, + getChildrenOfElement = getChildrenFunc, + sortChildren = sortChildrenFunc, + }) + end + self.state.expandedItems = expandedItems + end + + if expandRoot then + self.state.expandedItems = { + [dataTreeRoot] = true, + } + end +end + +function TreeView:render() + return withTheme(function(theme) + local props = self.props + + local padding = theme.treeView.scrollbar.scrollbarPadding + + local size = FFlagStudioFixTreeViewForSquish and props.Size or UDim2.new(1, -2*padding, 1, -2*padding) + + local layoutOrder = props.LayoutOrder + + local childrenPadding = not FFlagStudioFixTreeViewForSquish and UDim.new(0, theme.treeView.elementPadding) or nil + + local treeViewChildren = { + Layout = Roact.createElement("UIListLayout", { + SortOrder = Enum.SortOrder.LayoutOrder, + FillDirection = Enum.FillDirection.Vertical, + Padding = childrenPadding, + [Roact.Ref] = self.layoutRef, + [Roact.Change.AbsoluteContentSize] = self.resizeScrollContent, + }) + } + + for name, node in pairs(self.getVisibleNodes()) do + -- each of these children will be rendered by the consumer + treeViewChildren[name] = node + end + + return Roact.createElement("ScrollingFrame", { + Position = UDim2.new(0, padding, 0, padding), + Size = size, + BorderSizePixel = 0, + BackgroundTransparency = 1, + ScrollBarThickness = theme.treeView.scrollbar.scrollbarThickness, + ClipsDescendants = true, + LayoutOrder = layoutOrder, + + TopImage = theme.treeView.scrollbar.topImage, + MidImage = theme.treeView.scrollbar.midImage, + BottomImage = theme.treeView.scrollbar.bottomImage, + + ScrollBarImageColor3 = theme.treeView.scrollbar.scrollbarImageColor, + + ElasticBehavior = Enum.ElasticBehavior.Always, + ScrollingDirection = Enum.ScrollingDirection.XY, + + [Roact.Ref] = self.contentRef, + }, treeViewChildren) + end) +end + +function TreeView:didUpdate() + self.onTreeUpdated() +end + +function TreeView:didMount() + if not FFlagStudioFixTreeViewForSquish then + local resizeSignal = self.layoutRef.current:GetPropertyChangedSignal("AbsoluteContentSize") + self.scrollbarResizeSignalToken = resizeSignal:Connect(self.resizeScrollContent) + end + + self.onTreeUpdated() +end + +function TreeView:didUnmount() + self.previousNodesArray = nil + self.nodesArray = nil + if self.scrollbarResizeSignalToken then + self.scrollbarResizeSignalToken:Disconnect() + self.scrollbarResizeSignalToken = nil + end +end + +return TreeView \ No newline at end of file diff --git a/BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Components/TreeView.spec.lua b/BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Components/TreeView.spec.lua new file mode 100644 index 0000000000..482c173dc5 --- /dev/null +++ b/BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Components/TreeView.spec.lua @@ -0,0 +1,355 @@ +local TreeView = require(script.Parent.TreeView) + +local Library = script.Parent.Parent +local Roact = require(Library.Parent.Parent.Roact) +local MockWrapper = require(Library.MockWrapper) + +local FFlagStudioFixTreeViewForSquish = settings():GetFFlag("StudioFixTreeViewForSquish") + +local function mockDataNode(value, parent) + local Node = { + Value = value, + Children = {}, + } + + if parent then + table.insert(parent.Children, Node) + end + + return Node +end + +local function mockDataTree() + local root = mockDataNode("Players") + local node1 = mockDataNode("John Doe", root) + local node2 = mockDataNode("Jane Doe", root) + local node3 = mockDataNode("Builderman", root) + mockDataNode("Sword", node1) + mockDataNode("Shield", node1) + mockDataNode("Gun", node2) + mockDataNode("Hammer", node3) + + return root +end + +local function mockGetChildren(node) + return node.Children +end + +local function mockRenderElement(props) + return Roact.createElement("Frame",{}) +end + +local function mockSortChildren(nodeA, nodeB) + return nodeA.Value > nodeB.Value +end + +return function() + describe("TreeView", function() + it("should create and destroy without errors", function() + local element = Roact.createElement(MockWrapper, {}, { + TreeView = Roact.createElement(TreeView, { + dataTree = mockDataTree(), + getChildren = mockGetChildren, + renderElement = mockRenderElement, + + -- optional props + sortChildren = mockSortChildren, + }), + }) + local container = Instance.new("Frame") + local instance = Roact.mount(element, container) + Roact.unmount(instance) + end) + + it("should error when it is missing important props", function() + expect(function() + local element = Roact.createElement(MockWrapper, {}, { + TreeView = Roact.createElement(TreeView, { + dataTree = mockDataTree(), + getChildren = mockGetChildren, + }), + }) + local instance = Roact.mount(element) + Roact.unmount(instance) + end).to.throw() + + expect(function() + local element = Roact.createElement(MockWrapper, {}, { + TreeView = Roact.createElement(TreeView, { + dataTree = mockDataTree, + renderElement = mockRenderElement, + }), + }) + local instance = Roact.mount(element) + Roact.unmount(instance) + end).to.throw() + + expect(function() + local element = Roact.createElement(MockWrapper, {}, { + TreeView = Roact.createElement(TreeView, { + getChildren = mockGetChildren, + renderElement = mockRenderElement, + }), + }) + local instance = Roact.mount(element) + Roact.unmount(instance) + end).to.throw() + end) + + it("should render correctly", function() + local element = Roact.createElement(MockWrapper, {}, { + TreeView = Roact.createElement(TreeView, { + dataTree = mockDataTree(), + getChildren = mockGetChildren, + renderElement = mockRenderElement, + }), + }) + local container = Instance.new("Folder") + local instance = Roact.mount(element, container) + + local treeView = container:FindFirstChildOfClass("ScrollingFrame") + expect(treeView).to.be.ok() + expect(treeView.Root).to.be.ok() + expect(treeView.Layout).to.be.ok() + + Roact.unmount(instance) + end) + + it("should render the children when you expand the node", function() + local nodesRenderedCount = 0 + local expandChildFunc + local function renderElement(props) + -- if rendering the root node, grab the callback to expand it + if props.element.Value == "Players" then + expandChildFunc = props.toggleExpanded + end + + nodesRenderedCount = nodesRenderedCount + 1 + + -- create an element + return Roact.createElement("TextLabel", { + Text = props.element.Value + }, props.children) + end + + local count = 0 + local function dfCount(root) + local children = root:GetChildren() + count = count + 1 + + if #children == 0 then + return + end + + for _, child in ipairs(children) do + dfCount(child) + end + end + + -- render the tree + local element = Roact.createElement(MockWrapper, {}, { + TreeView = Roact.createElement(TreeView, { + dataTree = mockDataTree(), + getChildren = mockGetChildren, + renderElement = renderElement, + }), + }) + local container = Instance.new("Folder") + local instance = Roact.mount(element, container) + local treeView = container:FindFirstChildOfClass("ScrollingFrame") + + -- Remove with FFlagStudioFixTreeViewForSquish + local renderedChildren + renderedChildren = treeView:GetChildren() + + if FFlagStudioFixTreeViewForSquish then + dfCount(treeView) + expect(count).to.equal(nodesRenderedCount + 2)-- should equal number of nodes + 1 UIListLayout + 1 for RoactTree + else + expect(#renderedChildren).to.equal(nodesRenderedCount + 1)-- should equal number of nodes + 1 UIListLayout + end + expect(nodesRenderedCount).to.equal(1) + + -- expand the root node, it should re-render the root node and its three children + nodesRenderedCount = 0 + expandChildFunc() + + -- it should have rendered the children + treeView = container:FindFirstChildOfClass("ScrollingFrame") + renderedChildren = treeView:GetChildren() + if FFlagStudioFixTreeViewForSquish then + count = 0 + dfCount(treeView) + expect(count).to.equal(nodesRenderedCount + 2)-- should equal number of nodes + 1 UIListLayout + 1 for RoactTree + else + expect(#renderedChildren).to.equal(nodesRenderedCount + 1)-- should equal number of nodes + 1 UIListLayout + end + + local foundChildNodes = 0 + local foundRoot = false + local foundChild1 = false + local foundChild2 = false + local foundChild3 = false + if FFlagStudioFixTreeViewForSquish then + local function dfs(node) + local children = node:GetChildren() + + if node:IsA("TextLabel") then + foundChildNodes = foundChildNodes + 1 + if node.Text == "Players" then + foundRoot = true + elseif node.Text == "John Doe" then + foundChild1 = true + elseif node.Text == "Jane Doe" then + foundChild2 = true + elseif node.Text == "Builderman" then + foundChild3 = true + end + end + + if #children == 0 then + return + end + + for _, child in ipairs(children) do + dfs(child) + end + end + + dfs(treeView) + else + for _, childNode in ipairs(renderedChildren) do + if childNode:IsA("TextLabel") then + foundChildNodes = foundChildNodes + 1 + if childNode.Text == "Players" then + foundRoot = true + elseif childNode.Text == "John Doe" then + foundChild1 = true + elseif childNode.Text == "Jane Doe" then + foundChild2 = true + elseif childNode.Text == "Builderman" then + foundChild3 = true + end + end + end + end + expect(foundChildNodes).to.equal(nodesRenderedCount) + expect(foundRoot).to.equal(true) + expect(foundChild1).to.equal(true) + expect(foundChild2).to.equal(true) + expect(foundChild3).to.equal(true) + + -- clean up + Roact.unmount(instance) + end) + + it("should allow you to select one or multiple elements in the tree", function() + local isRootSelected = false + local selectNodeFunc + + local function renderElement(props) + isRootSelected = props.isSelected + selectNodeFunc = props.toggleSelected + + return Roact.createElement("Frame") + end + + local element = Roact.createElement(MockWrapper, {}, { + TreeView = Roact.createElement(TreeView, { + dataTree = mockDataTree(), + getChildren = mockGetChildren, + renderElement = renderElement, + }), + }) + local container = Instance.new("Folder") + local instance = Roact.mount(element, container) + + -- select the root node + expect(isRootSelected).to.equal(false) + selectNodeFunc() + expect(isRootSelected).to.equal(true) + + Roact.unmount(instance) + end) + + it("should render all of the children immediately if expandAll is set", function() + local nodeCount = 0 + local renderElement = function(props) + nodeCount = nodeCount + 1 + return Roact.createElement("Frame", {}, props.children) + end + + local element = Roact.createElement(MockWrapper, {}, { + TreeView = Roact.createElement(TreeView, { + dataTree = mockDataTree(), + getChildren = mockGetChildren, + renderElement = renderElement, + expandAll = true, + }), + }) + local container = Instance.new("Folder") + local instance = Roact.mount(element, container) + + local treeView = container:FindFirstChildOfClass("ScrollingFrame") + local treeViewChildren = treeView:GetChildren() + + local count = 0 + local function dfCount(root) + local children = root:GetChildren() + count = count + 1 + + if #children == 0 then + return + end + + for _, child in ipairs(children) do + dfCount(child) + end + end + + -- mockDataTree has 8 nodes + expect(nodeCount).to.equal(8) + if FFlagStudioFixTreeViewForSquish then + dfCount(treeView) + expect(count).to.equal(nodeCount + 2) + else + -- there should be 8 nodes + 1 UIListLayout + expect(#treeViewChildren).to.equal(nodeCount + 1) + end + + Roact.unmount(instance) + end) + + itSKIP("should fire update callback", function() + local nodeCount = 0 + local renderElement = function(props) + nodeCount = nodeCount + 1 + return Roact.createElement("Frame", {}) + end + + local numInvoked = 0 + local treeList = nil + local element = Roact.createElement(MockWrapper, {}, { + TreeView = Roact.createElement(TreeView, { + dataTree = mockDataTree(), + getChildren = mockGetChildren, + renderElement = renderElement, + expandAll = true, + onTreeUpdated = function(tree) + treeList = tree + numInvoked = numInvoked + 1 + end, + }), + }) + local container = Instance.new("Folder") + local instance = Roact.mount(element, container) + + expect(treeList).to.be.ok() + expect(#treeList).to.equal(nodeCount) + expect(numInvoked).to.equal(1) + + Roact.unmount(instance) + end) + end) +end diff --git a/BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Components/createFitToContent.lua b/BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Components/createFitToContent.lua new file mode 100644 index 0000000000..014946770f --- /dev/null +++ b/BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Components/createFitToContent.lua @@ -0,0 +1,70 @@ +--[[ + This component generates a list of elements and resizes the container so that it fits the size of the list. + + Call the function createFitToContent and pass in the container, the layout of the elements, and any properties +]] +local FFlagFixFitToContentOnCloseError = game:DefineFastFlag("FixFitToContentOnCloseError", false) + +local Library = script.Parent.Parent +local Roact = require(Library.Parent.Parent.Roact) + +local join = require(Library.join) + +local function createFitToContent(containerComponent, layoutComponent, layoutProps) + local name = ("FitComponent(%s, %s)"):format(containerComponent, layoutComponent) + local FitComponent = Roact.Component:extend(name) + + function FitComponent:init() + self.layoutRef = Roact.createRef() + self.containerRef = Roact.createRef() + + self.layoutProps = join(layoutProps, { + [Roact.Ref] = self.layoutRef, + [Roact.Change.AbsoluteContentSize] = function() + if self.layoutRef.current ~= nil and self.containerRef.current ~= nil then + self:resizeContainer() + end + end, + }) + end + + function FitComponent:render() + assert(self.props.Size == nil, "Size must not be specified!") + + local children = join({ + ["Layout"] = Roact.createElement(layoutComponent, self.layoutProps), + }, self.props[Roact.Children]) + + local props = join(self.props, { + [Roact.Children] = children, + [Roact.Ref] = self.containerRef, + }) + + return Roact.createElement(containerComponent, props) + end + + function FitComponent:didMount() + self:resizeContainer() + end + + function FitComponent:didUpdate() + self:resizeContainer() + end + + function FitComponent:resizeContainer() + if FFlagFixFitToContentOnCloseError then + local layout = self.layoutRef.current + if layout then + local layoutSize = layout.AbsoluteContentSize + self.containerRef.current.Size = UDim2.new(1, 0, 0, layoutSize.Y) + end + else + local layoutSize = self.layoutRef.current.AbsoluteContentSize + self.containerRef.current.Size = UDim2.new(1, 0, 0, layoutSize.Y) + end + end + + return FitComponent +end + +return createFitToContent \ No newline at end of file diff --git a/BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Components/createFitToContent.spec.lua b/BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Components/createFitToContent.spec.lua new file mode 100644 index 0000000000..ed5fa5f239 --- /dev/null +++ b/BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Components/createFitToContent.spec.lua @@ -0,0 +1,44 @@ +return function() + local Library = script.Parent.Parent +local Roact = require(Library.Parent.Parent.Roact) + + local createFitToContent = require(script.Parent.createFitToContent) + + it("should create and destroy without errors", function() + local fitToContent = createFitToContent("Frame", "UIListLayout", {}) + local component = Roact.createElement(fitToContent, {}, {}) + local instance = Roact.mount(component) + + Roact.unmount(instance) + end) + + it("should throw an error if size is specified", function() + local fitToContent = createFitToContent("Frame", "UIListLayout", {}) + local component = Roact.createElement(fitToContent, { + Size = UDim2.new() + }, {}) + + expect(function() + Roact.mount(component) + end).to.throw() + end) + + it("should add a Layout to its children", function() + local container = Instance.new("Folder") + + local fitToContent = createFitToContent("Frame", "UIListLayout", {}) + local component = Roact.createElement(fitToContent, {}, { + Frame1 = Roact.createElement("Frame"), + Frame2 = Roact.createElement("Frame"), + }) + + local instance = Roact.mount(component, container) + local frame = container:FindFirstChildOfClass("Frame") + + expect(frame.Frame1).to.be.ok() + expect(frame.Frame2).to.be.ok() + expect(frame.Layout).to.be.ok() + + Roact.unmount(instance) + end) +end \ No newline at end of file diff --git a/BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Focus.lua b/BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Focus.lua new file mode 100644 index 0000000000..2aa90263cf --- /dev/null +++ b/BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Focus.lua @@ -0,0 +1,181 @@ +--[[ + A utility for elements that need to display on top of all other elements, + and elements that need to capture focus and block input to all other elements. + + Uses Portals to place elements in the main PluginGui. You will need to + pass in the main PluginGui when creating a FocusProvider. + + withFocus(pluginGui) + Gets the top-level PluginGui. Useful for querying its size or state. + For example, a Tooltip queries the size of the pluginGui to avoid + clipping the tooltip off of the right or bottom side of the screen. + + ShowOnTop + A Roact component that wraps its children such that they will be + rendered on top of all other components. + Props: + int Priority = The ZIndex of this component relative to other + focused elements. + + CaptureFocus + A Roact component that wraps its children such that they will be + rendered on top of all other components, and will block input to all + other components. + + Props: + int Priority = The ZIndex of this component relative to other + focused elements. + callback OnFocusLost = A callback for when the user clicks + outside of the focused element. + + KeyboardListener + A Roact component that listens to keyboard events within the PluginGui. + + Props: + callback OnKeyPressed(input, keysHeld) + A callback for when the user presses a key inside the plugin. + The input param is the InputObject for the InputBegan event. The + keysHeld param is a map containing every key that is currently held. + callback OnKeyReleased(input) + A callback for when the user releases a key. +]] + +local FOCUSED_ZINDEX = 100000 +local Library = script.Parent +local Roact = require(Library.Parent.Parent.Roact) +local Symbol = require(Library.Utils.Symbol) +local focusKey = Symbol.named("UILibraryFocus") + +local FocusProvider = Roact.PureComponent:extend("UILibraryFocusProvider") +function FocusProvider:init() + local pluginGui = self.props.pluginGui + assert(pluginGui ~= nil, "No pluginGui was given to this FocusProvider.") + + self._context[focusKey] = pluginGui +end +function FocusProvider:render() + return Roact.oneChild(self.props[Roact.Children]) +end + +-- the consumer should complain if it doesn't have a focus +local FocusConsumer = Roact.PureComponent:extend("UILibraryFocusConsumer") +function FocusConsumer:init() + assert(self._context[focusKey] ~= nil, "No FocusProvider found.") + assert(self.props.focusedRender ~= nil, "Use withFocus, not FocusConsumer.") + self.pluginGui = self._context[focusKey] +end +function FocusConsumer:render() + return self.props.focusedRender(self.pluginGui) +end + +-- withFocus should provide a simple way to make components that use focus +-- callback : function(FocusConsumer) +local function withFocus(callback) + return Roact.createElement(FocusConsumer, { + focusedRender = callback + }) +end + +local CaptureFocus = Roact.PureComponent:extend("UILibraryCaptureFocus") +function CaptureFocus:render() + return withFocus(function(pluginGui) + local priority = self.props.Priority or 0 + return Roact.createElement(Roact.Portal, { + target = pluginGui, + }, { + -- Consume all clicks outside the element to close it when it loses focus + TopLevelDetector = Roact.createElement("ImageButton", { + ZIndex = priority + FOCUSED_ZINDEX, + Size = UDim2.new(1, 0, 1, 0), + BackgroundTransparency = 1, + [Roact.Event.Activated] = self.props.OnFocusLost, + }, { + -- Also block all scrolling events going through + ScrollBlocker = Roact.createElement("ScrollingFrame", { + Size = UDim2.new(1, 0, 1, 0), + -- We need to have ScrollingEnabled = true for this frame for it to block + -- But we don't want it to actually scroll, so its canvas must be same size as the frame + ScrollingEnabled = true, + CanvasSize = UDim2.new(1, 0, 1, 0), + BackgroundTransparency = 1, + ScrollBarThickness = 0, + }, self.props[Roact.Children]), + }), + }) + end) +end + +local ShowOnTop = Roact.PureComponent:extend("UILibraryShowOnTop") +function ShowOnTop:render() + return withFocus(function(pluginGui) + local priority = self.props.Priority or 0 + return Roact.createElement(Roact.Portal, { + target = pluginGui, + }, { + TopLevelFrame = Roact.createElement("Frame", { + ZIndex = priority + FOCUSED_ZINDEX, + Size = UDim2.new(1, 0, 1, 0), + BackgroundTransparency = 1, + }, self.props[Roact.Children]), + }) + end) +end + +local KeyboardListener = Roact.PureComponent:extend("KeyboardListener") +function KeyboardListener:init() + self.keysHeld = {} + assert(self._context[focusKey] ~= nil, "No FocusProvider found.") + self.pluginGui = self._context[focusKey] + + self.onInputBegan = function(input) + if input.UserInputType == Enum.UserInputType.Keyboard then + self.keysHeld[input.KeyCode] = true + self.props.OnKeyPressed(input, self.keysHeld) + end + end + self.onInputEnded = function(input) + if input.UserInputType == Enum.UserInputType.Keyboard then + self.keysHeld[input.KeyCode] = nil + self.props.OnKeyReleased(input) + end + end + if self.pluginGui:IsA("DockWidgetPluginGui") then + self.focusConnection = self.pluginGui.WindowFocusReleased:Connect(function() + for key, _ in pairs(self.keysHeld) do + self.props.OnKeyReleased({ + KeyCode = key, + UserInputType = Enum.UserInputType.Keyboard, + }) + end + self.keysHeld = {} + end) + end +end +function KeyboardListener:render() + return Roact.createElement(ShowOnTop, {}, { + Listener = Roact.createElement("Frame", { + Size = UDim2.new(1, 0, 1, 0), + BackgroundTransparency = 1, + [Roact.Event.InputBegan] = function(_, input) + self.onInputBegan(input) + end, + [Roact.Event.InputEnded] = function(_, input) + self.onInputEnded(input) + end, + }), + }) +end +function KeyboardListener:willUnmount() + if self.focusConnection then + self.focusConnection:Disconnect() + end +end + +return { + Provider = FocusProvider, + Consumer = FocusConsumer, + CaptureFocus = CaptureFocus, + ShowOnTop = ShowOnTop, + KeyboardListener = KeyboardListener, + withFocus = withFocus, +} \ No newline at end of file diff --git a/BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Focus.spec.lua b/BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Focus.spec.lua new file mode 100644 index 0000000000..4be889f2e9 --- /dev/null +++ b/BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Focus.spec.lua @@ -0,0 +1,118 @@ +return function() + local Library = script.Parent + local Roact = require(Library.Parent.Parent.Roact) + + local MockWrapper = require(Library.MockWrapper) + + local Focus = require(script.Parent.Focus) + local ShowOnTop = Focus.ShowOnTop + local CaptureFocus = Focus.CaptureFocus + local KeyboardListener = Focus.KeyboardListener + + describe("ShowOnTop", function() + local function createTestShowOnTop(children, container) + return Roact.createElement(MockWrapper, { + Container = container, + }, { + ShowOnTop = Roact.createElement(ShowOnTop, {}, children) + }) + end + + it("should create and destroy without errors", function() + local element = createTestShowOnTop() + local instance = Roact.mount(element) + Roact.unmount(instance) + end) + + it("should render correctly", function() + local container = Instance.new("Folder") + local element = createTestShowOnTop({}, container) + local instance = Roact.mount(element) + + local gui = container:FindFirstChild("MockGui") + expect(gui).to.be.ok() + expect(gui.TopLevelFrame).to.be.ok() + Roact.unmount(instance) + end) + + it("should render its children", function() + local container = Instance.new("Folder") + local element = createTestShowOnTop({ + ChildFrame = Roact.createElement("Frame"), + }, container) + local instance = Roact.mount(element) + + local gui = container:FindFirstChild("MockGui") + expect(gui.TopLevelFrame.ChildFrame).to.be.ok() + Roact.unmount(instance) + end) + end) + + describe("CaptureFocus", function() + local function createTestCaptureFocus(children, container) + return Roact.createElement(MockWrapper, { + Container = container, + }, { + CaptureFocus = Roact.createElement(CaptureFocus, {}, children) + }) + end + + it("should create and destroy without errors", function() + local element = createTestCaptureFocus() + local instance = Roact.mount(element) + Roact.unmount(instance) + end) + + it("should render correctly", function() + local container = Instance.new("Folder") + local element = createTestCaptureFocus({}, container) + local instance = Roact.mount(element) + + local gui = container:FindFirstChild("MockGui") + expect(gui).to.be.ok() + expect(gui.TopLevelDetector).to.be.ok() + expect(gui.TopLevelDetector.ScrollBlocker).to.be.ok() + Roact.unmount(instance) + end) + + it("should render its children", function() + local container = Instance.new("Folder") + local element = createTestCaptureFocus({ + ChildFrame = Roact.createElement("Frame"), + }, container) + local instance = Roact.mount(element) + + local gui = container:FindFirstChild("MockGui") + expect(gui.TopLevelDetector.ScrollBlocker.ChildFrame).to.be.ok() + Roact.unmount(instance) + end) + end) + + describe("KeyboardListener", function() + local function createTestKeyboardListener(container) + return Roact.createElement(MockWrapper, { + Container = container, + }, { + KeyboardListener = Roact.createElement(KeyboardListener) + }) + end + + it("should create and destroy without errors", function() + local element = createTestKeyboardListener() + local instance = Roact.mount(element) + Roact.unmount(instance) + end) + + it("should render correctly", function() + local container = Instance.new("Folder") + local element = createTestKeyboardListener(container) + local instance = Roact.mount(element) + + local gui = container:FindFirstChild("MockGui") + expect(gui).to.be.ok() + expect(gui.TopLevelFrame).to.be.ok() + expect(gui.TopLevelFrame.Listener).to.be.ok() + Roact.unmount(instance) + end) + end) +end \ No newline at end of file diff --git a/BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Localizing.lua b/BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Localizing.lua new file mode 100644 index 0000000000..1c8f65af37 --- /dev/null +++ b/BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Localizing.lua @@ -0,0 +1,98 @@ +--[[ + A utility for elements that require text to be localized and display translated + strings. + + When initializing LocalizationProvider, it expects a Localization object, an example being + src/Studio/Localization.lua where there is two tables for development strings and translated + strings. withLocalization is mainly used to render elements with the localized strings using + the localization object passed into LocalizationProvider +]] + +local Library = script.Parent + +local Roact = require(Library.Parent.Parent.Roact) +local Symbol = require(Library.Utils.Symbol) + + +local localizationKey = Symbol.named("Localization") + +--[[ + Inserted near the top of the Roact tree, this stores the localization object into _context + + Props: + localization : (Localization) an object that can provide localized strings, preferrably a Localization object +]] +local LocalizationProvider = Roact.PureComponent:extend("LocalizationProvider") + +function LocalizationProvider:init() + assert(self.props.localization ~= nil, "LocalizationProvider expects a Localization object") + local localization = self.props.localization + + self._context[localizationKey] = localization +end + +function LocalizationProvider:render() + return Roact.oneChild(self.props[Roact.Children]) +end + + +--[[ + Never explicitly created by the user, it exposes the localization object from context. + This should only ever be created by a call to withLocalization(). + + Props: + localizedRender : (function(Localization localization)) + a callback used to render children while exposing the Localization object stored in _context +]] +local LocalizationConsumer = Roact.PureComponent:extend("LocalizationConsumer") + +function LocalizationConsumer:init() + assert(type(self.props.localizedRender) == "function", "LocalizationConsumer expected to be created with withLocale()") + assert(self._context[localizationKey] ~= nil, "LocalizationConsumer expects a LocalizationProvider in the Roact tree") + + self.localization = self._context[localizationKey] + self.state = { + -- keep a simple string of the table reference so we have something to call setstate on later + localization = tostring(self.localization.translator) + } + + self.lcToken = self.localization.localeChanged:connect(function(newLocale) + -- force trigger a re-render of children + self:setState({ + localization = tostring(newLocale) + }) + end) +end + +function LocalizationConsumer:render() + return self.props.localizedRender(self.localization) +end + +function LocalizationConsumer:willUnmount() + if self.lcToken then + self.lcToken:disconnect() + self.lcToken = nil + end +end + +--[[ + callback : function(Localization localization) + a callback used to render children while exposing the localization stored in _context +]] +local function withLocalization(callback) + assert(type(callback) == "function", "withLocalization expects a function") + return Roact.createElement(LocalizationConsumer, { + localizedRender = callback + }) +end + +local function getLocalization(component) + return component._context[localizationKey] +end + +return { + Provider = LocalizationProvider, + Consumer = LocalizationConsumer, + withLocalization = withLocalization, + getLocalization = getLocalization, +} diff --git a/BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Localizing.spec.lua b/BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Localizing.spec.lua new file mode 100644 index 0000000000..3840e0ef41 --- /dev/null +++ b/BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Localizing.spec.lua @@ -0,0 +1,157 @@ +local Localizing = require(script.Parent.Localizing) + +local Library = script.Parent +local Roact = require(Library.Parent.Parent.Roact) +local Signal = require(Library.Utils.Signal) +local Localization = require(Library.Studio.Localization) + +local LocalizationProvider = Localizing.Provider +local LocalizationConsumer = Localizing.Consumer +local withLocalization = Localizing.withLocalization + +return function() + describe("LocalizationProvider", function() + it("should construct/deconstruct without a problem", function() + local localization = Localization.mock() + + local root = Roact.createElement(LocalizationProvider, { + localization = localization + }) + local handle = Roact.mount(root) + Roact.unmount(handle) + + localization:destroy() + end) + + it("should error if a localization object isn't provided", function() + expect(function() + local root = Roact.createElement(LocalizationProvider, {}) + local instance = Roact.mount(root) + Roact.unmount(instance) + end).to.throw() + end) + end) + + + describe("LocalizationConsumer", function() + it("should construct/deconstruct without a problem when used appropriately", function() + local mockLocalization = Localization.mock() + + local function createTestElement() + return withLocalization(function(localization) + return Roact.createElement("TextLabel", { + Text = localization:getText("Anything", "test") + }) + end) + end + + local root = Roact.createElement(LocalizationProvider, { + localization = mockLocalization + },{ + Roact.createElement(createTestElement, {}) + }) + + local handle = Roact.mount(root) + Roact.unmount(handle) + + mockLocalization:destroy() + end) + + it("should error if constructed without a LocalizationProvider in the Roact tree", function() + local function createTestElement() + return withLocalization(function(localization) + return Roact.createElement("TextLabel", { + Text = localization:getText("Anything", "test") + }) + end) + end + + expect(function() + local root = Roact.createElement(createTestElement) + local instance = Roact.mount(root) + Roact.unmount(instance) + end).to.throw() + end) + + it("should error if you construct it on its own", function() + expect(function() + local root = Roact.createElement(LocalizationConsumer, {}) + local instance = Roact.mount(root) + Roact.unmount(instance) + end).to.throw() + end) + + it("should re-render its contents if the localization changes", function() + local changeSignal = Signal.new() + local localization = Localization.mock(changeSignal) + + -- create a test element and keep track of how many times it renders + local renderCount = 0 + + local TestElement = Roact.PureComponent:extend("TestElement") + function TestElement:render() + renderCount = renderCount + 1 + local text = self.props.text + + return Roact.createElement("TextLabel", { + Text = text + }) + end + + -- create the roact tree + local root = Roact.createElement(LocalizationProvider, { + localization = localization + },{ + Roact.createElement(function() + return withLocalization(function(localizationObject) + return Roact.createElement(TestElement, { + text = localizationObject:getText("Test", "hello_world") + }) + end) + end) + }) + + local instance = Roact.mount(root) + expect(renderCount).to.equal(1) + + -- trigger a locale change + changeSignal:fire() + expect(renderCount).to.equal(2) + + -- clean up + Roact.unmount(instance) + localization:destroy() + end) + end) + + + describe("withLocalization()", function() + it("should error if a render callback isn't provided", function() + expect(function() + withLocalization() + end).to.throw() + end) + + it("should expose the stored localization object", function() + local mockLocalization = Localization.mock() + local foundLocalization = nil + + local function localizedRender(localization) + foundLocalization = localization + return Roact.createElement("TextLabel") + end + + local root = Roact.createElement(LocalizationProvider, { + localization = mockLocalization + },{ + Roact.createElement(function() + return withLocalization(localizedRender) + end) + }) + local instance = Roact.mount(root) + Roact.unmount(instance) + + expect(foundLocalization).to.equal(mockLocalization) + end) + end) +end \ No newline at end of file diff --git a/BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/MockWrapper.lua b/BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/MockWrapper.lua new file mode 100644 index 0000000000..1dc9b74743 --- /dev/null +++ b/BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/MockWrapper.lua @@ -0,0 +1,44 @@ +--[[ + USE IN TESTS ONLY + Provides mocks of all necessary context items for testing. +]] + +local Library = script.Parent +local Roact = require(Library.Parent.Parent.Roact) + +local UILibraryWrapper = require(Library.UILibraryWrapper) + +local createTheme = require(Library.createTheme) +local dummyTheme = createTheme() + +local MockWrapper = Roact.PureComponent:extend("MockWrapper") + +local function createMockPlugin(container) + return { + CreateQWidgetPluginGui = function() + return Instance.new("BillboardGui", container) + end + } +end + +function MockWrapper:init(props) + local container = props.Container + + self.mockGui = Instance.new("ScreenGui", container) + self.mockGui.Name = "MockGui" + self.mockPlugin = createMockPlugin(container) +end + +function MockWrapper:render() + return Roact.createElement(UILibraryWrapper, { + theme = dummyTheme, + focusGui = self.mockGui, + plugin = self.mockPlugin, + }, self.props[Roact.Children]) +end + +function MockWrapper:willUnmount() + self.mockGui:Destroy() +end + +return MockWrapper \ No newline at end of file diff --git a/BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Plugin.lua b/BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Plugin.lua new file mode 100644 index 0000000000..1eb3a1fec5 --- /dev/null +++ b/BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Plugin.lua @@ -0,0 +1,28 @@ +local Library = script.Parent + +local Roact = require(Library.Parent.Parent.Roact) +local Symbol = require(Library.Utils.Symbol) +local pluginKey = Symbol.named("Plugin") + +local PluginProvider = Roact.PureComponent:extend("PluginProvider") +function PluginProvider:init() + local plugin = self.props.plugin + assert(plugin ~= nil, "No plugin was given to this PluginProvider.") + + self._context[pluginKey] = plugin +end +function PluginProvider:render() + return Roact.oneChild(self.props[Roact.Children]) +end + +-- Gets the plugin at the passed in component's context. +local function getPlugin(component) + assert(component._context[pluginKey] ~= nil, "No PluginProvider found.") + local plugin = component._context[pluginKey] + return plugin +end + +return { + Provider = PluginProvider, + getPlugin = getPlugin, +} \ No newline at end of file diff --git a/BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Studio/Analytics.lua b/BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Studio/Analytics.lua new file mode 100644 index 0000000000..e08e32d7dc --- /dev/null +++ b/BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Studio/Analytics.lua @@ -0,0 +1,123 @@ +--[[ + Providable helper for sending analytics events and reporting counters. + Consumers can use this utility as a wrapper for AnalyticsService, + allowing mock Analytics objects to be made for testing without sending + actual analytics calls. + + function Analytics.new(props) + Creates a new Analytics object with the given props. + Props: + string Target = The target namespace to send EventStream events to. + For Studio plugins, this is usually "studio". If no Target is + provided, this defaults to "studio". + string Context = The context of the namespace to send EventStream + events to. This will usually match or reference the name of the plugin. + bool LogEvents = Whether to log Analytics events to the console. + + function Analytics.mock() + Creates a mock Analytics object which will do nothing, but provides a + proxy for every function so that it can be safely used for testing. + + function Analytics:sendEvent(string eventName, table additionalArgs) + Sends an EventStream event. The additionalArgs table can be used to pass + more info along with the event itself. + + function Analytics:reportCounter(string counterName, number num = 1) + Reports number num to the RCity bucket named counterName. Used for + iterating ephemeral counters. +]] + +local RbxAnalyticsService = game:GetService("RbxAnalyticsService") +local HttpService = game:GetService("HttpService") +local StudioService = game:GetService("StudioService") + +local Library = script.Parent.Parent +local join = require(Library.join) + +local Analytics = {} +Analytics.__index = Analytics + +function Analytics.new(props) + assert(type(props) == "table", "Analytics props is expected to be a table.") + assert(props.Context, "Analytics expected a context string.") + + local self = { + senders = props.Senders or RbxAnalyticsService, + logEvents = props.LogEvents, + + target = props.Target or "studio", + context = props.Context, + + placeId = game.PlaceId, + userId = StudioService:GetUserId(), + } + setmetatable(self, Analytics) + + self.sessionId = self.senders:GetSessionId() + self.clientId = self.senders:GetClientId() + + return self +end + +-- EventStream events handler +function Analytics:sendEventDeferred(eventName, additionalArgs) + self:logEvent(eventName, additionalArgs) + local args = join(additionalArgs, { + studioSid = self.sessionId, + clientId = self.clientId, + placeId = self.placeId, + userId = self.userId, + }) + self.senders:SendEventDeferred(self.target, self.context, eventName, args) +end + +-- RCity Ephemeral Counters handler +function Analytics:reportCounter(counterName, num) + self:logCounter(counterName, num) + self.senders:ReportCounter(counterName, num or 1) +end + +function Analytics:reportStats(statName, num) + self:logStats(statName, num) + self.senders:ReportStats(statName, num) +end + +function Analytics:logEvent(eventName, tab) + if self.logEvents then + local readableTable = HttpService:JSONEncode(tab) + print(string.format("Analytics: sendEventDeferred: \"%s\", %s", eventName, readableTable)) + end +end + +function Analytics:logCounter(counterName, value) + if self.logEvents then + print(string.format("Analytics: reportCounter: \"%s\", %s", counterName, value)) + end +end + +function Analytics:logStats(statName, value) + if self.logEvents then + print(string.format("Analytics: reportStats: \"%s\", %s", statName, value)) + end +end + +function Analytics.mock(props) + return Analytics.new(join(props, { + Senders = { + SendEventDeferred = function() + end, + ReportCounter = function() + end, + ReportStats = function() + end, + GetSessionId = function() + return 0 + end, + GetClientId = function() + return 0 + end, + } + })) +end + +return Analytics \ No newline at end of file diff --git a/BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Studio/ContextMenus.lua b/BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Studio/ContextMenus.lua new file mode 100644 index 0000000000..a49b560b3c --- /dev/null +++ b/BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Studio/ContextMenus.lua @@ -0,0 +1,42 @@ +--[[ + A Roact wrapper for the PluginMenu API. + + Props: + table Actions = The set of actions to send to MakePluginMenu. + function OnMenuOpened() = A callback for when the context menu has successfully opened. +]] + +local Library = script.Parent.Parent +local Roact = require(Library.Parent.Parent.Roact) + +local PluginContext = require(Library.Plugin) +local getPlugin = PluginContext.getPlugin +local PluginMenus = require(Library.Studio.PluginMenus) + +local ContextMenu = Roact.PureComponent:extend("ContextMenu") + +function ContextMenu:showMenu() + local props = self.props + local actions = props.Actions + local plugin = getPlugin(self) + + props.OnMenuOpened() + PluginMenus.makePluginMenu(plugin, actions) +end + +function ContextMenu:didMount() + self:showMenu() +end + +function ContextMenu:didUpdate() + self:showMenu() +end + +function ContextMenu:render() + return nil +end + +return { + ContextMenu = ContextMenu, + Separator = PluginMenus.Separator, +} \ No newline at end of file diff --git a/BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Studio/Hyperlink.lua b/BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Studio/Hyperlink.lua new file mode 100644 index 0000000000..f7a64aa58c --- /dev/null +++ b/BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Studio/Hyperlink.lua @@ -0,0 +1,60 @@ +--[[ + A widget that contains a hyperlink. + + Props: + bool Enabled = Whether this widget should be interactable. + string Text = The hyperlink text + int TextSize = The size of the text + int LayoutOrder = The order in which this element is displayed if in a UIListLayout. + function OnClick = what happens when the hyperlink is clicked + Mouse = plugin mouse for changing the mouse icon +]] + +local Library = script.Parent.Parent +local Roact = require(Library.Parent.Parent.Roact) + +local Mouse = require(script.Parent.Internal.Mouse) + +local Theming = require(Library.Theming) +local withTheme = Theming.withTheme + +local Hyperlink = Roact.PureComponent:extend("hyperlink") + +local function calculateTextSize(text, textSize, font) + local hugeFrameSizeNoTextWrapping = Vector2.new(5000, 5000) + local size = game:GetService("TextService"):GetTextSize(text, textSize, font, hugeFrameSizeNoTextWrapping) + return UDim2.new(0, size.X, 0, size.Y) +end + +function Hyperlink:render() + return withTheme(function(theme) + if self.props.Enabled == nil then + self.props.Enabled = true + end + + local textSize = self.props.TextSize or 22 + + return Roact.createElement("TextButton", { + BackgroundTransparency = 1, + Text = self.props.Text, + TextSize = textSize, + Font = Enum.Font.SourceSans, + TextColor3 = theme.hyperlink.textColor, + Size = self.props.Size or calculateTextSize(self.props.Text, textSize, Enum.Font.SourceSans), + Position = self.props.Position, + TextXAlignment = Enum.TextXAlignment.Left, + LayoutOrder = self.props.LayoutOrder, + + [Roact.Event.MouseEnter] = function() if self.props.Enabled then Mouse.onEnter(self.props.Mouse) end end, + [Roact.Event.MouseLeave] = function() if self.props.Enabled then Mouse.onLeave(self.props.Mouse) end end, + + [Roact.Event.Activated] = function() + if self.props.Enabled and nil ~= self.props.OnClick then + self.props.OnClick() + end + end, + }) + end) +end + +return Hyperlink \ No newline at end of file diff --git a/BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Studio/Internal/Mouse.lua b/BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Studio/Internal/Mouse.lua new file mode 100644 index 0000000000..8db89bd316 --- /dev/null +++ b/BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Studio/Internal/Mouse.lua @@ -0,0 +1,19 @@ +--[[ + Mouse helper functions +]] + +local Mouse = {} + +function Mouse.onEnter(pluginMouse, iconName) + if pluginMouse then + pluginMouse.Icon = iconName and "rbxasset://SystemCursors/" .. iconName or "rbxasset://SystemCursors/PointingHand" + end +end + +function Mouse.onLeave(pluginMouse) + if pluginMouse then + pluginMouse.Icon = "rbxasset://SystemCursors/Arrow" + end +end + +return Mouse \ No newline at end of file diff --git a/BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Studio/Localization.lua b/BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Studio/Localization.lua new file mode 100644 index 0000000000..bec44c12bb --- /dev/null +++ b/BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Studio/Localization.lua @@ -0,0 +1,284 @@ +--[[ + Reads data out of the localization table, provides a simple interface for fetching strings + + Props : + stringResourceTable : (CSV localization file) the file with the English strings, used for development + translationResourceTable : (CSV localization file) the file with all of the translated strings + pluginName : (string) the "plugin_name" field used in the localization file's keys + + Optional Props : + namespace : (string) the namespace of all keys in the localization, defaults to "Studio" + overrideGetLocale : (function(void)) a function that returns a localeId + overrideLocaleId : (string) a locale used to ignore the current locale set by Roblox + overrideLocaleChangedSignal : (Signal) a signal that the user has changed to a different language + overrideTranslator : (function<>()) + + - NOTE - + To make the localization resource files backend friendly, the keys should be structured like this: + ... + + For formatted strings, follow this guide online : https://developer.roblox.com/articles/localization-format-strings + + For example, your DevelopmentReferenceTable.csv should look something like this : + Key,Context,Example,Source,en + Studio.MoneyManager.Currency.Robux,the name displayed for Robux,,,R$ + Studio.MoneyManager.Currency.USD,the name displayed for US dollars,,,USD + Studio.MoneyManager.Sell.LimitedsTitle,the page title for selling limited items,,,Sell your limiteds + Studio.MoneyManager.Sell.LimitedValue,shows how much an item is worth,,,{1:translate} is worth {2:fixed} {3:translate} + + And your TranslationReferenceTable.csv should look something like this : (line breaks added for readability) + Key,Context,Example,Source,de,es,es-es,ja,ko + Studio.MoneyManager.Sell.LimitedValue,{1:translate} ist fünf {2:fixed} {3:translate}, + {1:translate} vale {2:fixed} {3:translate}, + {1:translate} vale {2:fixed} {3:translate}, + {1:translate}は{2:fixed}{3:translate}の価値がある, + {1:translate}은{2:fixed}{3:translate}가치가있다. + + (it is okay for keys to be missing in this file. This file can be empty and that's fine) + + + Localization Usage : + local rsTable = script.Parent.DevelopmentReferenceTable + local trsTable = script.Parent.TranslationReferenceTable + local pluginLocalization = Localization.new({ + stringResourceTable = rsTable, + translationResourceTable = trsTable, + pluginName = "MoneyManager" + }) + local example = pluginLocalization:getText("Sell", "LimitedsValue", "Valkyrie Helm", 71850, "R$") +]] + +game:DefineFastFlag("FixStudioLocalizationLocaleId", false) + +-- services +local LocalizationService = game:GetService("LocalizationService") +local StudioService = game:GetService("StudioService") + +-- libraries +local Library = script.Parent.Parent +local Signal = require(Library.Utils.Signal) + +-- constants +local FALLBACK_LOCALE = "en-us" + + +local Localization = {} +Localization.__index = Localization + +function Localization.new(props) + assert(type(props) == "table", "Localization props is expected to be a table.") + assert(props.stringResourceTable ~= nil, "Localization must have a .csv string resource table for English strings") + assert(props.translationResourceTable ~= nil, "Localization must have a .csv string resource table of translations") + assert(type(props.pluginName) == "string", "Please specify the plugin's name") + + local stringResourceTable = props.stringResourceTable + local translationResourceTable = props.translationResourceTable + local overrideGetLocale = props.getLocale + local overrideLocaleId = props.overrideLocaleId + local overrideLocaleChangedSignal = props.overrideLocaleChangedSignal + local keyNamespace = props.namespace + local keyPluginName = props.pluginName + + if keyNamespace == nil then + keyNamespace = "Studio" + end + + local externalLocaleChanged + if overrideLocaleChangedSignal then + externalLocaleChanged = overrideLocaleChangedSignal + elseif game:GetFastFlag("FixStudioLocalizationLocaleId") then + externalLocaleChanged = StudioService:GetPropertyChangedSignal("StudioLocaleId") + else + externalLocaleChanged = LocalizationService:GetPropertyChangedSignal("RobloxLocaleId") + end + + -- a function that gets called when the locale changes, returns the new locale + local function getLocale() + if overrideGetLocale then + return overrideGetLocale() + end + + if overrideLocaleId ~= nil then + return overrideLocaleId + elseif game:GetFastFlag("FixStudioLocalizationLocaleId") then + return StudioService["StudioLocaleId"] + else + return LocalizationService["RobloxLocaleId"] + end + end + + local self = { + -- localeChanged : (Signal) + -- a public facing signal for Localization consumers to observe updates + localeChanged = Signal.new(), + + -- externalLocaleChanged : (Signal) + -- the system signal fired when a user changes their language settings + externalLocaleChanged = externalLocaleChanged, + + -- externalLocaleChangedConnection : (connection token) + -- a subscription token for cleaning up the connection + externalLocaleChangedConnection = nil, + + -- locale : (string) + -- an id for knowing which translation to read from. ex) "en-us" + locale = FALLBACK_LOCALE, + + -- keyNamespace : (string) + -- the first field used to construct a key + keyNamespace = keyNamespace, + + -- keyPluginName : (string) + -- the second field used to construct a key + keyPluginName = keyPluginName, + + -- getLocale : (function()) + -- gets the current locale string + getLocale = getLocale, + + -- stringResourceTable : a CSV file containing all of the English strings + -- this is converted into a proper resource by Rojo + stringResourceTable = stringResourceTable, + + -- translationResourceTable : a CSV file containing all of the translated strings + -- this is converted into a proper resource by Rojo + translationResourceTable = translationResourceTable, + + -- translator & fallbackTranslator : (Translator) + -- objects that handle the string formatting from the current stringResourceTable + translator = nil, + fallbackTranslator = nil + } + setmetatable(self, Localization) + + -- listen to changes to the locale to alert all listeners of the change + self.localeChangedConnection = self.externalLocaleChanged:connect(function() + self:updateLocaleAndTranslator() + self.localeChanged:fire(self.locale) + end) + + -- create the translators for the first time + self:updateLocaleAndTranslator() + + return self +end + +-- scope : (string) the general group of data that the key belongs to +-- key : (string) the id of the string in the resource table +-- ... : (optional, Variant) values used to format a string +function Localization:getText(scope, key, ...) + assert(type(scope) == "string", "Cannot fetch the string without a scope") + assert(type(key) == "string", "Cannot fetch a string without the key") + + local stringKey = string.format("%s.%s.%s.%s", self.keyNamespace, self.keyPluginName, scope, key) + local args = {...} + + local function getTranslation(translator) + if not translator then + return false, nil + end + + local success, result = pcall(function() + return translator:FormatByKey(stringKey, args) + end) + return success, result + end + + -- optimize for one lookup when the locale is English + local success + local translated + if self.locale == FALLBACK_LOCALE then + -- English strings are only written into the development string table, + -- so don't bother looking up the key in the localization table. + success, translated = getTranslation(self.fallbackTranslator) + if success then + return translated + end + + else + -- try to find a translation in our translation file + success, translated = getTranslation(self.translator) + if success then + return translated + end + + -- If no translation exists for this locale id, fall back to default (English) + success, translated = getTranslation(self.fallbackTranslator) + if success then + return translated + end + end + + -- Fall back to the given key if there is no translation for this value + -- Useful for finding misspelled or missing keys + return stringKey +end + +function Localization:destroy() + if self.localeChangedConnection then + self.localeChangedConnection:disconnect() + end +end + +function Localization:updateLocaleAndTranslator() + -- the locale has changed, update the translators + self.locale = self.getLocale() + self.translator = self.translationResourceTable:GetTranslator(self.locale) + self.fallbackTranslator = self.stringResourceTable:GetTranslator(FALLBACK_LOCALE) +end + +-- changeSignal : (Signal, optional) a signal to trigger localization changes +function Localization.mock(localizationChangedSignal) + local changeSignal + if localizationChangedSignal then + changeSignal = localizationChangedSignal + else + changeSignal = Signal.new() + end + + -- any time the localizationChangedSignal fires, this will get the next one + -- this should trigger re-renders for any elements + local currentLocale = 0 + local localeIDs = {"en-us", "es", "es-es", "ko", "ja"} + local function getLocale() + currentLocale = (currentLocale + 1) % 5 + local nextLocale = localeIDs[currentLocale] + return nextLocale + end + + local fakeResourceTable = { + GetTranslator = function(stringResourceTableSelf, localeId) + local translator = { + FormatByKey = function(translatorSelf, key, args) + if not args then + args = {} + elseif type(args) ~= "table" then + error("Args must be a table") + end + + -- return a string like en-us|TEST.MOCK_LOCALIZATION.A.hello_world:[a,b,c,10] + return string.format("%s|%s:[%s]", localeId,key, table.concat(args, ",")) + end, + } + + return translator + end + } + + -- create a fake localization object for tests + return Localization.new({ + -- create a fake resource file that mimics the real thing + stringResourceTable = fakeResourceTable, + translationResourceTable = fakeResourceTable, + + namespace = "TEST", + pluginName = "MOCK_LOCALIZATION", + + -- for tests, don't connect to any system signals to ensure stuff doesn't change mid test + overrideLocaleChangedSignal = changeSignal, + getLocale = getLocale, + }) +end + + +return Localization \ No newline at end of file diff --git a/BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Studio/Localization.spec.lua b/BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Studio/Localization.spec.lua new file mode 100644 index 0000000000..ede3c1b091 --- /dev/null +++ b/BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Studio/Localization.spec.lua @@ -0,0 +1,228 @@ +local Localization = require(script.Parent.Localization) + +local Library = script.Parent.Parent +local Signal = require(Library.Utils.Signal) + +local TestLocalizationChangedSignal = Signal.new() +local TestDevStrings = Library.Studio.TestDevStrings +local TestTranslationStrings = Library.Studio.TestTranslationStrings + + + +return function() + -- since Localization connects to system signals, it's important to clean up after the test + describe("Localization", function() + it("should construct with the correct inputs", function() + local localization = Localization.new({ + stringResourceTable = TestDevStrings, + translationResourceTable = TestTranslationStrings, + pluginName = "UILibrary", + }) + expect(localization).to.be.ok() + + localization:destroy() + end) + + it("should error if it is missing any props", function() + expect(function() + Localization.new() + end).to.throw() + + expect(function() + Localization.new({}) + end).to.throw() + + expect(function() + Localization.new({ stringResourceTable = TestDevStrings }) + end).to.throw() + + expect(function() + Localization.new({ translationResourceTable = TestTranslationStrings }) + end).to.throw() + + expect(function() + Localization.new({ pluginName = "UILibrary" }) + end).to.throw() + end) + + it("should return localized strings when given keys to look up", function() + local localization = Localization.new({ + stringResourceTable = TestDevStrings, + translationResourceTable = TestTranslationStrings, + pluginName = "UILibrary", + overrideLocaleId = "en-us", + overrideLocaleChangedSignal = TestLocalizationChangedSignal + }) + + local greeting = localization:getText("Spec", "greeting_formal") + expect(greeting).to.equal("Hello") + + localization:destroy() + end) + + it("should return a formatted string when args are provided", function() + local localization = Localization.new({ + stringResourceTable = TestDevStrings, + translationResourceTable = TestTranslationStrings, + pluginName = "UILibrary", + overrideLocaleId = "en-us", + overrideLocaleChangedSignal = TestLocalizationChangedSignal + }) + + local greeting = localization:getText("Spec", "greeting_informal", "Builderman", 2) + expect(greeting).to.equal("Sup Builderman, I haven't seen you in 2 days") + + localization:destroy() + end) + + it("should return the English text of a string if a translation is missing in the resourceTable", function() + local localization = Localization.new({ + stringResourceTable = TestDevStrings, + translationResourceTable = TestTranslationStrings, + pluginName = "UILibrary", + overrideLocaleId = "es-es", + overrideLocaleChangedSignal = TestLocalizationChangedSignal + }) + + local normal = localization:getText("Spec", "greeting_formal") + expect(normal).to.equal("Hola") + + local informal = localization:getText("Spec", "greeting_informal", "John Doe", 100) + expect(informal).to.equal("¿Qué pasa John Doe? No te he visto en 100 días") + + local surprise = localization:getText("Spec", "greeting_surprise") + expect(surprise).to.equal("No one expects the Spanish Inquisition!") + + localization:destroy() + end) + + it("should return the key if the string does not exist in the resourceTable at all", function() + local localization = Localization.new({ + stringResourceTable = TestDevStrings, + translationResourceTable = TestTranslationStrings, + pluginName = "UILibrary", + overrideLocaleId = "en-us", + overrideLocaleChangedSignal = TestLocalizationChangedSignal + }) + + local greeting = localization:getText("Spec", "greeting_serious") + expect(greeting).to.equal("Studio.UILibrary.Spec.greeting_serious") + + localization:destroy() + end) + + it("should update its strings if the localization changes", function() + local changeSignal = Signal.new() + local currentLocale = "en-us" + local function getLocale() + return currentLocale + end + + local localization = Localization.new({ + stringResourceTable = TestDevStrings, + translationResourceTable = TestTranslationStrings, + pluginName = "UILibrary", + getLocale = getLocale, + overrideLocaleChangedSignal = changeSignal + }) + + local greeting = localization:getText("Spec", "greeting_formal") + expect(greeting).to.equal("Hello") + + -- trigger a locale change + currentLocale = "es-es" + changeSignal:fire() + + greeting = localization:getText("Spec", "greeting_formal") + expect(greeting).to.equal("Hola") + + + localization:destroy() + end) + + it("should remove the observer to the localization changed signal when it is destroyed", function() + local changeSignal = Signal.new() + local currentLocale = "en-us" + local callCount = 0 + local function getLocale() + callCount = callCount + 1 + return currentLocale + end + + local localization = Localization.new({ + stringResourceTable = TestDevStrings, + translationResourceTable = TestTranslationStrings, + pluginName = "UILibrary", + getLocale = getLocale, + overrideLocaleChangedSignal = changeSignal + }) + + expect(callCount).to.equal(1) + + local greeting = localization:getText("Spec", "greeting_formal") + expect(greeting).to.equal("Hello") + + -- trigger a locale change + currentLocale = "es-es" + changeSignal:fire() + + greeting = localization:getText("Spec", "greeting_formal") + expect(greeting).to.equal("Hola") + expect(callCount).to.equal(2) + + -- destroy the connection and trigger another change + localization:destroy() + currentLocale = "en-us" + changeSignal:fire() + + greeting = localization:getText("Spec", "greeting_formal") + expect(greeting).to.equal("Hola") + expect(callCount).to.equal(2) + end) + + it("should fallback to the base language if it is available when a specific locale isn't supported", function() + local localization = Localization.new({ + stringResourceTable = TestDevStrings, + translationResourceTable = TestTranslationStrings, + pluginName = "UILibrary", + overrideLocaleId = "es-mx", + }) + + local greeting = localization:getText("Spec", "greeting_formal") + expect(greeting).to.equal("Hola") + end) + end) + + + describe("Localization.mock()", function() + it("should always return a string, even without the actual resourceTable", function() + local mock = Localization.mock() + + -- expect it to return someting, if not the real string + -- something like : |...:[list of args] + local strA = mock:getText("Anything", "greeting_formal") + expect(strA).to.equal("en-us|TEST.MOCK_LOCALIZATION.Anything.greeting_formal:[]") + + local strB = mock:getText("Anything", "greeting_informal", "Jane Doe", 1) + expect(strB).to.equal("en-us|TEST.MOCK_LOCALIZATION.Anything.greeting_informal:[Jane Doe,1]") + + mock:destroy() + end) + + it("should allow for an external signal to fake locale changes", function() + local testSignal = Signal.new() + local mockLocalization = Localization.mock(testSignal) + + local callCount = 0 + local mockToken = mockLocalization.localeChanged:connect(function() + callCount = callCount + 1 + end) + + testSignal:fire() + expect(callCount).to.equal(1) + + mockToken:disconnect() + mockLocalization:destroy() + end) + end) +end \ No newline at end of file diff --git a/BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Studio/PartialHyperLink.lua b/BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Studio/PartialHyperLink.lua new file mode 100644 index 0000000000..ef8cf9b89b --- /dev/null +++ b/BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Studio/PartialHyperLink.lua @@ -0,0 +1,39 @@ +local Library = script.Parent.Parent +local Roact = require(Library.Parent.Parent.Roact) + +local StudioWidgetHyperlink = require(script.Parent.Hyperlink) + +local PartialHyperlink = Roact.PureComponent:extend("PartialHyperlink") + +local function calculateTextSize(text, textSize, font) + local hugeFrameSizeNoTextWrapping = Vector2.new(5000, 5000) + return game:GetService('TextService'):GetTextSize(text, textSize, font, hugeFrameSizeNoTextWrapping) +end + + +function PartialHyperlink:render() + local hyperLinkTextSize = calculateTextSize(self.props.HyperLinkText, self.props.Theme.fontStyle.Normal.TextSize, self.props.Theme.fontStyle.Normal.Font) + return Roact.createElement("Frame", { + Size = UDim2.new(1, 0, 0, hyperLinkTextSize.Y), + BackgroundTransparency = 1, + }, { + HyperLink = Roact.createElement(StudioWidgetHyperlink, { + Text = self.props.HyperLinkText, + Size = UDim2.new(0, hyperLinkTextSize.X, 0, hyperLinkTextSize.Y), + Mouse = self.props.Mouse, + OnClick = self.props.OnClick, + }), + TextLabel = Roact.createElement("TextLabel", { + BackgroundTransparency = 1, + Position = UDim2.new(0, hyperLinkTextSize.X, 0, 0), + Size = UDim2.new(1, -hyperLinkTextSize.X, 1, 0), + TextColor3 = self.props.Theme.fontStyle.Normal.TextColor3, + Font = Enum.Font.SourceSans, + TextSize = 22, + TextXAlignment = Enum.TextXAlignment.Left, + Text = self.props.NonHyperLinkText, + }), + }) +end + +return PartialHyperlink \ No newline at end of file diff --git a/BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Studio/PluginMenus.lua b/BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Studio/PluginMenus.lua new file mode 100644 index 0000000000..ee9caf58b9 --- /dev/null +++ b/BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Studio/PluginMenus.lua @@ -0,0 +1,131 @@ +--[[ + Returns a Separator constant and makePluginMenu. makePluginMenu creates a PluginMenu made up of + PluginActions, or SubMenus of PluginActions. + + Parameters: + plugin = The Roblox plugin instance that this menu belongs to + entries = a list of actions to be displayed in the menu. Actions can be a PluginAction instance + created externally, the SEPARATOR constant, or a dictionary containing action information that + will be used to create a new action. Action dictionaries have the following fields: + + Text = the action text displayed in the menu + ItemSelected = a callback that is invoked whenever the action is clicked + [Key] = an optional value provided as an argument to the ItemSelected callback. Can be nil if you + aren't sharing ItemSelected callbacks between actions + [Icon] = an optional icon displayed next to the action in the menu + [Checked] optionally show a check mark next to the action in the menu. Ignored if the action is in + a selection submenu (see below) + [Enabled] optionally control whether an action is selectable or not. Defaults to true + + If you insert one or more actions into the dictionary, the dictionary it was inserted into will + become a submenu. Submenus have the same properties as actions since the submenu is an action, + but ItemSelected will never be invoked. + + There is also a special kind of selection submenu that will automatically display a checkmark next + to a selected action. If you include a CurrentKey field in the submenu, any action whose Key field + equals CurrentKey will have a checkmark displayed. It is the responsibility of the consumer to use + the actions' ItemSelected callback to update the CurrentKey field of the submenu. + + Examples: + + Explorer context menu: + { + -- Ordinary actions + { Text = "Cut", Icon = "rbxasset://textures/cutIcon.png", ItemSelected = function() cutSelection() end }, + { Text = "Copy", Icon = "rbxasset://textures/copyIcon.png", ItemSelected = function() copySelection() end }, + { Text = "Paste Into", Enabled = false, ItemSelected = function() pasteIntoSelection() end }, + ... + PluginMenus.Separator, + ... + -- A submenu action with two inner items (the inner items can also be submenus) + { + Text = "Insert Object", Icon = "insertObjects.png", Enabled = true, + { Text = "Part", Key = partKey, Icon = "part.png", ItemSelected = function(key) insertObject(key) end }, + { Text = "Wedge", Key = wedgeKey, Icon = "wedge.png", ItemSelected = function(key) insertObject(key) end }, + ... + }, + } + + Tools Context Menu + { + { Text = "Collisions Enabled", Checked = true, ItemSelected = function(text) toggleCollisions() end }, + { Text = "Constraints Enabled", Checked = false, ItemSelected = function(text) toggleConstraints() end }, + { + CurrentKey = alwaysKey, Text = "Join Mode", Icon = "rbxasset://textures/joinMode.png", + { Text = "Always", Key = alwaysKey, ItemSelected = function(key) joinModeSelected(key) end }, + { Text = "None", Key = noneKey, ItemSelected = function(key) joinModeSelected(key) end }, + } + } +]] + +-- Importing HttpService only for GenerateGUID +local HttpService = game:GetService("HttpService") + +local Library = script.Parent.Parent +local Symbol = require(Library.Utils.Symbol) + +local SEPARATOR = Symbol.named("(PluginMenuSeparator)") + +local function newId() + return HttpService:GenerateGUID() +end + +local function connectAction(connections, action, entry, item) + table.insert(connections, action.Triggered:Connect(function() + for _, connection in ipairs(connections) do + connection:Disconnect() + end + entry.ItemSelected(item) + end)) +end + +local function createPluginMenu(plugin, entries, subMenus, connections) + local menu = plugin:CreatePluginMenu(newId(), entries.Text, entries.Icon) + + for _, entry in ipairs(entries) do + if entry == SEPARATOR then + menu:AddSeparator() + elseif typeof(entry) == "Instance" and entry:IsA("PluginAction") then + menu:AddAction(entry) + elseif typeof(entry) == "table" then + if #entry > 0 then + local subMenu = createPluginMenu(plugin, entry, subMenus, connections) + table.insert(subMenus, subMenu) + menu:AddMenu(subMenu) + else + local action = menu:AddNewAction(newId(), entry.Text, entry.Icon) + action.Enabled = (entry.Enabled == nil) and true or entry.Enabled + + if entries.CurrentKey then + action.Checked = entries.CurrentKey == entry.Key + else + action.Checked = entry.Checked + end + + connectAction(connections, action, entry, entry.Key) + end + elseif entry then -- Ignore false/nil for when plugins do {xyz, fflag and abc, ...} + error("Unsupported action "..tostring(entry)) + end + end + + return menu +end + +local function makePluginMenu(plugin, entries) + local subMenus = {} + local connections = {} + + local menu = createPluginMenu(plugin, entries, subMenus, connections) + + menu:ShowAsync() + for _, subMenu in ipairs(subMenus) do + subMenu:Destroy() + end + menu:Destroy() +end + +return { + makePluginMenu = makePluginMenu, + Separator = SEPARATOR, +} \ No newline at end of file diff --git a/BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Studio/StudioStyle.lua b/BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Studio/StudioStyle.lua new file mode 100644 index 0000000000..b7af169df8 --- /dev/null +++ b/BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Studio/StudioStyle.lua @@ -0,0 +1,54 @@ +--[[ + Matches the table structure of UILibrary's Style table. Provides a default mapping for colors + +]] +local StudioStyle = {} +StudioStyle.__index = StudioStyle + +-- c : StudioStyleGuideColor +-- m : StudioStyleGuideModifier +function StudioStyle.new(getColor, c, m) + local FStringMainFont = game:GetFastString("StudioBuiltinPluginDefaultFont") + + return { + font = Enum.Font[FStringMainFont], + + backgroundColor = getColor(c.MainBackground), + liveBackgroundColor = Color3.new(), + textColor = getColor(c.MainText), + subTextColor = getColor(c.SubText), + dimmerTextColor = getColor(c.DimmedText), + + itemColor = getColor(c.Item), + borderColor = getColor(c.Border), + + hoveredItemColor = getColor(c.Item, m.Hover), + hoveredTextColor = getColor(c.MainText, m.Hover), + + primaryItemColor = getColor(c.DialogMainButton), + primaryBorderColor = getColor(c.DialogMainButton), + primaryTextColor = getColor(c.DialogMainButtonText), + + primaryHoveredItemColor = getColor(c.DialogMainButton, m.Hover), + primaryHoveredBorderColor = getColor(c.DialogMainButton, m.Hover), + primaryHoveredTextColor = getColor(c.DialogMainButtonText, m.Hover), + + selectionColor = getColor(c.Item, m.Selected), + selectionBorderColor = getColor(c.Border, m.Selected), + selectedTextColor = getColor(c.MainText, m.Selected), + + shadowColor = getColor(c.Shadow), + shadowTransparency = getColor(c.Shadow, m.Hover), + + separationLineColor = getColor(c.Separator), + + disabledColor = getColor(c.MainText, m.Disabled), + errorColor = getColor(c.ErrorText), + + hoverColor = getColor(c.MainBackground, m.Hover), + + hyperlinkTextColor = getColor(c.LinkText), + } +end + +return StudioStyle \ No newline at end of file diff --git a/BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Studio/StudioStyle.spec.lua b/BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Studio/StudioStyle.spec.lua new file mode 100644 index 0000000000..c79266d200 --- /dev/null +++ b/BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Studio/StudioStyle.spec.lua @@ -0,0 +1,35 @@ +local StudioStyle = require(script.Parent.StudioStyle) + +local Library = script.Parent.Parent +local Style = require(Library.StyleDefaults) +local StudioTheme = require(Library.Studio.StudioTheme) + +return function() + it("should define all of the keys in the Style.Defaults object", function() + local numKeysFound = 0 + local numKeysExpected = 0 + + local defaultStyle = Style.Defaults + local studioTheme = StudioTheme.newDummyTheme(function() return {} end) + local mockStudioTheme = studioTheme.getTheme() + local studioStyle = StudioStyle.new(mockStudioTheme.GetColor, + Enum.StudioStyleGuideColor, + Enum.StudioStyleGuideModifier) + + -- every key in the default style should be accounted for + for colorKey, _ in pairs(defaultStyle) do + numKeysExpected = numKeysExpected + 1 + expect(studioStyle[colorKey]).to.be.ok() + end + + -- there should not be extra keys defined + for _, _ in pairs(studioStyle) do + numKeysFound = numKeysFound + 1 + end + + expect(numKeysFound).to.equal(numKeysExpected) + + -- clean up + studioTheme:destroy() + end) +end \ No newline at end of file diff --git a/BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Studio/StudioTheme.lua b/BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Studio/StudioTheme.lua new file mode 100644 index 0000000000..159fc6ce35 --- /dev/null +++ b/BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Studio/StudioTheme.lua @@ -0,0 +1,133 @@ +--[[ + Wraps theme colors and update logic for Roblox Studio. + Plugins are responsible for making a wrapper around this StudioTheme that + defines a createValues function. This function maps Studio theme enums + to named table entries which can be used in the plugin's theme. + + Example usage (a Theme.lua module in your plugin): + + local StudioTheme = require(Plugin.UILibrary.Studio.StudioTheme) + local Theme = {} + + function Theme.createValues(getColor, c, m) + return { + backgroundColor = getColor(c.MainBackground), + } + end + + function Theme.new() + return StudioTheme.new(Theme.createValues) + end + + return Theme +]] + +game:DefineFastFlag("FixMockStudioTheme", false) + +local Library = script.Parent.Parent +local join = require(Library.join) +local Signal = require(Library.Utils.Signal) + +local StudioTheme = {} +StudioTheme.__index = StudioTheme + +function StudioTheme.new(createValues, overrideSignal) + local self = { + getTheme = function() + return settings().Studio.Theme + end, + + createValues = function(...) + return createValues(...) + end, + + valuesChanged = Signal.new(), + values = {}, + themeChangedConnection = nil, + } + + setmetatable(self, StudioTheme) + + if overrideSignal then + self.themeChangedConnection = overrideSignal:Connect(function() + self:recalculateTheme() + end) + else + self.themeChangedConnection = settings().Studio.ThemeChanged:Connect(function() + self:recalculateTheme() + end) + end + + self:recalculateTheme() + + return self +end + +function StudioTheme:connect(...) + return self.valuesChanged:connect(...) +end + +function StudioTheme:destroy() + if self.themeChangedConnection then + self.themeChangedConnection:Disconnect() + end +end + +function StudioTheme:update(changedValues) + self.values = join(self.values, changedValues) + + if self.valuesChanged then + self.valuesChanged:fire(self.values) + end +end + +function StudioTheme:recalculateTheme() + local theme = self.getTheme() + + -- Shorthands for getting a color + local c = Enum.StudioStyleGuideColor + local m = Enum.StudioStyleGuideModifier + + local function getColor(...) + return theme:GetColor(...) + end + + local newValues = self.createValues(getColor, c, m) + + self:update(newValues) +end + +function StudioTheme.newDummyTheme(createValues) + local self = { + getTheme = function() + return { + GetColor = function() + return Color3.new() + end, + } + end, + + createValues = function(...) + return createValues(...) + end, + + valuesChanged = Signal.new(), + values = {}, + } + + setmetatable(self, StudioTheme) + + if game:GetFastFlag("FixMockStudioTheme") then + local newValues = self.createValues(function() + return self.getTheme():GetColor() + end, {}, {}) + + self:update(newValues) + else + self:recalculateTheme() + end + + return self +end + +return StudioTheme diff --git a/BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Studio/StudioTheme.spec.lua b/BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Studio/StudioTheme.spec.lua new file mode 100644 index 0000000000..100b16a65f --- /dev/null +++ b/BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Studio/StudioTheme.spec.lua @@ -0,0 +1,102 @@ +return function() + local Library = script.Parent.Parent + local StudioTheme = require(Library.Studio.StudioTheme) + + local function createValues() + return {} + end + + describe("StudioTheme.new", function() + it("should return a new StudioTheme", function() + local theme = StudioTheme.new(createValues) + expect(theme).to.be.ok() + expect(theme.values).to.be.ok() + + theme:destroy() + end) + + it("should have a getTheme function that gets the Studio theme", function() + local theme = StudioTheme.new(createValues) + expect(theme.getTheme).to.be.ok() + expect(theme.getTheme()).to.equal(settings().Studio.Theme) + + theme:destroy() + end) + + it("should listen for Studio theme changes", function() + local event = Instance.new("BindableEvent") + local theme = StudioTheme.new(createValues, event.Event) + expect(theme.themeChangedConnection).to.be.ok() + + local called = false + theme:connect(function() + called = true + end) + event:Fire() + expect(called).to.equal(true) + + event:Destroy() + theme:destroy() + end) + end) + + describe("StudioTheme.newDummyTheme", function() + it("should return a new fake StudioTheme", function() + local theme = StudioTheme.newDummyTheme(createValues) + expect(theme).to.be.ok() + expect(theme.values).to.be.ok() + + theme:destroy() + end) + + it("should have a getTheme function that returns a constant color", function() + local theme = StudioTheme.newDummyTheme(createValues) + expect(theme.getTheme).to.be.ok() + expect(theme.getTheme()).never.to.equal(settings().Studio.Theme) + expect(theme.getTheme().GetColor).to.be.ok() + expect(theme.getTheme().GetColor()).to.equal(Color3.new()) + + theme:destroy() + end) + + it("should not listen for theme changes", function() + local theme = StudioTheme.newDummyTheme(createValues) + expect(theme.themeChangedConnection).never.to.be.ok() + + theme:destroy() + end) + end) + + describe("StudioTheme.recalculateTheme", function() + it("should call the createValues function", function() + local called = false + + local theme = StudioTheme.new(function() + called = true + return {} + end) + + expect(called).to.equal(true) + called = false + theme:recalculateTheme() + expect(called).to.equal(true) + + theme:destroy() + end) + + it("should update the theme values", function() + local called = false + + local theme = StudioTheme.new(function() + return called and {newColor = Color3.new()} or {} + end) + + expect(theme.values.newColor).never.to.be.ok() + called = true + theme:recalculateTheme() + expect(theme.values.newColor).to.be.ok() + + theme:destroy() + end) + end) +end \ No newline at end of file diff --git a/BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/StyleDefaults.lua b/BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/StyleDefaults.lua new file mode 100644 index 0000000000..537f6b3c69 --- /dev/null +++ b/BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/StyleDefaults.lua @@ -0,0 +1,71 @@ +--[[ + Provides style utility for the UILibrary. + Default values for style can be defined here. +]] + +local Style = {} + +--Defines default entries for the Style table in createTheme. +--Default entries are only to provide compatibility, not actual theme info. +Style.Defaults = { + font = Enum.Font.SourceSans, + + backgroundColor = Color3.new(), + liveBackgroundColor = Color3.new(), + textColor = Color3.new(), + subTextColor = Color3.new(), + dimmerTextColor = Color3.new(), + + itemColor = Color3.new(), + borderColor = Color3.new(), + + hoveredItemColor = Color3.new(), + hoveredTextColor = Color3.new(), + + primaryItemColor = Color3.new(), + primaryBorderColor = Color3.new(), + primaryTextColor = Color3.new(), + + primaryHoveredItemColor = Color3.new(), + primaryHoveredBorderColor = Color3.new(), + primaryHoveredTextColor = Color3.new(), + + selectionColor = Color3.new(), + selectionBorderColor = Color3.new(), + selectedTextColor = Color3.new(), + + shadowColor = Color3.new(), + shadowTransparency = Color3.new(), + + separationLineColor = Color3.new(), + + disabledColor = Color3.new(), + errorColor = Color3.new(), + + hoverColor = Color3.new(), + + hyperlinkTextColor = Color3.new(), +} + +-- A function that checks to see if there are any missing or +-- extraneous keys in the given style. If a value is available +-- for all necessary entries, then the UILibrary will be able to run. +Style.isValid = function(style) + local requiredStyle = Style.Defaults + + for key, _ in pairs(requiredStyle) do + if not style[key] then + return false + end + end + + for key, _ in pairs(style) do + if not requiredStyle[key] then + return false + end + end + + return true +end + +return Style \ No newline at end of file diff --git a/BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Theming.lua b/BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Theming.lua new file mode 100644 index 0000000000..4bda76d40d --- /dev/null +++ b/BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Theming.lua @@ -0,0 +1,63 @@ +local Library = script.Parent + +local Roact = require(Library.Parent.Parent.Roact) +local Signal = require(Library.Utils.Signal) +local Symbol = require(Library.Utils.Symbol) +local themeKey = Symbol.named("UILibraryTheme") + +local ThemeProvider = Roact.PureComponent:extend("UILibraryThemeProvider") +function ThemeProvider:init() + local theme = self.props.theme + assert(theme ~= nil, "No theme was given to this ThemeProvider.") + self.themeChanged = Signal.new() + + self._context[themeKey] = { + values = theme, + themeChanged = self.themeChanged, + } +end +function ThemeProvider:render() + self._context[themeKey].values = self.props.theme + self.themeChanged:fire() + return Roact.oneChild(self.props[Roact.Children]) +end + +-- the consumer should complain if it doesn't have a theme +local ThemeConsumer = Roact.PureComponent:extend("UILibraryThemeConsumer") +function ThemeConsumer:init() + assert(self._context[themeKey] ~= nil, "No ThemeProvider found.") + local theme = self._context[themeKey] + + self.state = { + themeValues = theme.values, + } + + self.themeConnection = theme.themeChanged:connect(function() + self:setState({ + themeValues = theme.values, + }) + end) +end +function ThemeConsumer:render() + local themeValues = self.state.themeValues + return self.props.themedRender(themeValues) +end +function ThemeConsumer:willUnmount() + if self.themeConnection then + self.themeConnection:disconnect() + end +end + +-- withTheme should provide a simple way to style elements +-- callback : function(theme) +local function withTheme(callback) + return Roact.createElement(ThemeConsumer, { + themedRender = callback + }) +end + +return { + Provider = ThemeProvider, + Consumer = ThemeConsumer, + withTheme = withTheme, +} \ No newline at end of file diff --git a/BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/UILibraryWrapper.lua b/BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/UILibraryWrapper.lua new file mode 100644 index 0000000000..51b7868561 --- /dev/null +++ b/BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/UILibraryWrapper.lua @@ -0,0 +1,69 @@ +--[[ + A component that wraps all external elements needed for the UILibrary. + Entries in the wrapper are optional, but if you do not provide an + element that is needed by the components you are using, you will get + an error upon trying to mount those components. + + Props: + Theme theme = A theme object to be used by a ThemeProvider. + PluginGui focusGui = The top-level gui to be used by a FocusProvider. + Plugin plugin = A Plugin object which can be used to construct guis. +]] + +local Library = script.Parent +local Roact = require(Library.Parent.Parent.Roact) + +local Theming = require(Library.Theming) +local ThemeProvider = Theming.Provider + +local Focus = require(Library.Focus) +local FocusProvider = Focus.Provider + +local Plugin = require(Library.Plugin) +local PluginProvider = Plugin.Provider + +local Camera = require(Library.Camera) +local CameraProvider = Camera.Provider + +local UILibraryWrapper = Roact.PureComponent:extend("UILibraryWrapper") + +function UILibraryWrapper:addProvider(root, provider, props) + return Roact.createElement(provider, props, {root}) +end + +function UILibraryWrapper:render() + local props = self.props + local children = props[Roact.Children] + local root = Roact.oneChild(children) + + -- ThemeProvider + local theme = props.theme + if theme then + root = self:addProvider(root, ThemeProvider, { + theme = theme, + }) + end + + -- FocusProvider + local focusGui = props.focusGui + if focusGui then + root = self:addProvider(root, FocusProvider, { + pluginGui = focusGui, + }) + end + + -- PluginProvider + local plugin = props.plugin + if plugin then + root = self:addProvider(root, PluginProvider, { + plugin = plugin, + }) + end + + -- CameraProvider + root = self:addProvider(root, CameraProvider, nil) + + return root +end + +return UILibraryWrapper \ No newline at end of file diff --git a/BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/UILibraryWrapper.spec.lua b/BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/UILibraryWrapper.spec.lua new file mode 100644 index 0000000000..089f6bf3ee --- /dev/null +++ b/BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/UILibraryWrapper.spec.lua @@ -0,0 +1,86 @@ +return function() + local Library = script.Parent + local Roact = require(Library.Parent.Parent.Roact) + + local workspace = game:GetService("Workspace") + + local UILibraryWrapper = require(script.Parent.UILibraryWrapper) + + local function createTestWrapper(props, children) + return Roact.createElement(UILibraryWrapper, props, children) + end + + it("should create and destroy without errors", function() + local element = createTestWrapper() + local instance = Roact.mount(element) + Roact.unmount(instance) + end) + + it("should render its children if nothing is provided", function () + local container = Instance.new("Folder") + local instance = Roact.mount(createTestWrapper({}, { + Frame = Roact.createElement("Frame") + }), container) + + local frame = container:FindFirstChildOfClass("Frame") + + expect(frame).to.be.ok() + + Roact.unmount(instance) + end) + + it("should render its children if items are provided", function () + local container = Instance.new("Folder") + local instance = Roact.mount(createTestWrapper({ + theme = {}, + focusGui = {}, + plugin = {}, + }, { + Frame = Roact.createElement("Frame") + }), container) + + local frame = container:FindFirstChildOfClass("Frame") + + expect(frame).to.be.ok() + + Roact.unmount(instance) + end) + + describe("addProvider", function() + it("should place the new provider above the root", function () + local container = Instance.new("Folder") + local root = Roact.createElement("Frame") + + local result = UILibraryWrapper:addProvider(root, "Frame") + local instance = Roact.mount(result, container) + local frame = container:FindFirstChildOfClass("Frame") + + expect(frame).to.be.ok() + expect(frame[1]).to.be.ok() + + Roact.unmount(instance) + end) + + it("should not modify the tree below the root", function () + local container = Instance.new("Folder") + local root = Roact.createElement("Frame", {}, { + ChildFrame = Roact.createElement("Frame", {}, { + DescendantFrame = Roact.createElement("Frame"), + }), + OtherChild = Roact.createElement("Frame"), + }) + + local result = UILibraryWrapper:addProvider(root, "Frame") + local instance = Roact.mount(result, container) + local frame = container:FindFirstChildOfClass("Frame") + + expect(frame).to.be.ok() + expect(frame[1]).to.be.ok() + expect(frame[1].ChildFrame).to.be.ok() + expect(frame[1].ChildFrame.DescendantFrame).to.be.ok() + expect(frame[1].OtherChild).to.be.ok() + + Roact.unmount(instance) + end) + end) +end \ No newline at end of file diff --git a/BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Utils/AssetType.lua b/BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Utils/AssetType.lua new file mode 100644 index 0000000000..ad59cfcd6a --- /dev/null +++ b/BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Utils/AssetType.lua @@ -0,0 +1,126 @@ +local FFlagFixGetAssetTypeErrorHandling = game:DefineFastFlag("FixGetAssetTypeErrorHandling", false) +local FFlagStudioUILibFixAssetTypeMap = game:DefineFastFlag("StudioUILibFixAssetTypeMap", false) +local FFlagStudioFixMeshPartPreview = game:DefineFastFlag("StudioFixMeshPartPreview", false) +local FFlagEnableToolboxVideos = game:GetFastFlag("EnableToolboxVideos") + +local AssetType = {} + +AssetType.TYPES = { + ModelType = 1, -- MeshPart, Mesh, Model + ImageType = 2, + SoundType = 3, -- Sound comes with the model or mesh. + ScriptType = 4, -- Server, local, module + PluginType = 5, + OtherType = 6, + LoadingType = 7, + VideoType = 8, +} + +-- For check if we show preview button or not. +AssetType.AssetTypesPreviewEnabled = { + [Enum.AssetType.Mesh.Value] = true, + [Enum.AssetType.MeshPart.Value] = true, + [Enum.AssetType.Model.Value] = true, + [Enum.AssetType.Decal.Value] = true, + [Enum.AssetType.Image.Value] = true, + [Enum.AssetType.Audio.Value] = true, + [Enum.AssetType.Lua.Value] = true, + [Enum.AssetType.Plugin.Value] = true, + [Enum.AssetType.Video.Value] = FFlagEnableToolboxVideos or nil, +} + +local classTypeMap = { + BasePart = AssetType.TYPES.ModelType, + Model = AssetType.TYPES.ModelType, + BackpackItem = AssetType.TYPES.ModelType, + Accoutrement = AssetType.TYPES.ModelType, + + Decal = AssetType.TYPES.ImageType, + ImageLabel = AssetType.TYPES.ImageType, + ImageButton = AssetType.TYPES.ImageType, + Texture =AssetType.TYPES.ImageType, + Sky = AssetType.TYPES.ImageType, + + Sound = AssetType.TYPES.SoundType, + VideoFrame = AssetType.TYPES.VideoType, + + BaseScript = AssetType.TYPES.ScriptType, +} + +if FFlagStudioUILibFixAssetTypeMap then + classTypeMap.Part = AssetType.TYPES.ModelType +end + +if FFlagStudioFixMeshPartPreview then + classTypeMap.MeshPart = AssetType.TYPES.ModelType +end + +-- For AssetPreview, we divide assets into four categories. +-- For any parts or meshes, we will need to do a model preview. +-- For images, we show only an image. +-- For sound, we will need to show something and provide play control. (Will +-- probably improve this in the future) +-- For BaseScript, show only names while for all other types show assetName and type +function AssetType:getAssetType(assetInstance) + local notInstance + if FFlagFixGetAssetTypeErrorHandling then + notInstance = not assetInstance or typeof(assetInstance) ~= "Instance" + else + notInstance = not assetInstance + end + + if notInstance then + return self.TYPES.LoadingType + end + local className = assetInstance.className + local type = classTypeMap[className] + + if not type then + return self.TYPES.OtherType + end + + return type +end + +function AssetType:isModel(currentType) + return currentType == self.TYPES.ModelType +end + +function AssetType:isImage(currentType) + return currentType == self.TYPES.ImageType +end + +function AssetType:isAudio(currentType) + return currentType == self.TYPES.SoundType +end + +function AssetType:isScript(currentType) + return currentType == self.TYPES.ScriptType +end + +function AssetType:isPlugin(currentType) + return currentType == self.TYPES.PluginType +end + +function AssetType:markAsPlugin() + return self.TYPES.PluginType +end + +function AssetType:isOtherType(currentType) + return currentType == self.TYPES.OtherType +end + +function AssetType:isLoading(currentType) + return currentType == self.TYPES.LoadingType +end + +function AssetType:isVideo(currentType) + return currentType == self.TYPES.VideoType +end + +function AssetType:isPreviewAvailable(typeId) + assert(typeId ~= nil, "AssetPreviewType can't be nil") + return AssetType.AssetTypesPreviewEnabled[typeId] ~= nil +end + +return AssetType \ No newline at end of file diff --git a/BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Utils/AssetType.spec.lua b/BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Utils/AssetType.spec.lua new file mode 100644 index 0000000000..f4bbeee4ce --- /dev/null +++ b/BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Utils/AssetType.spec.lua @@ -0,0 +1,24 @@ +return function() + local AssetType = require(script.Parent.AssetType) + + describe("isPreviewAvailable()", function() + it("should make sure the assetPreviewType is nil.", function() + expect(function() + AssetType.isPreviewAvailable(nil) + end).to.throw() + end) + + it("should show preview for sound.", function() + local typeId = Enum.AssetType.Audio.Value + local result = AssetType:isPreviewAvailable(typeId) + expect(result).to.equal(true) + end) + + it("should not show preview for LeftArm.", function() + local typeId = Enum.AssetType.LeftArm.Value + local result = AssetType:isPreviewAvailable(typeId) + expect(result).to.equal(false) + end) + end) + +end diff --git a/BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Utils/GetClassIcon.lua b/BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Utils/GetClassIcon.lua new file mode 100644 index 0000000000..4d3cf1a3e7 --- /dev/null +++ b/BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Utils/GetClassIcon.lua @@ -0,0 +1,31 @@ +local FFlagStudioFixGetClassIcon = settings():GetFFlag("StudioFixGetClassIcon") +local FFlagStudioMinorFixesForAssetPreview = settings():GetFFlag("StudioMinorFixesForAssetPreview") + +local StudioService = game:GetService("StudioService") + +local function GetClassIcon(instance) + if FFlagStudioFixGetClassIcon then + local className = instance.ClassName + if instance.IsA then + if instance:IsA("JointInstance") and className == "ManualWeld" or className == "ManualGlue" then + return StudioService:GetClassIcon("JointInstance") + end + end + return StudioService:GetClassIcon(className) + else + if FFlagStudioMinorFixesForAssetPreview then + if typeof(instance) ~= "Instance" then + return StudioService:GetClassIcon("Model") + end + end + + local className = instance.ClassName + if instance:IsA("JointInstance") and className == "ManualWeld" or className == "ManualGlue" then + return StudioService:GetClassIcon("JointInstance") + else + return StudioService:GetClassIcon(className) + end + end +end + +return GetClassIcon diff --git a/BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Utils/GetClassIcon.spec.lua b/BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Utils/GetClassIcon.spec.lua new file mode 100644 index 0000000000..6d1800ed04 --- /dev/null +++ b/BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Utils/GetClassIcon.spec.lua @@ -0,0 +1,43 @@ +-- Icon location is determined by an offset within a sequential list of icons in a single image. +local ICON_NOTFOUND = Vector2.new(0,0) +local ICON_JOINTINSTANCE = Vector2.new(544,0) +local ICON_SCRIPT = Vector2.new(96,0) + +return function() + local GetClassIcon = require(script.Parent.GetClassIcon) + + describe("getClassIcon", function() + it("should correctly return 'JointInstance' classIcon for ManualWelds", function() + local manualWeld = Instance.new("ManualWeld") + local classIconTable = GetClassIcon(manualWeld) + + expect(classIconTable.ImageRectOffset).to.equal(ICON_JOINTINSTANCE) + expect(classIconTable.ImageRectOffset).never.to.equal(ICON_NOTFOUND) + end) + + it("should correctly return 'JointInstance' classIcon for ManualGlues", function() + local manualGlue = Instance.new("ManualGlue") + local classIconTable = GetClassIcon(manualGlue) + + expect(classIconTable.ImageRectOffset).to.equal(ICON_JOINTINSTANCE) + expect(classIconTable.ImageRectOffset).never.to.equal(ICON_NOTFOUND) + end) + + it("should return the Script Class Icon for scripts", function() + local script = Instance.new("Script") + local classIconTable = GetClassIcon(script) + + expect(classIconTable.ImageRectOffset).to.equal(ICON_SCRIPT) + expect(classIconTable.ImageRectOffset).never.to.equal(ICON_NOTFOUND) + end) + + it("should support non-instance objects that have a ClassName member", function() + local notAnInstance = { + ClassName = "Folder" + } + + local classIconTable = GetClassIcon(notAnInstance) + expect(classIconTable).to.be.ok() + end) + end) +end \ No newline at end of file diff --git a/BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Utils/GetTextSize.lua b/BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Utils/GetTextSize.lua new file mode 100644 index 0000000000..37984ffb20 --- /dev/null +++ b/BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Utils/GetTextSize.lua @@ -0,0 +1,20 @@ +local TextService = game:GetService("TextService") + +local FStringMainFont = game:DefineFastString("StudioBuiltinPluginDefaultFont", "Gotham") + +local FONT_SIZE_MEDIUM = 16 +local FONT = Enum.Font.Gotham +pcall(function() + FONT = Enum.Font[FStringMainFont] +end) + +local function GetTextSize(text, fontSize, font, frameSize) + + fontSize = fontSize or FONT_SIZE_MEDIUM + font = font or FONT + frameSize = frameSize or Vector2.new(math.huge, math.huge) + + return TextService:GetTextSize(text, fontSize, font, frameSize) +end + +return GetTextSize \ No newline at end of file diff --git a/BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Utils/Immutable.lua b/BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Utils/Immutable.lua new file mode 100644 index 0000000000..a73d2035b1 --- /dev/null +++ b/BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Utils/Immutable.lua @@ -0,0 +1,141 @@ +--[[ + Provides functions for manipulating immutable data structures. +]] + +local Immutable = {} + +--[[ + Merges dictionary-like tables together. +]] +function Immutable.JoinDictionaries(...) + local result = {} + + for i = 1, select("#", ...) do + local dictionary = select(i, ...) + for key, value in pairs(dictionary) do + result[key] = value + end + end + + return result +end + +--[[ + Joins any number of lists together into a new list +]] +function Immutable.JoinLists(...) + local new = {} + + for listKey = 1, select("#", ...) do + local list = select(listKey, ...) + local len = #new + + for itemKey = 1, #list do + new[len + itemKey] = list[itemKey] + end + end + + return new +end + +--[[ + Creates a new copy of the dictionary and sets a value inside it. +]] +function Immutable.Set(dictionary, key, value) + local new = {} + + for key, value in pairs(dictionary) do + new[key] = value + end + + new[key] = value + + return new +end + +--[[ + Creates a new copy of the list with the given elements appended to it. +]] +function Immutable.Append(list, ...) + local new = {} + local len = #list + + for key = 1, len do + new[key] = list[key] + end + + for i = 1, select("#", ...) do + new[len + i] = select(i, ...) + end + + return new +end + +--[[ + Remove elements from a dictionary +]] +function Immutable.RemoveFromDictionary(dictionary, ...) + local result = {} + + for key, value in pairs(dictionary) do + local found = false + for listKey = 1, select("#", ...) do + if key == select(listKey, ...) then + found = true + break + end + end + if not found then + result[key] = value + end + end + + return result +end + +--[[ + Remove the given key from the list. +]] +function Immutable.RemoveFromList(list, removeIndex) + local new = {} + + for i = 1, #list do + if i ~= removeIndex then + table.insert(new, list[i]) + end + end + + return new +end + +--[[ + Remove the range from the list starting from the index. +]] +function Immutable.RemoveRangeFromList(list, index, count) + local new = {} + + for i = 1, #list do + if i < index or i >= index + count then + table.insert(new, list[i]) + end + end + + return new +end + +--[[ + Creates a new list that has no occurrences of the given value. +]] +function Immutable.RemoveValueFromList(list, removeValue) + local new = {} + + for i = 1, #list do + if list[i] ~= removeValue then + table.insert(new, list[i]) + end + end + + return new +end + +return Immutable diff --git a/BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Utils/Immutable.spec.lua b/BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Utils/Immutable.spec.lua new file mode 100644 index 0000000000..12ad39a52a --- /dev/null +++ b/BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Utils/Immutable.spec.lua @@ -0,0 +1,284 @@ +return function() + local Immutable = require(script.Parent.Immutable) + + describe("JoinDictionaries", function() + it("should preserve immutability", function() + local a = {} + local b = {} + + local c = Immutable.JoinDictionaries(a, b) + + expect(c).never.to.equal(a) + expect(c).never.to.equal(b) + end) + + it("should treat list-like values like dictionary values", function() + local a = { + [1] = 1, + [2] = 2, + [3] = 3 + } + + local b = { + [1] = 11, + [2] = 22 + } + + local c = Immutable.JoinDictionaries(a, b) + + expect(c[1]).to.equal(b[1]) + expect(c[2]).to.equal(b[2]) + expect(c[3]).to.equal(a[3]) + end) + + it("should merge dictionary values correctly", function() + local a = { + hello = "world", + foo = "bar" + } + + local b = { + foo = "baz", + tux = "penguin" + } + + local c = Immutable.JoinDictionaries(a, b) + + expect(c.hello).to.equal(a.hello) + expect(c.foo).to.equal(b.foo) + expect(c.tux).to.equal(b.tux) + end) + + it("should merge multiple dictionaries", function() + local a = { + foo = "yes" + } + + local b = { + bar = "yup" + } + + local c = { + baz = "sure" + } + + local d = Immutable.JoinDictionaries(a, b, c) + + expect(d.foo).to.equal(a.foo) + expect(d.bar).to.equal(b.bar) + expect(d.baz).to.equal(c.baz) + end) + end) + + describe("JoinLists", function() + it("should preserve immutability", function() + local a = {} + local b = {} + + local c = Immutable.JoinLists(a, b) + + expect(c).never.to.equal(a) + expect(c).never.to.equal(b) + end) + + it("should treat list-like values correctly", function() + local a = {1, 2, 3} + local b = {4, 5, 6} + + local c = Immutable.JoinLists(a, b) + + expect(#c).to.equal(6) + + for i = 1, #c do + expect(c[i]).to.equal(i) + end + end) + + it("should merge multiple lists", function() + local a = {1, 2} + local b = {3, 4} + local c = {5, 6} + + local d = Immutable.JoinLists(a, b, c) + + expect(#d).to.equal(6) + + for i = 1, #d do + expect(d[i]).to.equal(i) + end + end) + end) + + describe("Set", function() + it("should preserve immutability", function() + local a = {} + + local b = Immutable.Set(a, "foo", "bar") + + expect(b).never.to.equal(a) + end) + + it("should treat numeric keys normally", function() + local a = {1, 2, 3} + + local b = Immutable.Set(a, 2, 4) + + expect(b[1]).to.equal(1) + expect(b[2]).to.equal(4) + expect(b[3]).to.equal(3) + end) + + it("should overwrite dictionary-like keys", function() + local a = { + foo = "bar", + baz = "qux" + } + + local b = Immutable.Set(a, "foo", "hello there") + + expect(b.foo).to.equal("hello there") + expect(b.baz).to.equal(a.baz) + end) + end) + + describe("Append", function() + it("should preserve immutability", function() + local a = {} + + local b = Immutable.Append(a, "another happy landing") + + expect(b).never.to.equal(a) + end) + + it("should append values", function() + local a = {1, 2, 3} + local b = Immutable.Append(a, 4, 5) + + expect(#b).to.equal(5) + + for i = 1, #b do + expect(b[i]).to.equal(i) + end + end) + end) + + describe("RemoveFromDictionary", function() + it("should preserve immutability", function() + local a = { foo = "bar" } + + local b = Immutable.RemoveFromDictionary(a, "foo") + + expect(b).to.never.equal(a) + end) + + it("should remove fields from the dictionary", function() + local a = { + foo = "bar", + baz = "qux", + boof = "garply", + } + + local b = Immutable.RemoveFromDictionary(a, "foo", "boof") + + expect(b.foo).to.never.be.ok() + expect(b.baz).to.equal("qux") + expect(b.boof).to.never.be.ok() + end) + end) + + describe("RemoveFromList", function() + it("should preserve immutability", function() + local a = {1, 2, 3} + local b = Immutable.RemoveFromList(a, 2) + + expect(b).never.to.equal(a) + end) + + it("should remove elements from the list", function() + local a = {1, 2, 3} + local b = Immutable.RemoveFromList(a, 2) + + expect(b[1]).to.equal(1) + expect(b[2]).to.equal(3) + expect(b[3]).never.to.be.ok() + end) + end) + + describe("RemoveRangeFromList", function() + it("should preserve immutability", function() + local a = {1, 2, 3} + local b = Immutable.RemoveRangeFromList(a, 2, 1) + + expect(b).never.to.equal(a) + end) + + it("should remove single elements from the middle of the list", function() + local a = {1, 2, 3} + local b = Immutable.RemoveRangeFromList(a, 2, 1) + + expect(b[1]).to.equal(1) + expect(b[2]).to.equal(3) + expect(b[3]).never.to.be.ok() + end) + + it("should remove multiple elements from the front of the list", function() + local a = {1, 2, 3, 4, 5, 6} + local b = Immutable.RemoveRangeFromList(a, 1, 4) + + expect(b[1]).to.equal(5) + expect(b[2]).to.equal(6) + expect(b[3]).never.to.be.ok() + end) + + it("should remove multiple elements properly from middle of the list", function() + local a = {1, 2, 3, 4, 5, 6} + local b = Immutable.RemoveRangeFromList(a, 2, 4) + + expect(b[1]).to.equal(1) + expect(b[2]).to.equal(6) + expect(b[3]).never.to.be.ok() + end) + + it("should remove multiple elements properly from the end of the list", function() + local a = {1, 2, 3, 4, 5, 6, 7} + local b = Immutable.RemoveRangeFromList(a, 4, 4) + + expect(b[1]).to.equal(1) + expect(b[2]).to.equal(2) + expect(b[3]).to.equal(3) + expect(b[4]).never.to.be.ok() + end) + + it("should not remove any elements when count is 0 or less", function() + local a = {1, 2, 3} + local b = Immutable.RemoveRangeFromList(a, 2, 0) + + expect(b[1]).to.equal(1) + expect(b[2]).to.equal(2) + expect(b[3]).to.equal(3) + + local c = Immutable.RemoveRangeFromList(a, 2, -1) + expect(c[1]).to.equal(1) + expect(c[2]).to.equal(2) + expect(c[3]).to.equal(3) + end) + end) + + describe("RemoveValueFromList", function() + it("should preserve immutability", function() + local a = {1, 1, 1} + local b = Immutable.RemoveValueFromList(a, 1) + + expect(b).never.to.equal(a) + end) + + it("should remove all elements from the list", function() + local a = {1, 2, 2, 3} + local b = Immutable.RemoveValueFromList(a, 2) + + expect(b[1]).to.equal(1) + expect(b[2]).to.equal(3) + expect(b[3]).never.to.be.ok() + end) + end) +end \ No newline at end of file diff --git a/BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Utils/InsertToolEvent.lua b/BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Utils/InsertToolEvent.lua new file mode 100644 index 0000000000..3e399ab0d0 --- /dev/null +++ b/BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Utils/InsertToolEvent.lua @@ -0,0 +1,51 @@ +local InsertToolEvent = {} +InsertToolEvent.__index = InsertToolEvent + +InsertToolEvent.INSERT_TO_WORKSPACE = 0 +InsertToolEvent.INSERT_TO_STARTER_PACK = 1 +InsertToolEvent.INSERT_CANCELLED = 2 + +function InsertToolEvent.new(onPromptCallback) + local self = { + _onPromptCallback = onPromptCallback, + _bindable = Instance.new("BindableEvent"), + _waiting = false, + } + setmetatable(self, InsertToolEvent) + return self +end + +function InsertToolEvent:isWaiting() + return self._waiting +end + +function InsertToolEvent:destroy() + self:cancel() + self._bindable:Destroy() +end + +function InsertToolEvent:insertToWorkspace() + self._bindable:Fire(InsertToolEvent.INSERT_TO_WORKSPACE) +end + +function InsertToolEvent:insertToStarterPack() + self._bindable:Fire(InsertToolEvent.INSERT_TO_STARTER_PACK) +end + +function InsertToolEvent:cancel() + self._bindable:Fire(InsertToolEvent.INSERT_CANCELLED) +end + +function InsertToolEvent:promptAndWait() + if self._waiting then + return InsertToolEvent.INSERT_CANCELLED + end + + self._waiting = true + self._onPromptCallback() + local result = self._bindable.Event:Wait() + self._waiting = false + return result +end + +return InsertToolEvent diff --git a/BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Utils/LayoutOrderIterator.lua b/BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Utils/LayoutOrderIterator.lua new file mode 100644 index 0000000000..3f5186b038 --- /dev/null +++ b/BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Utils/LayoutOrderIterator.lua @@ -0,0 +1,37 @@ +--[[ + LayoutOrderIterator + Dynamically generates the "LayoutOrder = ..." so LayoutOrder for all elements + does not need to be adjusted when adding or removing elements. + + e.g. + local orderIterator = LayoutOrderIterator.new() + ... + Part1 = Roact.createElement(..., { + ... + LayoutOrder = orderIterator:getNextOrder(), + ... + }), + Part2 = Roact.createElement(..., { + ... + LayoutOrder = orderIterator:getNextOrder(), + ... + }), +]] + +local LayoutOrderIterator = {} +LayoutOrderIterator.__index = LayoutOrderIterator + +function LayoutOrderIterator.new() + local self = setmetatable({}, LayoutOrderIterator) + + self.order = 0 + + return self +end + +function LayoutOrderIterator:getNextOrder() + self.order = self.order + 1 + return self.order +end + +return LayoutOrderIterator \ No newline at end of file diff --git a/BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Utils/LayoutOrderIterator.spec.lua b/BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Utils/LayoutOrderIterator.spec.lua new file mode 100644 index 0000000000..1619ac28a9 --- /dev/null +++ b/BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Utils/LayoutOrderIterator.spec.lua @@ -0,0 +1,30 @@ +return function() + local LayoutOrderIterator = require(script.Parent.LayoutOrderIterator) + + describe("new", function() + it("should construct from nothing", function() + local orderIterator = LayoutOrderIterator.new() + + expect(orderIterator).to.be.ok() + end) + end) + + describe("getNextOrder", function() + it("should correctly generate the next order", function() + local orderIterator = LayoutOrderIterator.new() + + expect(orderIterator:getNextOrder()).to.be.equal(1) + expect(orderIterator:getNextOrder()).to.be.equal(2) + expect(orderIterator:getNextOrder()).to.be.equal(3) + + end) + + it("should correctly generate the next order when more than one iterators are created", function() + local orderIterator1 = LayoutOrderIterator.new() + local orderIterator2 = LayoutOrderIterator.new() + + expect(orderIterator1:getNextOrder()).to.be.equal(1) + expect(orderIterator2:getNextOrder()).to.be.equal(1) + end) + end) +end \ No newline at end of file diff --git a/BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Utils/MathUtils.lua b/BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Utils/MathUtils.lua new file mode 100644 index 0000000000..dbafba5b63 --- /dev/null +++ b/BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Utils/MathUtils.lua @@ -0,0 +1,15 @@ +local MathUtils = {} + +MathUtils.NEAR_ZERO = 0.0001 + +function MathUtils:fuzzyEq(numOne, numTwo, epsilon) + epsilon = epsilon or MathUtils.NEAR_ZERO + return math.abs(numOne - numTwo) < epsilon +end + +function MathUtils:round(num, numDecimalPlaces) + local mult = 10^(numDecimalPlaces or 0) + return math.floor(num * mult + 0.5) / mult +end + +return MathUtils \ No newline at end of file diff --git a/BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Utils/MathUtils.spec.lua b/BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Utils/MathUtils.spec.lua new file mode 100644 index 0000000000..4a2e6195f4 --- /dev/null +++ b/BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Utils/MathUtils.spec.lua @@ -0,0 +1,40 @@ +return function() + local Utils = script.Parent + local mathUtils = require(Utils.MathUtils) + + describe("Round", function() + it("should round to specified place", function() + local num = 101.39901 + expect(mathUtils:round(num, 0)).to.be.equal(101) + expect(mathUtils:round(num, 1)).to.be.equal(101.4) + expect(mathUtils:round(num, 2)).to.be.equal(101.40) + expect(mathUtils:round(num, 3)).to.be.equal(101.399) + expect(mathUtils:round(num, 4)).to.be.equal(101.3990) + end) + + it("round should work for negative numbers", function() + local num = -0.99095 + expect(mathUtils:round(num, 0)).to.be.equal(-1) + expect(mathUtils:round(num, 1)).to.be.equal(-1) + expect(mathUtils:round(num, 2)).to.be.equal(-0.99) + expect(mathUtils:round(num, 3)).to.be.equal(-0.991) + expect(mathUtils:round(num, 4)).to.be.equal(-0.9909) + end) + end) + + describe("Fuzzy Equals", function() + it("fuzzyEq should work with no epsilon provided", function() + expect(mathUtils:fuzzyEq(2.00009, 2)).to.be.equal(true) + expect(mathUtils:fuzzyEq(2, 2)).to.be.equal(true) + expect(mathUtils:fuzzyEq(2, 3)).to.be.equal(false) + expect(mathUtils:fuzzyEq(mathUtils.NEAR_ZERO, 0)).to.be.equal(false) + end) + + it("fuzzyEq should work with supplied epsilon value", function() + expect(mathUtils:fuzzyEq(2.0000009, 2, 0.000001)).to.be.equal(true) + expect(mathUtils:fuzzyEq(2, 2, 0.1)).to.be.equal(true) + expect(mathUtils:fuzzyEq(2, 3, 0.01)).to.be.equal(false) + expect(mathUtils:fuzzyEq(0.00000001, 0, 0.00000001)).to.be.equal(false) + end) + end) +end \ No newline at end of file diff --git a/BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Utils/Signal.lua b/BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Utils/Signal.lua new file mode 100644 index 0000000000..cde7956269 --- /dev/null +++ b/BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Utils/Signal.lua @@ -0,0 +1,63 @@ +--[[ + A limited, simple implementation of a Signal. + + Handlers are fired in order, and (dis)connections are properly handled when + executing an event. + + Signal uses Immutable to avoid invalidating the 'Fire' loop iteration. +]] + +local Immutable = require(script.Parent.Immutable) + +local Signal = {} + +Signal.__index = Signal + +function Signal.new() + local self = { + _listeners = {} + } + + setmetatable(self, Signal) + + return self +end + +function Signal:connect(callback) + local listener = { + callback = callback, + isConnected = true, + } + self._listeners = Immutable.Append(self._listeners, listener) + + local function disconnect() + listener.isConnected = false + self._listeners = Immutable.RemoveValueFromList(self._listeners, listener) + end + + return { + Disconnect = function() + disconnect() + end, + disconnect = disconnect, + } +end + +function Signal:fire(...) + for _, listener in ipairs(self._listeners) do + if listener.isConnected then + listener.callback(...) + end + end +end + +function Signal:Connect(...) + return self:connect(...) +end + +function Signal:Fire(...) + self:fire(...) +end + + +return Signal diff --git a/BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Utils/Signal.spec.lua b/BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Utils/Signal.spec.lua new file mode 100644 index 0000000000..f00f9477b0 --- /dev/null +++ b/BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Utils/Signal.spec.lua @@ -0,0 +1,114 @@ +return function() + local Signal = require(script.Parent.Signal) + + it("should construct from nothing", function() + local signal = Signal.new() + + expect(signal).to.be.ok() + end) + + it("should fire connected callbacks", function() + local callCount = 0 + local value1 = "Hello World" + local value2 = 7 + + local callback = function(arg1, arg2) + expect(arg1).to.equal(value1) + expect(arg2).to.equal(value2) + callCount = callCount + 1 + end + + local signal = Signal.new() + + local connection = signal:connect(callback) + signal:fire(value1, value2) + + expect(callCount).to.equal(1) + + connection:disconnect() + signal:fire(value1, value2) + + expect(callCount).to.equal(1) + end) + + it("should disconnect handlers", function() + local callback = function() + error("Callback was called after disconnect!") + end + + local signal = Signal.new() + + local connection = signal:connect(callback) + connection:disconnect() + + signal:fire() + end) + + it("should fire handlers in order", function() + local signal = Signal.new() + local x = 0 + local y = 0 + + local callback1 = function() + expect(x).to.equal(0) + expect(y).to.equal(0) + x = x + 1 + end + + local callback2 = function() + expect(x).to.equal(1) + expect(y).to.equal(0) + y = y + 1 + end + + signal:connect(callback1) + signal:connect(callback2) + signal:fire() + + expect(x).to.equal(1) + expect(y).to.equal(1) + end) + + it("should continue firing despite mid-event disconnection", function() + local signal = Signal.new() + local countA = 0 + local countB = 0 + + local connectionA + connectionA = signal:connect(function() + connectionA:disconnect() + countA = countA + 1 + end) + + signal:connect(function() + countB = countB + 1 + end) + + signal:fire() + + expect(countA).to.equal(1) + expect(countB).to.equal(1) + end) + + it("should skip listeners that were disconnected during event evaluation", function() + local signal = Signal.new() + local countA = 0 + local countB = 0 + + local connectionB + + signal:connect(function() + countA = countA + 1 + connectionB:disconnect() + end) + + connectionB = signal:connect(function() + countB = countB + 1 + end) + + signal:fire() + + expect(countA).to.equal(1) + expect(countB).to.equal(0) + end) +end \ No newline at end of file diff --git a/BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Utils/Spritesheet.lua b/BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Utils/Spritesheet.lua new file mode 100644 index 0000000000..0b9d15893d --- /dev/null +++ b/BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Utils/Spritesheet.lua @@ -0,0 +1,86 @@ +--[[ + Parses spritesheets into a list of sprites that you can then join with Roact ImageLabel/Button props + Sprites are returned in the order they appear in the spritesheet in the form of: + { + Image = asset, + ImageRectSize = SpriteSize, + ImageRectOffset = positionOfSpriteInSheet, + } + + Required arguments: + string asset - AssetId or path to the spritesheet + table props - Table of properties, similar to creating a Roact element + + Required props: + number/Vector2 SpriteSize - how large each sprite is (must be the same for all sprites) + a number SpriteSize is converted to a uniform Vector2 + number NumSprites - how many sprites there are in the spritesheet + + Optional props: + number SpritesheetWidth - how wide the entire spritesheet is. Defaults to 1024 (max image size) + You do not need to change this unless you break your sprites onto a new + line before X=1024 when there is still enough room for another sprite, or + your spritesheet is wider than 1024 (Don't do this! The engine will automatically + downscale images larger than the max size) + + Example usage: + + local expandButton = Spritesheet("rbxasset://textures/folder/expand.png", { + SpriteSize = 32, + NumSprites = 4, + } + + local onStateImageProps = expandButton[1] + local offStateImageProps = expandButton[2] + + component:render() + local expandImageProps = self.state.expanded and onStateImage or offStateImage + Roact.createElement("ImageLabel", Cryo.Dictionary.join(expandImageProps, { + ... + })) +]] + +-- TODO check if 1K limitation is necessary in /content or only web +local MAX_IMAGE_SIZE = 1024 + +local function Spritesheet(image, props) + local spriteSizeType = typeof(props.SpriteSize) + local spriteCountType = typeof(props.NumSprites) + local sheetWidthType = typeof(props.SpritesheetWidth) + + assert(spriteSizeType == "number" or spriteSizeType == "Vector2", + "SpriteSize must be number or Vector2. Got type '"..spriteSizeType.."'") + assert(spriteCountType == "number", + "NumSprites must be number. Got type'"..spriteCountType.."'") + assert(sheetWidthType == "number" or sheetWidthType == "nil", + "SpritesheetWidth must be a number or nil. Got '"..sheetWidthType.."'") + + local spriteSize = spriteSizeType == "number" and Vector2.new(1, 1) * props.SpriteSize or props.SpriteSize + local numSprites = props.NumSprites + local sheetWidth = props.SpritesheetWidth or MAX_IMAGE_SIZE + + assert(spriteSize.X > 0 and spriteSize.Y > 0, + "SpriteSize does not support <= 0 values. Got '"..tostring(spriteSize).."'") + assert(numSprites > 0, + "NumSprites must be > 0. Got '"..numSprites) + assert(sheetWidth > 0, + "SpritesheetWidth does not support <= 0 values. Got '"..sheetWidth.."'") + + local sprites = {} + + local numColumns = math.floor(sheetWidth / spriteSize.X) + for i = 0, props.NumSprites - 1 do + local row = math.floor(i / numColumns) + local column = i % numColumns + + table.insert(sprites, { + Image = image, + ImageRectSize = spriteSize, + ImageRectOffset = Vector2.new(column * spriteSize.X, row * spriteSize.Y), + }) + end + + return sprites +end + +return Spritesheet \ No newline at end of file diff --git a/BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Utils/Spritesheet.spec.lua b/BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Utils/Spritesheet.spec.lua new file mode 100644 index 0000000000..479459d975 --- /dev/null +++ b/BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Utils/Spritesheet.spec.lua @@ -0,0 +1,115 @@ +return function() + local Spritesheet = require(script.Parent.Spritesheet) + + it("should verify correct props", function() + local success,_ + + -- Missing required props + success,_ = pcall(function() + return Spritesheet("", {}) + end) + expect(success).to.equal(false) + + -- Missing required prop NumSprites + success,_ = pcall(function() + return Spritesheet("", {SpriteSize = 1, NumSprites = nil}) + end) + expect(success).to.equal(false) + + -- Missing required props SpriteSize + success,_ = pcall(function() + return Spritesheet("", {SpriteSize = nil, NumSprites = 1}) + end) + expect(success).to.equal(false) + + -- SpritesheetWidth is invalid type + success,_ = pcall(function() + return Spritesheet("", {SpriteSize = 1, NumSprites = 1, SpritesheetWidth = ""}) + end) + expect(success).to.equal(false) + + -- Has all required props of correct type + success,_ = pcall(function() + return Spritesheet("", {SpriteSize = 1, NumSprites = 1}) + end) + expect(success).to.equal(true) + + -- Has all required props of correct type + success,_ = pcall(function() + return Spritesheet("", {SpriteSize = Vector2.new(1,1), NumSprites = 1}) + end) + expect(success).to.equal(true) + + -- Has all props of correct type + success,_ = pcall(function() + return Spritesheet("", {SpriteSize = 1, NumSprites = 1, SpritesheetWidth = 1}) + end) + expect(success).to.equal(true) + + -- SpriteSize out of range + success,_ = pcall(function() + return Spritesheet("", {SpriteSize = 0, NumSprites = 1}) + end) + expect(success).to.equal(false) + + -- NumSprites out of range + success,_ = pcall(function() + return Spritesheet("", {SpriteSize = 1, NumSprites = 0}) + end) + expect(success).to.equal(false) + + -- SpritesheetWidth out of range + success,_ = pcall(function() + return Spritesheet("", {SpriteSize = 1, NumSprites = 1, SpritesheetWidth = 0}) + end) + expect(success).to.equal(false) + end) + + it("should return correct result for a single-row spritesheet", function() + local asset = "rbxasset://test" + local numSprites = 3 + local spriteSize = Vector2.new(32, 16) + local sprites = Spritesheet(asset, { + NumSprites = numSprites, + SpriteSize = spriteSize, + }) + + -- Correct number of sprites + expect(#sprites).to.equal(numSprites) + + -- Correct sprite properties + for i,sprite in pairs(sprites) do + expect(sprite.Image).to.equal(asset) + expect(sprite.ImageRectSize).to.equal(spriteSize) + expect(sprite.ImageRectOffset.X).to.equal((i - 1) * spriteSize.X) + expect(sprite.ImageRectOffset.Y).to.equal(0) + end + end) + + it("should return correct result for a multi-row spritesheet", function() + local asset = "rbxasset://test" + local numSprites = 5 + local spriteSize = Vector2.new(32, 16) + local sheetWidth = 66 + local sprites = Spritesheet(asset, { + NumSprites = numSprites, + SpriteSize = spriteSize, + SpritesheetWidth = sheetWidth, + }) + + -- Correct number of sprites + expect(#sprites).to.equal(numSprites) + + -- Correct sprite properties + local numColumns = math.floor(sheetWidth / spriteSize.X) + for i,sprite in pairs(sprites) do + local row = math.floor((i - 1) / numColumns) + local column = (i - 1) % numColumns + + expect(sprite.Image).to.equal(asset) + expect(sprite.ImageRectSize).to.equal(spriteSize) + expect(sprite.ImageRectOffset.X).to.equal(column * spriteSize.X) + expect(sprite.ImageRectOffset.Y).to.equal(row * spriteSize.Y) + end + end) +end \ No newline at end of file diff --git a/BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Utils/Symbol.lua b/BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Utils/Symbol.lua new file mode 100644 index 0000000000..d9e26d9c65 --- /dev/null +++ b/BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Utils/Symbol.lua @@ -0,0 +1,44 @@ +--[[ + A 'Symbol' is an opaque marker type. + + Symbols have the type 'userdata', but when printed to the console, the name + of the symbol is shown. +]] + +local Symbol = {} + +--[[ + Creates a Symbol with the given name. + + When printed or coerced to a string, the symbol will turn into the string + given as its name. +]] +function Symbol.named(name) + assert(type(name) == "string", "Symbols must be created using a string name!") + + local self = newproxy(true) + + local wrappedName = ("Symbol(%s)"):format(name) + + getmetatable(self).__tostring = function() + return wrappedName + end + + return self +end + +--[[ + Create an unnamed Symbol. Usually, you should create a named Symbol using + Symbol.named(name) +]] +function Symbol.unnamed() + local self = newproxy(true) + + getmetatable(self).__tostring = function() + return "Unnamed Symbol" + end + + return self +end + +return Symbol \ No newline at end of file diff --git a/BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Utils/Symbol.spec.lua b/BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Utils/Symbol.spec.lua new file mode 100644 index 0000000000..f3312055c9 --- /dev/null +++ b/BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Utils/Symbol.spec.lua @@ -0,0 +1,45 @@ +return function() + local Symbol = require(script.Parent.Symbol) + + describe("named", function() + it("should give an opaque object", function() + local symbol = Symbol.named("foo") + + expect(symbol).to.be.a("userdata") + end) + + it("should coerce to the given name", function() + local symbol = Symbol.named("foo") + + expect(tostring(symbol):match("foo")).to.be.ok() + end) + + it("should be unique when constructed", function() + local symbolA = Symbol.named("abc") + local symbolB = Symbol.named("abc") + + expect(symbolA).never.to.equal(symbolB) + end) + end) + + describe("unnamed", function() + it("should give an opaque object", function() + local symbol = Symbol.unnamed() + + expect(symbol).to.be.a("userdata") + end) + + it("should coerce to some string", function() + local symbol = Symbol.unnamed() + + expect(tostring(symbol)).to.be.a("string") + end) + + it("should be unique when constructed", function() + local symbolA = Symbol.unnamed() + local symbolB = Symbol.unnamed() + + expect(symbolA).never.to.equal(symbolB) + end) + end) +end diff --git a/BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Utils/Urls.lua b/BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Utils/Urls.lua new file mode 100644 index 0000000000..5327c279e5 --- /dev/null +++ b/BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Utils/Urls.lua @@ -0,0 +1,43 @@ +local ContentProvider = game:GetService("ContentProvider") + +-- helper functions +local function parseBaseUrlInformation() + -- get the current base url from the current configuration + local baseUrl = ContentProvider.BaseUrl + -- keep a copy of the base url + -- append a trailing slash if there isn't one + if baseUrl:sub(#baseUrl) ~= "/" then + baseUrl = baseUrl .. "/" + end + -- parse out scheme (http, https) + local _, schemeEnd = baseUrl:find("://") + -- parse out the prefix (www, kyle, ying, etc.) + local prefixIndex, prefixEnd = baseUrl:find("%.", schemeEnd + 1) + local basePrefix = baseUrl:sub(schemeEnd + 1, prefixIndex - 1) + -- parse out the domain + local baseDomain = baseUrl:sub(prefixEnd + 1) + return baseUrl, basePrefix, baseDomain +end + +-- url construction building blocks +local baseUrl, basePrefix, baseDomain = parseBaseUrlInformation() + +local BASE_GAMEASSET_URL = "https://assetgame.%sasset/?id=%d&#assetTypeId=%d&isPackage=%s" +local RBXTHUMB_BASE_URL = "rbxthumb://type=%s&id=%d&w=%d&h=%d" +local ASSET_ID_STRING = "rbxassetid://%d" + +local Urls = {} + +function Urls.constructAssetThumbnailUrl(assetId, width, height) + return RBXTHUMB_BASE_URL:format("Asset", tonumber(assetId) or 0, width, height) +end + +function Urls.constructAssetIdString(assetId) + return ASSET_ID_STRING:format(assetId) +end + +function Urls.constructAssetGameAssetIdUrl(assetId, assetTypeId, isPackage) + return BASE_GAMEASSET_URL:format(baseDomain, assetId, assetTypeId, isPackage) +end + +return Urls \ No newline at end of file diff --git a/BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Utils/getTimeString.lua b/BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Utils/getTimeString.lua new file mode 100644 index 0000000000..3cb586f375 --- /dev/null +++ b/BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Utils/getTimeString.lua @@ -0,0 +1,11 @@ +-- return mm:ss format. +return function(seconds) + assert(type(seconds) == "number", "seconds must be a number") + + local isNegative = seconds < 0 + local adjustedSeconds = math.abs(seconds) + local min = math.floor(adjustedSeconds / 60) + local sec = math.floor(adjustedSeconds % 60) + + return string.format("%s%d:%02d", isNegative and "-" or "", min, sec) +end \ No newline at end of file diff --git a/BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Utils/getTimeString.spec.lua b/BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Utils/getTimeString.spec.lua new file mode 100644 index 0000000000..d24dd1953c --- /dev/null +++ b/BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/Utils/getTimeString.spec.lua @@ -0,0 +1,51 @@ +return function() + local getTimeString = require(script.Parent.getTimeString) + + it("should return a string", function() + local result = getTimeString(0) + + expect(result).to.be.a("string") + end) + + it("should handle 30 seconds correctly", function() + local result = getTimeString(30) + + expect(result).to.equal("0:30") + end) + + it("should ignore decimals", function() + local result = getTimeString(1.5) + + expect(result).to.equal("0:01") + end) + + it("should ignore negative decimals", function() + local result = getTimeString(-1.5) + + expect(result).to.equal("-0:01") + end) + + it("should handle 60 seconds correctly", function() + local result = getTimeString(60) + + expect(result).to.equal("1:00") + end) + + it("should handle 90 seconds correctly", function() + local result = getTimeString(90) + + expect(result).to.equal("1:30") + end) + + it("should handle 120 seconds correctly", function() + local result = getTimeString(120) + + expect(result).to.equal("2:00") + end) + + it("should handle 150 seconds correctly", function() + local result = getTimeString(150) + + expect(result).to.equal("2:30") + end) +end \ No newline at end of file diff --git a/BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/createTheme.lua b/BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/createTheme.lua new file mode 100644 index 0000000000..b9dd6a2d70 --- /dev/null +++ b/BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/createTheme.lua @@ -0,0 +1,578 @@ +--[[ + Transforms values from an app theme into a theme usable by the UILibrary. + + Parameters: + table style: + Specifies default style values which will be used to construct the theme. + These include the basic palette colors (backgroundColor, textColor, etc), + as well as other top-level fonts and sizes. Refer to defaultStyle for + a list of style values that can be overridden. + + table overrides (optional): + After the theme is created, overrides can be set for specific elements. + + For example, an overrides table of: + { + checkBox.backgroundColor = Color3.new(1, 1, 1), + } + + Would change the background color of only the Checkbox component. +]] + +local Style = require(script.Parent.StyleDefaults) +local replaceDefaults = require(script.Parent.deepJoin) + +return function(style, overrides) + style = style or {} + overrides = overrides or {} + + style = replaceDefaults(Style.Defaults, style) + assert(Style.isValid(style), "Provided style table could not be validated.") + + -- Theme entries for UILibrary components are defined below + local checkBox = { + font = style.font, + + --TODO: Move texture to StudioSharedUI + backgroundImage = "rbxasset://textures/GameSettings/UncheckedBox.png", + selectedImage = "rbxasset://textures/GameSettings/CheckedBoxLight.png", + + backgroundColor = style.backgroundColor, + titleColor = style.textColor, + } + + local roundFrame = { + --TODO: Move texture to StudioSharedUI + backgroundImage = "rbxasset://textures/StudioToolbox/RoundedBackground.png", + borderImage = "rbxasset://textures/StudioToolbox/RoundedBorder.png", + slice = Rect.new(3, 3, 13, 13), + } + + local dropShadow = { + --TODO: Move texture to StudioSharedUI + image = "rbxasset://textures/StudioUIEditor/resizeHandleDropShadow.png", + } + + local tooltip = { + font = style.font, + textSize = 12, + + backgroundColor = style.itemColor, + borderColor = style.borderColor, + textColor = style.textColor, + shadowColor = style.shadowColor, + shadowTransparency = style.shadowTransparency, + } + + local keyframe = { + Default = { + backgroundColor = style.itemColor, + borderColor = style.borderColor, + + selected = { + backgroundColor = style.selectionColor, + borderColor = style.selectionBorderColor, + }, + }, + + Primary = { + backgroundColor = style.primaryItemColor, + borderColor = style.primaryBorderColor, + + selected = { + backgroundColor = style.primaryHoveredItemColor, + borderColor = style.selectionBorderColor, + }, + }, + } + + local scrubber = { + backgroundColor = style.selectionColor, + image = "", + } + + local scrollingFrame = { + --TODO: Move texture to StudioSharedUI + topImage = "rbxasset://textures/StudioToolbox/ScrollBarTop.png", + midImage = "rbxasset://textures/StudioToolbox/ScrollBarMiddle.png", + bottomImage = "rbxasset://textures/StudioToolbox/ScrollBarBottom.png", + + backgroundColor = style.backgroundColor, + scrollbarColor = style.borderColor, + } + + local radioButton = { + radioButtonBackground = "rbxasset://textures/GameSettings/RadioButton.png", + radioButtonColor = style.separationLineColor, + radioButtonSelected = "rbxasset://textures/ui/LuaApp/icons/ic-blue-dot.png", + textSize = 18, + buttonHeight = 20, + font = style.font, + textColor = style.textColor, + contentPadding = 16, + buttonPadding = 6, + } + + local dropdownMenu = { + borderColor = style.borderColor, + --TODO: Move texture to StudioSharedUI + borderImage = "rbxasset://textures/StudioToolbox/RoundedBorder.png", + } + + local styledDropdown = { + font = style.font, + + backgroundColor = style.backgroundColor, + borderColor = style.borderColor, + textColor = style.textColor, + + --TODO: Move texture to StudioSharedUI + arrowImage = "rbxasset://textures/StudioToolbox/ArrowDownIconWhite.png", + + hovered = { + backgroundColor = style.hoveredItemColor, + textColor = style.hoveredTextColor, + }, + + selected = { + backgroundColor = style.selectionColor, + borderColor = style.selectionBorderColor, + textColor = style.selectedTextColor, + }, + } + + local detailedDropdown = { + font = style.font, + + backgroundColor = style.backgroundColor, + disabled = style.disabledColor, + disabledText = style.dimmerTextColor, + borderColor = style.borderColor, + displayText = style.textColor, + descriptionText = style.subTextColor, + + --TODO: Move texture to StudioSharedUI + arrowImage = "rbxasset://textures/StudioToolbox/ArrowDownIconWhite.png", + + hovered = { + backgroundColor = style.hoveredItemColor, + displayText = style.hoveredTextColor, + }, + + selected = { + backgroundColor = style.selectionColor, + disabled = style.disabledColor, + borderColor = style.selectionBorderColor, + displayText = style.selectedTextColor, + }, + } + + local titledFrame = { + font = style.font, + text = style.subTextColor, + } + + local textBox = { + font = style.font, + background = style.backgroundColor, + disabled = style.disabledColor, + borderDefault = style.borderColor, + borderHover = style.hoverColor, + tooltip = style.dimmerTextColor, + text = style.textColor, + error = style.errorColor, + } + + local textButton = { + font = style.font, + } + + local textEntry = { + textTransparency = { + enabled = 0, + disabled = 0.5 + } + } + + local separator = { + lineColor = style.borderColor, + } + + local treeView = { + elementPadding = 4, + margins = { + left = 2, + top = 2, + right = 2, + bottom = 2, + }, + indentOffset = 8, + scrollbar = replaceDefaults(scrollingFrame, { + scrollbarThickness = 16, + scrollbarPadding = 2, + scrollbarImageColor = style.borderColor, + }), + defaultElementWidth = 140, + } + + local dialog = { + font = style.font, + + background = style.backgroundColor, + textColor = style.textColor, + } + + local bulletPoint = { + font = style.font, + + text = style.textColor, + } + + local button = { + Default = { + font = style.font, + isRound = true, + + backgroundColor = style.itemColor, + textColor = style.textColor, + borderColor = style.borderColor, + + hovered = { + backgroundColor = style.hoveredItemColor, + textColor = style.hoveredTextColor, + borderColor = style.borderColor, + }, + }, + + Primary = { + font = style.font, + isRound = true, + + backgroundColor = style.primaryItemColor, + textColor = style.primaryTextColor, + borderColor = style.primaryBorderColor, + + hovered = { + backgroundColor = style.primaryHoveredItemColor, + textColor = style.primaryHoveredTextColor, + borderColor = style.primaryHoveredBorderColor, + }, + }, + } + + local loadingBar = { + font = style.font, + fontSize = 16, + text = style.textColor, + bar = { + foregroundColor = style.dimmerTextColor, + backgroundColor = style.backgroundColor, + }, + } + + local loadingIndicator = { + baseColor = style.hoveredItemColor, + endColor = style.dimmerTextColor, + } + + local toggleButton = { + defaultWidth = 20, + defaultHeight = 20, + + onImage = "rbxasset://textures/RoactStudioWidgets/toggle_on_light.png", + offImage = "rbxasset://textures/RoactStudioWidgets/toggle_off_light.png", + disabledImage = "rbxasset://textures/RoactStudioWidgets/toggle_disable_light.png", + } + + local hyperlink = { + textSize = 22, + textColor = style.hyperlinkTextColor, + font = style.font, + } + + local assetPreview = { + font = style.font, + textSize = 14, + textSizeMedium = 16, + textSizeLarge = 18, + textSizeTitle= 22, + fontBold = style.font, + background = style.backgroundColor, + + padding = 12, + + assetNameColor = style.textColor, + descriptionTextColor = style.textColor, + + actionBar = { + background = style.backgroundColor, + + button = { + backgroundColor = style.primaryItemColor, + backgroundDisabledColor = style.disabledColor, + backgroundHoveredColor = style.primaryHoveredItemColor + }, + + showMore = { + backgroundColor = style.backgroundColor, + borderColor = style.borderColor + }, + + text = { + color = style.textColor, + colorDisabled = style.disabledColor, + }, + + padding = 12, + centerPadding = 10, + + robuxSize = UDim2.fromOffset(16,16), + + images = { + showMore = "rbxasset://textures/StudioToolbox/AssetPreview/more.png", + robuxSmall = "rbxasset://textures/ui/common/robux_small.png", + colorWhite = Color3.fromRGB(255, 255, 255), + } + }, + + description = { + height = 28, + + searchBarIconSize = 14, + padding = 8, + + backgroundColor = style.backgroundColor, + leftTextColor = style.textColor, + rightTextColor = style.textColor, + lineColor = style.borderColor, + + images = { + searchIcon = "rbxasset://textures/StudioToolbox/Search.png", + }, + }, + + images = { + deleteButton = "rbxasset://textures/StudioToolbox/DeleteButton.png", + scrollbarTopImage = "rbxasset://textures/StudioToolbox/ScrollBarTop.png", + scrollbarMiddleImage = "rbxasset://textures/StudioToolbox/ScrollBarMiddle.png", + scrollbarBottomImage = "rbxasset://textures/StudioToolbox/ScrollBarBottom.png", + thumbUpSmall = "rbxasset://textures/StudioToolbox/AssetPreview/rating_small.png", + }, + + favorites = { + contentColor = Color3.fromRGB(246, 183, 2), + favorited = "rbxasset://textures/StudioToolbox/AssetPreview/star_filled.png", + unfavorited = "rbxasset://textures/StudioToolbox/AssetPreview/star_stroke.png" + }, + + imagePreview = { + background = style.backgroundColor, + textColor = style.textColor, + }, + + modelPreview = { + background = style.backgroundColor, + }, + + thumbnailIconPreview = { + background = style.backgroundColor, + textColor = style.textColor, + + textLabelPadding = 20, + iconSize = 16, + defaultTextLabelHeight = 20, + }, + + treeViewButton = { + buttonSize = 28, + backgroundTrans = 0.25, + backgroundColor = style.background, + backgroundDisabledColor = style.disabledColor, + hierarchy = "rbxasset://textures/StudioToolbox/AssetPreview/hierarchy.png" + }, + + audioPreview = { + backgroundColor = style.backgroundColor, + textColor = style.textColor, + playButton = "rbxasset://textures/StudioToolbox/AssetPreview/play_button.png", + pauseButton = "rbxasset://textures/StudioToolbox/AssetPreview/pause_button.png", + buttonBackgroundColor = style.background, + buttonDisabledBackgroundColor = style.disabledColor, + buttonDisabledBackgroundTransparency = 0.5, + buttonColor = style.textColor, + audioPlay_BG = "rbxasset://textures/StudioToolbox/AssetPreview/audioPlay_BG.png", + audioPlay_BG_Color = Color3.fromRGB(204, 204, 204), + progressBar = Color3.fromRGB(0, 162, 255), + progressBar_BG_Color = style.background, + progressKnob = "rbxasset://textures/DeveloperFramework/slider_knob.png", + progressKnobColor = style.background, + font = style.font, + fontSize = 16, + }, + + videoPreview = { + backgroundColor = style.backgroundColor, + videoBackgroundColor = style.backgroundColor, + playButton = "rbxasset://textures/StudioToolbox/AssetPreview/play_button.png", + pauseButton = "rbxasset://textures/StudioToolbox/AssetPreview/pause_button.png", + pauseOverlayColor =Color3.fromRGB(0, 0, 0), + pauseOverlayTransparency = 0.5, + }, + + vote = { + backgroundTrans = 0.9, + background = style.backgroundColor, + borderColor = style.borderColor, + textColor = style.textColor, + subTextColor = style.subTextColor, + + button = { + backgroundColor = style.itemColor, + backgroundTrans = 0, + disabledColor = Color3.fromRGB(10, 10, 10), + }, + + voteUp = { + backgroundColor = Color3.fromRGB(0, 100, 0), + borderColor = style.borderColor, + }, + + voteDown = { + backgroundColor = Color3.fromRGB(100, 0, 0), + borderColor = style.borderColor, + }, + + images = { + voteDown = "rbxasset://textures/StudioToolbox/AssetPreview/vote_down.png", + voteUp = "rbxasset://textures/StudioToolbox/AssetPreview/vote_up.png", + thumbUp = "rbxasset://textures/StudioToolbox/AssetPreview/rating_large.png" + } + }, + } + + local searchBar = { + backgroundColor = style.backgroundColor, + + text = { + placeholder = { + color = style.dimmerTextColor, + }, + font = style.font, + size = 16, + color = style.textColor, + }, + + divideLine = { + color = style.borderColor, + }, + + border = { + hovered = { + color = style.hoverColor, + }, + selected = { + color = style.selectionBorderColor, + }, + color = style.borderColor, + }, + + buttons = { + iconSize = 14, + size = 28, + inset = 2, + clear = { + color = Color3.fromRGB(184, 184, 184), + }, + + search = { + hovered = { + color = Color3.fromRGB(0, 162, 255), + }, + color = Color3.fromRGB(184, 184, 184), + }, + }, + + images = { + clear = { + hovered = { + image = "rbxasset://textures/StudioSharedUI/clear-hover.png", + }, + image = "rbxasset://textures/StudioSharedUI/clear.png", + }, + + search = { + image = "rbxasset://textures/StudioSharedUI/search.png", + }, + }, + } + + local instanceTreeView = { + font = style.font, + textSize = 14, + + background = style.background, + + treeItemHeight = 16, + treeViewIndent = 20, + + scrollbarPadding = 2, + scrollbarThickness = 8, + + scrollbarTopImage = "rbxasset://textures/StudioToolbox/ScrollBarTop.png", + scrollbarMiddleImage = "rbxasset://textures/StudioToolbox/ScrollBarMiddle.png", + scrollBarBottomImage = "rbxasset://textures/StudioToolbox/ScrollBarBottom.png", + + arrowExpanded = "rbxasset://textures/StudioToolbox/ArrowExpanded.png", + arrowCollapsed = "rbxasset://textures/StudioToolbox/ArrowCollapsed.png", + + elementPadding = 4, + + borderPadding = 15, + + tooltipShowDelay = 0.3, + + arrowColor = style.textColor, + selectedText = style.selectedTextColor, + textColor = style.textColor, + selected = style.selectedTextColor, + hover = style.hoverColor, + } + + local styledTooltip = { + backgroundColor = style.itemColor, + shadowColor = style.shadowColor, + shadowTransparency = style.shadowTransparency, + shadowOffset = Vector2.new(1, 1), + } + + return replaceDefaults({ + assetPreview = assetPreview, + checkBox = checkBox, + roundFrame = roundFrame, + dropShadow = dropShadow, + tooltip = tooltip, + keyframe = keyframe, + scrollingFrame = scrollingFrame, + dropdownMenu = dropdownMenu, + styledDropdown = styledDropdown, + detailedDropdown = detailedDropdown, + titledFrame = titledFrame, + textBox = textBox, + textButton = textButton, + textEntry = textEntry, + separator = separator, + dialog = dialog, + button = button, + scrubber = scrubber, + loadingBar = loadingBar, + loadingIndicator = loadingIndicator, + bulletPoint = bulletPoint, + toggleButton = toggleButton, + radioButton = radioButton, + treeView = treeView, + hyperlink = hyperlink, + instanceTreeView = instanceTreeView, + searchBar = searchBar, + styledTooltip = styledTooltip, + }, overrides) +end \ No newline at end of file diff --git a/BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/deepJoin.lua b/BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/deepJoin.lua new file mode 100644 index 0000000000..8affe568ef --- /dev/null +++ b/BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/deepJoin.lua @@ -0,0 +1,31 @@ +local function deepJoin(t1, t2) + local new = {} + + for key, value in pairs(t1) do + if typeof(value) == "table" then + if t2[key] and typeof(t2[key]) == "table" then + new[key] = deepJoin(value, t2[key]) + else + -- this essentially acts like a deepcopy to prevent + -- references getting all tangled up + new[key] = deepJoin(value, {}) + end + else + new[key] = value + end + end + + for key, value in pairs(t2) do + if typeof(value) == "table" then + if not t1[key] then + new[key] = deepJoin(value, {}) + end + else + new[key] = value + end + end + + return new +end + +return deepJoin \ No newline at end of file diff --git a/BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/deepJoin.spec.lua b/BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/deepJoin.spec.lua new file mode 100644 index 0000000000..de11f7c5d5 --- /dev/null +++ b/BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/deepJoin.spec.lua @@ -0,0 +1,76 @@ +return function() + local Library = script.Parent + local deepJoin = require(Library.deepJoin) + + it("should join two tables together", function() + local tableA = {key1 = "Value1"} + local tableB = {key2 = "Value2"} + + local result = deepJoin(tableA, tableB) + + expect(result.key1).to.equal("Value1") + expect(result.key2).to.equal("Value2") + end) + + it("should add all the entries from the first table", function() + local tableA = {key1 = "Value1", key2 = "Value2"} + local tableB = {} + + local result = deepJoin(tableA, tableB) + + expect(result.key1).to.equal("Value1") + expect(result.key2).to.equal("Value2") + end) + + it("should add all the entries from the second table", function() + local tableA = {} + local tableB = {key1 = "Value1", key2 = "Value2"} + + local result = deepJoin(tableA, tableB) + + expect(result.key1).to.equal("Value1") + expect(result.key2).to.equal("Value2") + end) + + it("should join values in nested tables", function() + local tableA = { + set = { + key1 = "Value1", + }, + } + + local tableB = { + set = { + key2 = "Value2", + }, + } + + local result = deepJoin(tableA, tableB) + + expect(result.set).to.be.ok() + expect(result.set.key1).to.equal("Value1") + expect(result.set.key2).to.equal("Value2") + end) + + it("should prioritize the second table if values overlap", function() + local tableA = { + outsideKey = "Old", + set = { + insideKey = "Old", + }, + } + + local tableB = { + outsideKey = "New", + set = { + insideKey = "New", + }, + } + + local result = deepJoin(tableA, tableB) + + expect(result.outsideKey).to.equal("New") + expect(result.set).to.be.ok() + expect(result.set.insideKey).to.equal("New") + end) +end \ No newline at end of file diff --git a/BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/join.lua b/BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/join.lua new file mode 100644 index 0000000000..2e0d809150 --- /dev/null +++ b/BuiltInPlugins/LocalizationTools/Packages/UILibrary/_internal/join.lua @@ -0,0 +1,15 @@ +local function join(...) + local new = {} + + for i = 1, select("#", ...) do + local source = select(i, ...) + + for key, value in pairs(source) do + new[key] = value + end + end + + return new +end + +return join \ No newline at end of file diff --git a/BuiltInPlugins/LocalizationTools/Src/Components/UploadDialogContent.lua b/BuiltInPlugins/LocalizationTools/Src/Components/UploadDialogContent.lua index 80151e0198..eca62acc03 100644 --- a/BuiltInPlugins/LocalizationTools/Src/Components/UploadDialogContent.lua +++ b/BuiltInPlugins/LocalizationTools/Src/Components/UploadDialogContent.lua @@ -1,3 +1,6 @@ +--!nolint ImplicitReturn +--^ DEVTOOLS-4493 + --[[ The content to be rendered in confirm dialog ]] @@ -259,4 +262,4 @@ ContextServices.mapToProps(UploadDialogContent, { Localization = ContextServices.Localization, }) -return UploadDialogContent \ No newline at end of file +return UploadDialogContent diff --git a/BuiltInPlugins/PlayerEmulator/Packages/Framework.lua b/BuiltInPlugins/PlayerEmulator/Packages/Framework.lua index d1c77a7116..401dae2663 100644 --- a/BuiltInPlugins/PlayerEmulator/Packages/Framework.lua +++ b/BuiltInPlugins/PlayerEmulator/Packages/Framework.lua @@ -7,6 +7,7 @@ return strict({ Http = require(script.Http), RobloxAPI = require(script.RobloxAPI), StudioUI = require(script.StudioUI), + Style = require(script.Style), TestHelpers = require(script.TestHelpers), UI = require(script.UI), Util = require(script.Util), diff --git a/BuiltInPlugins/PlayerEmulator/Packages/Framework/ContextServices.lua b/BuiltInPlugins/PlayerEmulator/Packages/Framework/ContextServices.lua index aec15f6aa5..1eda5fb68d 100644 --- a/BuiltInPlugins/PlayerEmulator/Packages/Framework/ContextServices.lua +++ b/BuiltInPlugins/PlayerEmulator/Packages/Framework/ContextServices.lua @@ -1,7 +1,7 @@ --[[ Public interface for ContextServices ]] - +local Framework = script.Parent local Src = script local strict = require(Src.Parent.Util.strict) @@ -21,6 +21,7 @@ local Plugin = require(Src.Plugin) local PluginActions = require(Src.PluginActions) local Provider = require(Src.Provider) local Store = require(Src.Store) +local Stylizer = require(Framework.Style.Stylizer) local Theme = require(Src.Theme) local UILibraryWrapper = require(Src.UILibraryWrapper) @@ -43,6 +44,7 @@ local ContextServices = strict({ Plugin = Plugin, PluginActions = PluginActions, Provider = Provider, + Stylizer = Stylizer, Store = Store, Theme = Theme, UILibraryWrapper = UILibraryWrapper, diff --git a/BuiltInPlugins/PlayerEmulator/Packages/Framework/ContextServices/ContextItem.lua b/BuiltInPlugins/PlayerEmulator/Packages/Framework/ContextServices/ContextItem.lua index 45aebb4a0b..4d4672b6b1 100644 --- a/BuiltInPlugins/PlayerEmulator/Packages/Framework/ContextServices/ContextItem.lua +++ b/BuiltInPlugins/PlayerEmulator/Packages/Framework/ContextServices/ContextItem.lua @@ -92,6 +92,13 @@ function ContextItem:createProvider() error(message, 0) end +--[[ + Cleans up the context item (e.g. disconnecting from events). + Optional override +]] +function ContextItem:destroy() +end + function ContextItem:__tostring() return tostring(self.__name) end @@ -110,7 +117,8 @@ end callback getChangedSignal: any => Signal optional Should return a signal for this context item to connect to. When that signal fires, this context item updates. If not provided, then the context item will be static - calllabck verifyNewItem: A callback fired when the simple ContextItem is being created for verification purposes. + callback verifyNewItem: A callback fired when the simple ContextItem is being created for verification purposes. + callback destroy: Optional function to destroy the wrapped object ]] function ContextItem:createSimple(name, options) assert(name, "ContextItem:createSimple expects a name parameter") @@ -144,6 +152,11 @@ function ContextItem:createSimple(name, options) self._connection:Disconnect() self._connection = nil end + + if options.destroy then + options.destroy(self._obj) + end + self._obj = nil end function SimpleContextItem:get() diff --git a/BuiltInPlugins/PlayerEmulator/Packages/Framework/ContextServices/FastFlags.lua b/BuiltInPlugins/PlayerEmulator/Packages/Framework/ContextServices/FastFlags.lua index 9645edae58..c65f15e479 100644 --- a/BuiltInPlugins/PlayerEmulator/Packages/Framework/ContextServices/FastFlags.lua +++ b/BuiltInPlugins/PlayerEmulator/Packages/Framework/ContextServices/FastFlags.lua @@ -27,7 +27,7 @@ function FastFlags.new(featuresMap, overrides) local flags = Flags.new(featuresMap) for featureName, isOn in pairs(overrides) do - flags:setLocalOverride(featuresName, isOn) + flags:setLocalOverride(featureName, isOn) end local self = { diff --git a/BuiltInPlugins/PlayerEmulator/Packages/Framework/ContextServices/FastFlags.spec.lua b/BuiltInPlugins/PlayerEmulator/Packages/Framework/ContextServices/FastFlags.spec.lua index e475804219..51166e1c3c 100644 --- a/BuiltInPlugins/PlayerEmulator/Packages/Framework/ContextServices/FastFlags.spec.lua +++ b/BuiltInPlugins/PlayerEmulator/Packages/Framework/ContextServices/FastFlags.spec.lua @@ -4,7 +4,6 @@ return function() local mapToProps = require(Framework.ContextServices.mapToProps) local provide = require(Framework.ContextServices.provide) - local Flags = require(Framework.Util).Flags local FastFlags = require(script.Parent.FastFlags) it("should construct just fine with no arguments", function() @@ -58,4 +57,4 @@ return function() expect(didRender).to.equal(true) end) -end \ No newline at end of file +end diff --git a/BuiltInPlugins/PlayerEmulator/Packages/Framework/ContextServices/Localization.lua b/BuiltInPlugins/PlayerEmulator/Packages/Framework/ContextServices/Localization.lua index c3280ddcc1..74e9154969 100644 --- a/BuiltInPlugins/PlayerEmulator/Packages/Framework/ContextServices/Localization.lua +++ b/BuiltInPlugins/PlayerEmulator/Packages/Framework/ContextServices/Localization.lua @@ -268,6 +268,14 @@ function Localization.mock() end } + local currentLocale = 0 + local localeIDs = {"en-us", "es", "es-es", "ko", "ja"} + local function getLocale() + currentLocale = (currentLocale + 1) % 5 + local nextLocale = localeIDs[currentLocale] + return nextLocale + end + -- create a mock localization object for tests return Localization.new({ -- create a mock resource file that mimics the real thing @@ -278,8 +286,9 @@ function Localization.mock() -- for tests, don't connect to any system signals to ensure stuff doesn't change mid test overrideLocaleChangedSignal = Signal.new(), + getLocale = getLocale, }) end -return Localization \ No newline at end of file +return Localization diff --git a/BuiltInPlugins/PlayerEmulator/Packages/Framework/ContextServices/Mouse.spec.lua b/BuiltInPlugins/PlayerEmulator/Packages/Framework/ContextServices/Mouse.spec.lua index 94f3f1c1aa..c0abe8041d 100644 --- a/BuiltInPlugins/PlayerEmulator/Packages/Framework/ContextServices/Mouse.spec.lua +++ b/BuiltInPlugins/PlayerEmulator/Packages/Framework/ContextServices/Mouse.spec.lua @@ -54,7 +54,7 @@ return function() mouse:__pushCursor("PointingHand") mouse:__pushCursor("PointingHand", 2) mouse:__resetCursor() - expect(next(mouse.cursors)).never.to.be.ok() + expect((next(mouse.cursors))).never.to.be.ok() end) it("should be a stack", function() @@ -94,4 +94,4 @@ return function() expect(test.Icon).to.equal("rbxasset://SystemCursors/Arrow") end) end) -end \ No newline at end of file +end diff --git a/BuiltInPlugins/PlayerEmulator/Packages/Framework/ContextServices/PluginActions.spec.lua b/BuiltInPlugins/PlayerEmulator/Packages/Framework/ContextServices/PluginActions.spec.lua index d6c4b52bb4..798ee03a52 100644 --- a/BuiltInPlugins/PlayerEmulator/Packages/Framework/ContextServices/PluginActions.spec.lua +++ b/BuiltInPlugins/PlayerEmulator/Packages/Framework/ContextServices/PluginActions.spec.lua @@ -18,12 +18,12 @@ return function() local Roact = require(Framework.Parent.Roact) local provide = require(Framework.ContextServices.provide) local mapToProps = require(Framework.ContextServices.mapToProps) - local mockPlugin = require(Framework.TestHelpers.Services.mockPlugin) + local MockPlugin = require(Framework.TestHelpers.Instances.MockPlugin) local PluginActions = require(script.Parent.PluginActions) it("should be providable as a ContextItem and call CreatePluginAction", function() - local plugin = mockPlugin.new() + local plugin = MockPlugin.new() local spy, wrapped = Spy.new(function(self, id) @@ -82,4 +82,4 @@ return function() Roact.unmount(instance) end) -end \ No newline at end of file +end diff --git a/BuiltInPlugins/PlayerEmulator/Packages/Framework/ContextServices/Theme.spec.lua b/BuiltInPlugins/PlayerEmulator/Packages/Framework/ContextServices/Theme.spec.lua index 22377dc00b..1547fb3c11 100644 --- a/BuiltInPlugins/PlayerEmulator/Packages/Framework/ContextServices/Theme.spec.lua +++ b/BuiltInPlugins/PlayerEmulator/Packages/Framework/ContextServices/Theme.spec.lua @@ -11,6 +11,13 @@ return function() local StyleTable = Util.StyleTable local StyleModifier = Util.StyleModifier local Signal = Util.Signal + local FlagsList = Util.Flags.new({ + FFlagRefactorDevFrameworkTheme = {"RefactorDevFrameworkTheme"}, + }) + + if FlagsList:get("FFlagRefactorDevFrameworkTheme") then + return + end local function createTestThemedComponent(render) local TestThemedComponent = Roact.PureComponent:extend("TestThemedComponent") diff --git a/BuiltInPlugins/PlayerEmulator/Packages/Framework/ContextServices/UILibraryWrapper.lua b/BuiltInPlugins/PlayerEmulator/Packages/Framework/ContextServices/UILibraryWrapper.lua index 13fa54244a..58a768f0bf 100644 --- a/BuiltInPlugins/PlayerEmulator/Packages/Framework/ContextServices/UILibraryWrapper.lua +++ b/BuiltInPlugins/PlayerEmulator/Packages/Framework/ContextServices/UILibraryWrapper.lua @@ -9,17 +9,23 @@ UILibraryWrapper expects to be provided after a Theme, Plugin, and Focus ContextItem. ]] - -local noGetThemeError = [[ -UILibraryProvider expects Theme to have a 'getUILibraryTheme' instance function.]] - local Framework = script.Parent.Parent local Util = require(Framework.Util) local FlagsList = Util.Flags.new({ FFlagStudioDevFrameworkPackage = {"StudioDevFrameworkPackage"}, FFlagRefactorDevFrameworkContextItems = {"RefactorDevFrameworkContextItems"}, + FFlagRefactorDevFrameworkTheme = {"RefactorDevFrameworkTheme"}, }) +local noGetThemeError +if FlagsList:get("FFlagRefactorDevFrameworkTheme") then + noGetThemeError = [[ + UILibraryProvider expects Stylizer to have a 'getUILibraryTheme' instance function.]] +else + noGetThemeError = [[ + UILibraryProvider expects Theme to have a 'getUILibraryTheme' instance function.]] +end + local isUsedAsPackage = require(Framework.Util.isUsedAsPackage) local shouldGetUILibraryFromParent = not FlagsList:get("FFlagStudioDevFrameworkPackage") or @@ -39,6 +45,7 @@ end local Roact = require(Framework.Parent.Roact) local ContextItem = require(Framework.ContextServices.ContextItem) local mapToProps = require(Framework.ContextServices.mapToProps) +local Stylizer = require(Framework.Style.Stylizer) local Theme = require(Framework.ContextServices.Theme) local Plugin = require(Framework.ContextServices.Plugin) local Focus = require(Framework.ContextServices.Focus) @@ -48,7 +55,12 @@ local UILibraryProvider = Roact.PureComponent:extend("UILibraryProvider") function UILibraryProvider:render() local props = self.props local plugin = props.Plugin - local theme = props.Theme + local theme + if FlagsList:get("FFlagRefactorDevFrameworkTheme") then + theme = props.Stylizer + else + theme = props.Theme + end local focus = props.Focus local UILibrary = props.UILibrary @@ -64,7 +76,8 @@ function UILibraryProvider:render() end mapToProps(UILibraryProvider, { - Theme = Theme, + Stylizer = FlagsList:get("FFlagRefactorDevFrameworkTheme") and Stylizer or nil, + Theme = (not FlagsList:get("FFlagRefactorDevFrameworkTheme")) and Theme or nil, Plugin = Plugin, Focus = Focus, }) diff --git a/BuiltInPlugins/PlayerEmulator/Packages/Framework/ContextServices/UILibraryWrapper.spec.lua b/BuiltInPlugins/PlayerEmulator/Packages/Framework/ContextServices/UILibraryWrapper.spec.lua index 0823cb2280..d4bbb673db 100644 --- a/BuiltInPlugins/PlayerEmulator/Packages/Framework/ContextServices/UILibraryWrapper.spec.lua +++ b/BuiltInPlugins/PlayerEmulator/Packages/Framework/ContextServices/UILibraryWrapper.spec.lua @@ -2,8 +2,10 @@ return function() local Framework = script.Parent.Parent local Util = require(Framework.Util) + local Signal = Util.Signal local FlagsList = Util.Flags.new({ FFlagStudioDevFrameworkPackage = {"StudioDevFrameworkPackage"}, + FFlagRefactorDevFrameworkTheme = {"RefactorDevFrameworkTheme"}, }) local isUsedAsPackage = require(Framework.Util.isUsedAsPackage) @@ -18,6 +20,7 @@ return function() local Focus = require(Framework.ContextServices.Focus) local Theme = require(Framework.ContextServices.Theme) local UILibraryWrapper = require(script.Parent.UILibraryWrapper) + local StudioTheme = require(Framework.Style.Themes.StudioTheme) local WrapperStub = Roact.PureComponent:extend("UILibraryWrapper") @@ -29,12 +32,26 @@ return function() Wrapper = WrapperStub } + local function createThemeMock() + + if FlagsList:get("FFlagRefactorDevFrameworkTheme") then + return StudioTheme.mock() + else + local createStyles = function() + return {} + end + local getTheme = function() + return "Light" + end + local themeChanged = Signal.new() + return Theme.mock(createStyles, getTheme, themeChanged) + end + end + it("should expect a Plugin ContextItem provided above", function() local focus = Focus.new(Instance.new("ScreenGui")) local wrapper = UILibraryWrapper.new(UILibraryStub) - local theme = Theme.new(function() - return {} - end) + local theme = createThemeMock() function theme:getUILibraryTheme() return {} end @@ -55,9 +72,7 @@ return function() it("should expect a Focus ContextItem provided above", function() local plugin = Plugin.new({}) local wrapper = UILibraryWrapper.new(UILibraryStub) - local theme = Theme.new(function() - return {} - end) + local theme = createThemeMock() function theme:getUILibraryTheme() return {} end @@ -97,9 +112,7 @@ return function() local plugin = Plugin.new({}) local focus = Focus.new(Instance.new("ScreenGui")) local wrapper = UILibraryWrapper.new(UILibraryStub) - local theme = Theme.new(function() - return {} - end) + local theme = createThemeMock() local element = provide({ plugin, @@ -119,9 +132,7 @@ return function() local plugin = Plugin.new({}) local focus = Focus.new(Instance.new("ScreenGui")) local wrapper = UILibraryWrapper.new(UILibraryStub) - local theme = Theme.new(function() - return {} - end) + local theme = createThemeMock() function theme:getUILibraryTheme() return {} end diff --git a/BuiltInPlugins/PlayerEmulator/Packages/Framework/ContextServices/mapToProps.lua b/BuiltInPlugins/PlayerEmulator/Packages/Framework/ContextServices/mapToProps.lua index d857093479..c0018d114a 100644 --- a/BuiltInPlugins/PlayerEmulator/Packages/Framework/ContextServices/mapToProps.lua +++ b/BuiltInPlugins/PlayerEmulator/Packages/Framework/ContextServices/mapToProps.lua @@ -42,14 +42,32 @@ local function mapToProps(component, contextMap) string.format(missingRenderMessage, tostring(component))) assert(contextMap, "mapToProps expects a contextMap table.") + local __initWithContext = component.init component.__renderWithContext = component.render + function component:init(props) + for key, item in pairs(contextMap) do + if item.initConsumer then + item:initConsumer(self) + end + end + if __initWithContext then + __initWithContext(self, props) + end + end + function component:render() return Roact.createElement(Consumer, { ContextMap = contextMap, Render = function(items) - for key, item in pairs(items) do - self.props[key] = item + if items then + for key, item in pairs(items) do + if item.getConsumerItem then + self.props[key] = item:getConsumerItem(self) + else + self.props[key] = item + end + end end return self:__renderWithContext() end, diff --git a/BuiltInPlugins/PlayerEmulator/Packages/Framework/ErrorReporter/Backtrace/BacktraceReport.lua b/BuiltInPlugins/PlayerEmulator/Packages/Framework/ErrorReporter/Backtrace/BacktraceReport.lua index 4beb882892..0c54a85faa 100644 --- a/BuiltInPlugins/PlayerEmulator/Packages/Framework/ErrorReporter/Backtrace/BacktraceReport.lua +++ b/BuiltInPlugins/PlayerEmulator/Packages/Framework/ErrorReporter/Backtrace/BacktraceReport.lua @@ -29,9 +29,14 @@ BacktraceReport.__index = BacktraceReport function BacktraceReport.new() -- Return a basic report that has all the required fields + + -- os.date can return nil if given an invalid input; this input is always valid + -- so it is safe to force it to be `any`. + local date: any = os.date("!*t") + local self = { uuid = HttpService:GenerateGUID(false):lower(), - timestamp = os.time(os.date("!*t")), + timestamp = os.time(date), lang = "lua", langVersion = "Roblox" .. _VERSION, agent = "backtrace-Lua", @@ -70,7 +75,7 @@ function BacktraceReport:addAttributes(newAttributes) end function BacktraceReport:addAnnotations(newAnnotations) - assert(self.IAnnotations(newAnnotions), "Expected newAnnotions to be a table") + assert(self.IAnnotations(newAnnotations), "Expected newAnnotations to be a table") self.annotations = Cryo.Dictionary.join(self.annotations or {}, newAnnotations) end diff --git a/BuiltInPlugins/PlayerEmulator/Packages/Framework/ErrorReporter/Backtrace/BacktraceReporter.spec.lua b/BuiltInPlugins/PlayerEmulator/Packages/Framework/ErrorReporter/Backtrace/BacktraceReporter.spec.lua index 4a6406dcf8..1728ea98b3 100644 --- a/BuiltInPlugins/PlayerEmulator/Packages/Framework/ErrorReporter/Backtrace/BacktraceReporter.spec.lua +++ b/BuiltInPlugins/PlayerEmulator/Packages/Framework/ErrorReporter/Backtrace/BacktraceReporter.spec.lua @@ -8,7 +8,6 @@ return function() local Util = Framework.Util local Cryo = require(Util.Cryo) local deepEqual = require(Util.deepEqual) - local tutils = require(Util.Typecheck.tutils) local Networking = require(Framework.Http).Networking local mockErrorMessage = "index nil" @@ -413,15 +412,10 @@ return function() describe("updateSharedAnnotations()", function() local requestsSent local reporter - local requestBody beforeEach(function() requestsSent = 0 reporter = BacktraceReporter.new({ networking = createMockNetworking(function(requestObj) - local success, result = pcall(HttpService.JSONDecode, HttpService, requestObj.Body) - if success then - requestBody = result - end requestsSent = requestsSent + 1 return requestBodySuccess end), @@ -433,7 +427,6 @@ return function() reporter:stop() reporter = nil requestsSent = 0 - requestBody = nil end) it("should add the same annotations to all error reports", function() @@ -522,10 +515,8 @@ return function() it("should throw if new annotations are ill-formatted", function() local requestsSent = 0 - local requestBody = nil local reporter = BacktraceReporter.new({ networking = createMockNetworking(function(requestObj) - requestBody = HttpService:JSONDecode(requestObj.Body) requestsSent = requestsSent + 1 return requestBodySuccess end), @@ -556,10 +547,7 @@ return function() requestsSent = 0 reporter = BacktraceReporter.new({ networking = createMockNetworking(function(requestObj) - local success, result = pcall(HttpService.JSONDecode, HttpService, requestObj.Body) - if success then - requestBody = result - end + requestBody = requestObj.Body requestsSent = requestsSent + 1 return requestBodySuccess end), @@ -580,14 +568,16 @@ return function() end) it("should send logs if provided generateLogMethod and error report is successful", function() + local logText = "test log text" + generateLogFunc = function() - return "test log text" + return logText end reporter:reportErrorImmediately(mockErrorMessage, mockErrorStack) expect(requestsSent).to.equal(2) -- one for error, one for log - expect(requestBody[2]).to.equal(logText) + expect(requestBody).to.equal(logText) end) it("should not send log if generateLogMethod did not return a string", function() @@ -602,14 +592,15 @@ return function() it("should not send more than 1 log in logIntervalInSeconds provided", function() logInterval = 2 + local logText = "test log text" generateLogFunc = function() - return "test log text" + return logText end reporter:reportErrorImmediately(mockErrorMessage, mockErrorStack) expect(requestsSent).to.equal(2) -- one for error, one for log - expect(requestBody[2]).to.equal(logText) + expect(requestBody).to.equal(logText) reporter:reportErrorImmediately(mockErrorMessage, mockErrorStack) expect(requestsSent).to.equal(3) -- only one more, the error report @@ -617,4 +608,4 @@ return function() reporter:stop() end) end) -end \ No newline at end of file +end diff --git a/BuiltInPlugins/PlayerEmulator/Packages/Framework/ErrorReporter/ErrorQueue.lua b/BuiltInPlugins/PlayerEmulator/Packages/Framework/ErrorReporter/ErrorQueue.lua index 0caa0b57bc..34e7050cd6 100644 --- a/BuiltInPlugins/PlayerEmulator/Packages/Framework/ErrorReporter/ErrorQueue.lua +++ b/BuiltInPlugins/PlayerEmulator/Packages/Framework/ErrorReporter/ErrorQueue.lua @@ -1,7 +1,6 @@ local RunService = game:GetService("RunService") local Framework = script.Parent.Parent -local Cryo = require(Framework.packages.Cryo) -- replace when properly supporting packages local t = require(Framework.Util.Typecheck.t) local DEFAULT_QUEUE_TIME_LIMIT_SECONDS = 30 diff --git a/BuiltInPlugins/PlayerEmulator/Packages/Framework/ErrorReporter/ErrorQueue.spec.lua b/BuiltInPlugins/PlayerEmulator/Packages/Framework/ErrorReporter/ErrorQueue.spec.lua index 0a5836189d..f118538ed8 100644 --- a/BuiltInPlugins/PlayerEmulator/Packages/Framework/ErrorReporter/ErrorQueue.spec.lua +++ b/BuiltInPlugins/PlayerEmulator/Packages/Framework/ErrorReporter/ErrorQueue.spec.lua @@ -3,7 +3,6 @@ return function() local Framework = script.Parent.Parent local deepEqual = require(Framework.Util.deepEqual) - local tutils = require(Framework.Util.Typecheck.tutils) local errorsToAdd = { [1] = { diff --git a/BuiltInPlugins/PlayerEmulator/Packages/Framework/ErrorReporter/StudioPluginErrorReporter.spec.lua b/BuiltInPlugins/PlayerEmulator/Packages/Framework/ErrorReporter/StudioPluginErrorReporter.spec.lua index df525d2cb9..1e0ce2c308 100644 --- a/BuiltInPlugins/PlayerEmulator/Packages/Framework/ErrorReporter/StudioPluginErrorReporter.spec.lua +++ b/BuiltInPlugins/PlayerEmulator/Packages/Framework/ErrorReporter/StudioPluginErrorReporter.spec.lua @@ -2,7 +2,7 @@ return function() local HttpService = game:GetService("HttpService") local Framework = script.Parent.Parent - local mockPlugin = require(Framework.TestHelpers.Services.mockPlugin) + local MockPlugin = require(Framework.TestHelpers.Instances.MockPlugin) local Networking = require(Framework.Http.Networking) local Signal = require(Framework.Util.Signal) local StudioPluginErrorReporter = require(script.Parent.StudioPluginErrorReporter) @@ -20,7 +20,7 @@ return function() it("should configure its attributes from the appropriate services", function() local testingSecurityLevel = 6 - local testPlugin = mockPlugin.new() + local testPlugin = MockPlugin.new() testPlugin.Name = "builtin_Test.rbxm" local testError = { @@ -132,7 +132,7 @@ return function() }, }, }) - + reporter:report("This is an error", "builtin_test.rbxm") reporter:stop() @@ -168,10 +168,10 @@ return function() } local errorSignal = Signal.new() - local pluginA = mockPlugin.new() + local pluginA = MockPlugin.new() pluginA.Name = "builtin_TestA.rbxm" - local pluginB = mockPlugin.new() + local pluginB = MockPlugin.new() pluginB.Name = "builtin_TestB.rbxm" local reporterA = StudioPluginErrorReporter.new({ @@ -188,15 +188,13 @@ return function() networking = networkingImpl, errorSignal = errorSignal, }) - + local errMsg = "This is an error" local errStack = pluginA.Name .. ".Blah.Foo Line 15 - " .. errMsg - local errSource = "" - local errDetails = "" errorSignal:Fire(errMsg, errStack, "", "", 6) reporterA:stop() reporterB:stop() expect(numCalls).to.equal(1) expect(analyticsCalls).to.equal(1) end) -end \ No newline at end of file +end diff --git a/BuiltInPlugins/PlayerEmulator/Packages/Framework/Examples/General.lua b/BuiltInPlugins/PlayerEmulator/Packages/Framework/Examples/General.lua index a4260a11cc..d2ab6a12c7 100644 --- a/BuiltInPlugins/PlayerEmulator/Packages/Framework/Examples/General.lua +++ b/BuiltInPlugins/PlayerEmulator/Packages/Framework/Examples/General.lua @@ -29,23 +29,44 @@ local Plugin = ContextServices.Plugin local UIFolderData = require(Framework.UI.UIFolderData) local StudioUIFolderData = require(Framework.StudioUI.StudioUIFolderData) -local exampleData = { - { - name = "theme", - label = "Theme", - folderPrefix = "examples", - }, - { - name = "localization", - label = "Localization", - folderPrefix = "examples", - }, - { - name = "stylevalue", - label = "StyleValue", - folderPrefix = "examples", - }, -} +local Util = require(Framework.Util) +local FlagsList = Util.Flags.new({ + FFlagRefactorDevFrameworkTheme = {"RefactorDevFrameworkTheme"}, +}) + +local exampleData +if (FlagsList:get("FFlagRefactorDevFrameworkTheme")) then + exampleData = { + { + name = "stylizer", + label = "Stylizer (Theme2)", + folderPrefix = "examples", + }, + { + name = "localization", + label = "Localization", + folderPrefix = "examples", + }, + } +else + exampleData = { + { + name = "theme", + label = "Theme", + folderPrefix = "examples", + }, + { + name = "localization", + label = "Localization", + folderPrefix = "examples", + }, + { + name = "stylevalue", + label = "StyleValue", + folderPrefix = "examples", + }, + } +end local overrideUiExampleName = { ["Container"] = "Container and Decoration", diff --git a/BuiltInPlugins/PlayerEmulator/Packages/Framework/Examples/General/stylizer.lua b/BuiltInPlugins/PlayerEmulator/Packages/Framework/Examples/General/stylizer.lua new file mode 100644 index 0000000000..d031f015ed --- /dev/null +++ b/BuiltInPlugins/PlayerEmulator/Packages/Framework/Examples/General/stylizer.lua @@ -0,0 +1,96 @@ +local Framework = script.Parent.Parent.Parent +local Roact = require(Framework.Parent.Roact) +local ContextServices = require(Framework.ContextServices) +local Plugin = ContextServices.Plugin +local Application = require(script.Application) + +local Dialog = require(Framework.StudioUI).Dialog + +local Cryo = require(Framework.Util.Cryo) + +local FrameworkStyle = require(Framework.Style) +local Colors = FrameworkStyle.Colors +local ui = FrameworkStyle.ComponentSymbols +local StyleKey = FrameworkStyle.StyleKey + +local StudioTheme = require(Framework.Style.Themes.StudioTheme) + +Colors = Cryo.Dictionary.join(Colors, { + Yellow = Color3.new(230, 230, 0), + Red = Color3.fromRGB(255, 0, 0), + Green = Color3.new(0, 255, 0), +}) + + +return function(plugin) + local pluginItem = Plugin.new(plugin) + + local styleRoot = StudioTheme.new() + styleRoot:extend({ + TextColor3 = StyleKey.MainText, + + [ui.Button] = { + BackgroundColor = StyleKey.DialogMainButton, + TextColor3 = StyleKey.Button, + }, + + [ui.Box] = { + BackgroundColor = StyleKey.Mid, + }, + + [ui.Dialog] = { + BackgroundColor = StyleKey.MainBackground, + + ["&Sub"] = { + BackgroundColor = Colors.lighter(Colors.Blue, 0.5), + TextColor3 = Colors.Black; + }, + }, + + Important = { + BackgroundColor = Colors.lighter(Colors.Red, 0.5), + TextColor3 = Colors.Red; + }, + }) + + local DemoApp = Roact.Component:extend("DemoApp") + + function DemoApp:init() + self.state = { + enabled = true, + } + + self.close = function() + self:setState({ + enabled = false, + }) + end + end + + function DemoApp:render() + local enabled = self.state.enabled + if not enabled then + return + end + return ContextServices.provide({pluginItem, styleRoot}, { + Main = Roact.createElement(Dialog, { + Enabled = enabled, + Title = "Stylizer (Theme) Example", + Size = Vector2.new(400, 600), + Resizable = false, + OnClose = self.close, + }, { + App = Roact.createElement(Application) + }) + }) + end + + local element = Roact.createElement(DemoApp) + local handle = Roact.mount(element) + + local function stop() + Roact.unmount(handle) + end + + return stop +end diff --git a/BuiltInPlugins/PlayerEmulator/Packages/Framework/Examples/General/stylizer/Application.lua b/BuiltInPlugins/PlayerEmulator/Packages/Framework/Examples/General/stylizer/Application.lua new file mode 100644 index 0000000000..770d76cd3f --- /dev/null +++ b/BuiltInPlugins/PlayerEmulator/Packages/Framework/Examples/General/stylizer/Application.lua @@ -0,0 +1,67 @@ +--[[ + Application + +]] +local Framework = script.Parent.Parent.Parent.Parent +local ContextServices = require(Framework.ContextServices) +local Roact = require(Framework.Parent.Roact) +local Button = require(script.Parent.Button) +local Dialog = require(script.Parent.Dialog) +local Stylizer = require(Framework.Style).Stylizer + +-- Application +local Application = Roact.PureComponent:extend("Application") + +function Application:init() + self.state = { + className = "Important" + } + + self.changeStyle = function() + local c = "Sub" + if self.state.className == c then + c = "Important" + end + self:setState({ + className = c + }) + end +end + +function Application:render() + return Roact.createElement("Frame", { + Size = UDim2.fromScale(1, 1), + }, { + UIListLayout = Roact.createElement("UIListLayout", { + FillDirection = Enum.FillDirection.Vertical, + HorizontalAlignment = Enum.HorizontalAlignment.Center, + Padding = UDim.new(0, 20), + SortOrder = Enum.SortOrder.LayoutOrder, + VerticalAlignment = Enum.VerticalAlignment.Center, + }), + + Dialog1 = Roact.createElement(Dialog, { + LayoutOrder = 1, + Size = UDim2.fromOffset(300, 150), + Style = self.state.className, + }), + + Dialog2 = Roact.createElement(Dialog, { + LayoutOrder = 2, + Size = UDim2.fromOffset(300, 150), + }), + + Button = Roact.createElement(Button, { + LayoutOrder = 3, + Size = UDim2.fromOffset(300, 60), + Text = "Click to change Style prop for Dialog 2", + OnClick = self.changeStyle, + }) + }) +end + +ContextServices.mapToProps(Application, { + Stylizer = Stylizer +}) + +return Application \ No newline at end of file diff --git a/BuiltInPlugins/PlayerEmulator/Packages/Framework/Examples/General/stylizer/Box.lua b/BuiltInPlugins/PlayerEmulator/Packages/Framework/Examples/General/stylizer/Box.lua new file mode 100644 index 0000000000..e1554580ab --- /dev/null +++ b/BuiltInPlugins/PlayerEmulator/Packages/Framework/Examples/General/stylizer/Box.lua @@ -0,0 +1,38 @@ +--[[ + Box +]] + +local Framework = script.Parent.Parent.Parent.Parent +local ContextServices = require(Framework.ContextServices) +local Roact = require(Framework.Parent.Roact) +local Stylizer = require(Framework.Style).Stylizer + +-- In the component +local Box = Roact.PureComponent:extend("Box") + +function Box:render() + local style = self.props.Stylizer + + return Roact.createElement("Frame", { + Size = self.props.Size, + Position = self.props.Position, + BorderSizePixel = 0, + BackgroundColor3 = style.BackgroundColor, + BackgroundTransparency = 0, + LayoutOrder = self.props.LayoutOrder or 0, + }, { + FrontText = Roact.createElement("TextLabel", { + Text = style:getPathString(), + Size = UDim2.new(1, 0, 1, -60), + BackgroundTransparency = 1, + LayoutOrder = 1, + TextColor3 = style.TextColor3, + }), + }) +end + +ContextServices.mapToProps(Box, { + Stylizer = Stylizer +}) + +return Box \ No newline at end of file diff --git a/BuiltInPlugins/PlayerEmulator/Packages/Framework/Examples/General/stylizer/Button.lua b/BuiltInPlugins/PlayerEmulator/Packages/Framework/Examples/General/stylizer/Button.lua new file mode 100644 index 0000000000..8e7c36daf6 --- /dev/null +++ b/BuiltInPlugins/PlayerEmulator/Packages/Framework/Examples/General/stylizer/Button.lua @@ -0,0 +1,32 @@ +--[[ + Button +]] + +local Framework = script.Parent.Parent.Parent.Parent +local ContextServices = require(Framework.ContextServices) +local Roact = require(Framework.Parent.Roact) +local Stylizer = require(Framework.Style).Stylizer + +-- In the component +local Button = Roact.PureComponent:extend("Button") + +function Button:render() + local style = self.props.Stylizer + + return Roact.createElement("TextButton", { + Size = self.props.Size, + Position = self.props.Position, + BackgroundColor3 = style.BackgroundColor, + BackgroundTransparency = 0, + LayoutOrder = self.props.LayoutOrder or 0, + Text = self.props.Text or style:getPathString(), + TextColor3 = style.TextColor3, + [Roact.Event.Activated] = self.props.OnClick, + }) +end + +ContextServices.mapToProps(Button, { + Stylizer = Stylizer +}) + +return Button \ No newline at end of file diff --git a/BuiltInPlugins/PlayerEmulator/Packages/Framework/Examples/General/stylizer/Dialog.lua b/BuiltInPlugins/PlayerEmulator/Packages/Framework/Examples/General/stylizer/Dialog.lua new file mode 100644 index 0000000000..dcc65b1db3 --- /dev/null +++ b/BuiltInPlugins/PlayerEmulator/Packages/Framework/Examples/General/stylizer/Dialog.lua @@ -0,0 +1,47 @@ +--[[ + Dialog +]] + +local Framework = script.Parent.Parent.Parent.Parent +local ContextServices = require(Framework.ContextServices) +local Roact = require(Framework.Parent.Roact) +local Stylizer = require(Framework.Style).Stylizer +local Box = require(script.Parent.Box) + +-- In the component +local Dialog = Roact.PureComponent:extend("Dialog") + +function Dialog:render() + local style = self.props.Stylizer + + return Roact.createElement("Frame", { + Size = self.props.Size, + Position = self.props.Position, + BackgroundColor3 = style.BackgroundColor, + BackgroundTransparency = 0, + }, { + Layout = Roact.createElement("UIListLayout", { + SortOrder = Enum.SortOrder.LayoutOrder, + HorizontalAlignment = Enum.HorizontalAlignment.Center, + }), + + FrontText = Roact.createElement("TextLabel", { + Text = style:getPathString(), + Size = UDim2.new(1, 0, 1, -60), + BackgroundTransparency = 1, + LayoutOrder = 1, + TextColor3 = style.TextColor3, + }), + + Box = Roact.createElement(Box, { + Position = UDim2.fromOffset(10, 10), + Size = UDim2.new(1, -20, 0, 100), + }) + }) +end + +ContextServices.mapToProps(Dialog, { + Stylizer = Stylizer +}) + +return Dialog diff --git a/BuiltInPlugins/PlayerEmulator/Packages/Framework/Http/Networking.lua b/BuiltInPlugins/PlayerEmulator/Packages/Framework/Http/Networking.lua index 9801436482..3f59fb1f4a 100644 --- a/BuiltInPlugins/PlayerEmulator/Packages/Framework/Http/Networking.lua +++ b/BuiltInPlugins/PlayerEmulator/Packages/Framework/Http/Networking.lua @@ -64,7 +64,6 @@ local HttpResponse = require(script.Parent.HttpResponse) local StatusCodes = require(script.Parent.StatusCodes) local FFlagStudioFixFrameworkJsonParsing = game:DefineFastFlag("StudioFixFrameworkJsonParsing", true) -local FFlagStudioFixFrameworkClientErrorRetries = game:DefineFastFlag("StudioFixFrameworkClientErrorRetries", false) local FFlagStudioFixFrameworkNonIdempotentRetries = game:DefineFastFlag("StudioFixFrameworkNonIdempotentRetries", false) local LOGGING_CHANNELS = { @@ -444,15 +443,13 @@ function Networking:handleRetry(requestPromise, numRetries, disableBackoff) return end - if FFlagStudioFixFrameworkClientErrorRetries then - -- Do not retry on HTTP 4xx (client) errors - if errResponse.responseCode >= 400 and errResponse.responseCode < 500 then - if self:_isLoggingEnabled(LOGGING_CHANNELS.RESPONSES) then - print("4xx error response. Rejecting request.") - end - reject(errResponse) - return + -- Do not retry on HTTP 4xx (client) errors + if errResponse.responseCode >= 400 and errResponse.responseCode < 500 then + if self:_isLoggingEnabled(LOGGING_CHANNELS.RESPONSES) then + print("4xx error response. Rejecting request.") end + reject(errResponse) + return end if FFlagStudioFixFrameworkNonIdempotentRetries then diff --git a/BuiltInPlugins/PlayerEmulator/Packages/Framework/Http/Networking.spec.lua b/BuiltInPlugins/PlayerEmulator/Packages/Framework/Http/Networking.spec.lua index 25379907ae..68395bcab6 100644 --- a/BuiltInPlugins/PlayerEmulator/Packages/Framework/Http/Networking.spec.lua +++ b/BuiltInPlugins/PlayerEmulator/Packages/Framework/Http/Networking.spec.lua @@ -3,7 +3,6 @@ return function() local HttpService = game:GetService("HttpService") local FFlagStudioFixFrameworkJsonParsing = game:GetFastFlag("StudioFixFrameworkJsonParsing") - local FFlagStudioFixFrameworkClientErrorRetries = game:GetFastFlag("StudioFixFrameworkClientErrorRetries") local FFlagStudioFixFrameworkNonIdempotentRetries = game:GetFastFlag("StudioFixFrameworkNonIdempotentRetries") describe("new()", function() @@ -43,7 +42,7 @@ return function() onRequest = function(requestOptions) callCount = callCount + 1 expect(requestOptions.Url).to.equal("https://www.test.com/fakeApi") - + return { Body = "hello world", Success = true, @@ -177,7 +176,7 @@ return function() n:parseJson(httpPromise):andThen(function(json) didResolve = true end, function(err) - expect(string.find(err, "Can't parse JSON")).to.never.equal(nil) + expect((string.find(err, "Can't parse JSON"))).to.never.equal(nil) didError = true end) @@ -340,32 +339,30 @@ return function() expect(callCount).to.equal(2) -- 1 original + 1 retries end) - if FFlagStudioFixFrameworkClientErrorRetries then - it("should not retry on 4xx errors", function() - local callCount = 0 - - local n = Networking.mock({ - onRequest = function(requestOptions) - callCount = callCount + 1 - return { - Body = "{ \"message\":\"foo\" }", - Success = false, - StatusMessage = "Bad Request", - StatusCode = 400, - } - end, - }) + it("should not retry on 4xx errors", function() + local callCount = 0 - local didError = false - local httpPromise = n:get("https://www.example.com") - n:handleRetry(httpPromise, 3, true):catch(function() - didError = true - end) + local n = Networking.mock({ + onRequest = function(requestOptions) + callCount = callCount + 1 + return { + Body = "{ \"message\":\"foo\" }", + Success = false, + StatusMessage = "Bad Request", + StatusCode = 400, + } + end, + }) - expect(didError).to.equal(true) - expect(callCount).to.equal(1) + local didError = false + local httpPromise = n:get("https://www.example.com") + n:handleRetry(httpPromise, 3, true):catch(function() + didError = true end) - end + + expect(didError).to.equal(true) + expect(callCount).to.equal(1) + end) if FFlagStudioFixFrameworkNonIdempotentRetries then it("should not retry POST requests", function() @@ -687,4 +684,4 @@ return function() expect(callCount).to.equal(1) end) end) -end \ No newline at end of file +end diff --git a/BuiltInPlugins/PlayerEmulator/Packages/Framework/RobloxAPI.lua b/BuiltInPlugins/PlayerEmulator/Packages/Framework/RobloxAPI.lua index e8b9d0d690..76d02cdab3 100644 --- a/BuiltInPlugins/PlayerEmulator/Packages/Framework/RobloxAPI.lua +++ b/BuiltInPlugins/PlayerEmulator/Packages/Framework/RobloxAPI.lua @@ -33,10 +33,14 @@ local Url = require(script.Url) local Networking = require(DevFrameworkRoot.Http).Networking local StudioService = game:GetService("StudioService") +local strict = require(DevFrameworkRoot.Util.strict) + +local FFlagDevFrameworkStrictAPITables = game:DefineFastFlag("DevFrameworkStrictAPITables", false) + -- helper functions -- dir : (Instance) a Folder to dig through -- ... : (Variant) any number of arguments to initialize the children with -local function initDirectoryWithArgs(dir, ...) +local function initDirectoryWithArgs(dir, networkingImpl, baseUrl) --[[ When pointed at an Instance, will recurse through the children to initialize all of the required elements with the arguments supplied to this function. @@ -47,11 +51,11 @@ local function initDirectoryWithArgs(dir, ...) local childrenMap = {} for _, child in ipairs(dir:GetChildren()) do if child.ClassName == "Folder" then - childrenMap[child.Name] = initDirectoryWithArgs(child, ...) + childrenMap[child.Name] = initDirectoryWithArgs(child, networkingImpl, baseUrl) elseif child.ClassName == "ModuleScript" then local targetFunction = require(child) - childrenMap[child.Name] = targetFunction(...) + childrenMap[child.Name] = targetFunction(networkingImpl, baseUrl) else warn(string.format("Unexpected object found when constructing children table : %s", child:GetFullName())) @@ -62,7 +66,11 @@ local function initDirectoryWithArgs(dir, ...) warn(string.format("Could not find any children for %s", dir:GetFullName())) end - return childrenMap + if FFlagDevFrameworkStrictAPITables then + return strict(childrenMap) + else + return childrenMap + end end local RobloxAPI = {} @@ -102,6 +110,7 @@ function RobloxAPI.new(props) Develop = initDirectoryWithArgs(script.Develop, networkingImpl, baseUrl), TranslationRoles = initDirectoryWithArgs(script.TranslationRoles, networkingImpl, baseUrl), WWW = initDirectoryWithArgs(script.WWW, networkingImpl, baseUrl), + ToolboxService = initDirectoryWithArgs(script.ToolboxService, networkingImpl, baseUrl), -- add more endpoint domains here } setmetatable(robloxApi, RobloxAPI) @@ -113,4 +122,4 @@ function RobloxAPI:baseURLHasChineseHost() return StudioService:BaseURLHasChineseHost() end -return RobloxAPI \ No newline at end of file +return RobloxAPI diff --git a/BuiltInPlugins/PlayerEmulator/Packages/Framework/RobloxAPI/GameInternationalization/V1/LocalizationTable/Games/assetsGenerationRequest.lua b/BuiltInPlugins/PlayerEmulator/Packages/Framework/RobloxAPI/GameInternationalization/V1/LocalizationTable/Games/assetsGenerationRequest.lua index c1d11b0958..faebe00916 100644 --- a/BuiltInPlugins/PlayerEmulator/Packages/Framework/RobloxAPI/GameInternationalization/V1/LocalizationTable/Games/assetsGenerationRequest.lua +++ b/BuiltInPlugins/PlayerEmulator/Packages/Framework/RobloxAPI/GameInternationalization/V1/LocalizationTable/Games/assetsGenerationRequest.lua @@ -14,10 +14,6 @@ return function(networkingImpl, baseUrl) baseUrl.GAMES_INTERNATIONALIZATION_URL, string.format("v1/localizationtable/games/%d/assets-generation-request", gameId)) - local headers = { - ["Content-Type"] = "application/json" - } - return { getUrl = function() return url @@ -29,4 +25,4 @@ return function(networkingImpl, baseUrl) end, } end -end \ No newline at end of file +end diff --git a/BuiltInPlugins/PlayerEmulator/Packages/Framework/RobloxAPI/ToolboxService/V1/Items/details.lua b/BuiltInPlugins/PlayerEmulator/Packages/Framework/RobloxAPI/ToolboxService/V1/Items/details.lua new file mode 100644 index 0000000000..040d72eb77 --- /dev/null +++ b/BuiltInPlugins/PlayerEmulator/Packages/Framework/RobloxAPI/ToolboxService/V1/Items/details.lua @@ -0,0 +1,51 @@ +--[[ + Returns details of Toolbox items. + Example Request : + { + "items": [ + { + "id": 1, + "itemType": "Asset" + } + ] + } + + Example : + https://apis.roblox.com/toolbox-service/v1/items/details +]] + +local HttpService = game:GetService("HttpService") + +local Framework = script.Parent.Parent.Parent.Parent.Parent +local t = require(Framework.Util.Typecheck.t) + +-- networkingImpl : (Http.Networking) supplied by RobloxAPI.init.lua, a Networking object that makes the network requests +-- baseUrl : (RobloxAPI.Url) supplied by RobloxAPI.init.lua, an object for constructing urls +return function(networkingImpl, baseUrl) + + return function(itemsToRequest) + assert(t.strictInterface({ + items = t.array(t.strictInterface({ + id = t.integer, + itemType = t.string, + })) + })(itemsToRequest), "Request does not match expected format") + + local url = baseUrl.composeUrl(baseUrl.APIS_URL, "toolbox-service/v1/items/details") + + return { + getUrl = function() + return url + end, + + makeRequest = function() + local httpPromise = networkingImpl:post(url, HttpService:JSONEncode(itemsToRequest), { + ["Content-Type"] = "application/json", + }) + -- TODO DEVTOOLS-4914: This will not retry because POST is non-idempotent in REST, but this endpoint is apparently + -- only POST method to facilitate passing a request body, so do we want to retry it? + return networkingImpl:parseJson(networkingImpl:handleRetry(httpPromise)) + end, + } + end +end \ No newline at end of file diff --git a/BuiltInPlugins/PlayerEmulator/Packages/Framework/RobloxAPI/Url.lua b/BuiltInPlugins/PlayerEmulator/Packages/Framework/RobloxAPI/Url.lua index 66f1bc52b1..9319c34e92 100644 --- a/BuiltInPlugins/PlayerEmulator/Packages/Framework/RobloxAPI/Url.lua +++ b/BuiltInPlugins/PlayerEmulator/Packages/Framework/RobloxAPI/Url.lua @@ -63,6 +63,7 @@ function Url.new(baseUrl) BASE_URL = _baseUrl, -- https://www.roblox.com/ API_URL = string.format("https://api.%s", _baseDomain), + APIS_URL = string.format("https://apis.%s", _baseDomain), ASSET_GAME_URL = string.format("https://assetgame.%s", _baseDomain), AUTH_URL = string.format("https://auth.%s", _baseDomain), CATALOG_URL = string.format("https://catalog.%s", _baseDomain), @@ -94,7 +95,7 @@ function Url.composeUrl(base, path, args) assert(type(path) == "string", "Expected 'path' to be a string.") if args then assert(type(args) == "table", "Expected 'args' to be a map.") - assert(type(next(args)) == "string", "Expected 'args' to be map, not an array.") + assert(type((next(args))) == "string", "Expected 'args' to be map, not an array.") end -- append a slash to the end if the base doesn't have one @@ -133,4 +134,4 @@ function Url.composeUrl(base, path, args) return string.format("%s%s%s", base, path, argString) end -return Url \ No newline at end of file +return Url diff --git a/BuiltInPlugins/PlayerEmulator/Packages/Framework/RobloxAPI/Url.spec.lua b/BuiltInPlugins/PlayerEmulator/Packages/Framework/RobloxAPI/Url.spec.lua index b98f32f8f5..bf00b44158 100644 --- a/BuiltInPlugins/PlayerEmulator/Packages/Framework/RobloxAPI/Url.spec.lua +++ b/BuiltInPlugins/PlayerEmulator/Packages/Framework/RobloxAPI/Url.spec.lua @@ -1,7 +1,6 @@ return function() local ContentProvider = game:GetService("ContentProvider") local RobloxAPI = script.Parent - local FrameworkRoot = RobloxAPI.Parent local Url = require(RobloxAPI.Url) it("has not changed the base url for debugging", function() @@ -19,6 +18,7 @@ return function() it("should construct all of the baseUrls based on the current environment", function() local baseUrl = Url.new("https://www.roblox.com") expect(baseUrl.API_URL).to.equal("https://api.roblox.com/") + expect(baseUrl.APIS_URL).to.equal("https://apis.roblox.com/") expect(baseUrl.ASSET_GAME_URL).to.equal("https://assetgame.roblox.com/") expect(baseUrl.AUTH_URL).to.equal("https://auth.roblox.com/") expect(baseUrl.CATALOG_URL).to.equal("https://catalog.roblox.com/") @@ -96,27 +96,27 @@ return function() expect(function() Url.composeUrl(nil, validPath, validArgs) end).to.throw() expect(function() Url.composeUrl(123, validPath, validArgs) end).to.throw() expect(function() Url.composeUrl({}, validPath, validArgs) end).to.throw() - expect(function() Url.composeUrl(newproxy(), validPath, validArgs) end).to.throw() + expect(function() Url.composeUrl(newproxy(true), validPath, validArgs) end).to.throw() expect(function() Url.composeUrl(true, validPath, validArgs) end).to.throw() -- path expect(function() Url.composeUrl(validBase, nil, validArgs) end).to.throw() expect(function() Url.composeUrl(validBase, 123, validArgs) end).to.throw() expect(function() Url.composeUrl(validBase, {}, validArgs) end).to.throw() - expect(function() Url.composeUrl(validBase, newproxy(), validArgs) end).to.throw() + expect(function() Url.composeUrl(validBase, newproxy(true), validArgs) end).to.throw() expect(function() Url.composeUrl(validBase, true, validArgs) end).to.throw() -- args expect(function() Url.composeUrl(validBase, validPath, 123) end).to.throw() expect(function() Url.composeUrl(validBase, validPath, "123") end).to.throw() - expect(function() Url.composeUrl(validBase, validPath, newproxy()) end).to.throw() + expect(function() Url.composeUrl(validBase, validPath, newproxy(true)) end).to.throw() expect(function() Url.composeUrl(validBase, validPath, true) end).to.throw() end) it("should throw errors for invalid argument datatypes", function() -- userdata expect(function() - Url.composeUrl("https://www.test.com/", "a/b/c", { d = newproxy() }) + Url.composeUrl("https://www.test.com/", "a/b/c", { d = newproxy(true) }) end).to.throw() -- maps @@ -130,4 +130,4 @@ return function() end).to.throw() end) end) -end \ No newline at end of file +end diff --git a/BuiltInPlugins/PlayerEmulator/Packages/Framework/RobloxAPI/WWW/Develop/library.lua b/BuiltInPlugins/PlayerEmulator/Packages/Framework/RobloxAPI/WWW/Develop/library.lua index 51e81df9dc..b84bc0f6b7 100644 --- a/BuiltInPlugins/PlayerEmulator/Packages/Framework/RobloxAPI/WWW/Develop/library.lua +++ b/BuiltInPlugins/PlayerEmulator/Packages/Framework/RobloxAPI/WWW/Develop/library.lua @@ -55,7 +55,7 @@ local function assertInEnum(valueName, value, enum) end if enum[value] ~= nil then - error(string.format("Expected %s to be a valid enum value."), 1) + error(string.format("Expected %s to be a valid enum value.", valueName), 1) end end @@ -88,4 +88,4 @@ return function(_, baseUrl) end, } end -end \ No newline at end of file +end diff --git a/BuiltInPlugins/PlayerEmulator/Packages/Framework/RobloxAPI/init.spec.lua b/BuiltInPlugins/PlayerEmulator/Packages/Framework/RobloxAPI/init.spec.lua index ae5295ba66..ab5675b0d0 100644 --- a/BuiltInPlugins/PlayerEmulator/Packages/Framework/RobloxAPI/init.spec.lua +++ b/BuiltInPlugins/PlayerEmulator/Packages/Framework/RobloxAPI/init.spec.lua @@ -19,7 +19,7 @@ return function() expect(api).to.be.ok() - local Url = Url.new("https://www.roblox.com") + local url = Url.new("https://www.roblox.com") api = RobloxAPI.new({ baseUrl = url, }) @@ -33,4 +33,4 @@ return function() expect(api).to.be.ok() end) end) -end \ No newline at end of file +end diff --git a/BuiltInPlugins/PlayerEmulator/Packages/Framework/StudioUI/PluginButton/test.spec.lua b/BuiltInPlugins/PlayerEmulator/Packages/Framework/StudioUI/PluginButton/test.spec.lua index 77e96550f7..1f3f35d834 100644 --- a/BuiltInPlugins/PlayerEmulator/Packages/Framework/StudioUI/PluginButton/test.spec.lua +++ b/BuiltInPlugins/PlayerEmulator/Packages/Framework/StudioUI/PluginButton/test.spec.lua @@ -3,23 +3,10 @@ return function() local Roact = require(Framework.Parent.Roact) local PluginButton = require(script.Parent) - local function mockToolbar() - local toolbar = { - CreateButton = function() - return { - Click = { - Connect = function() - end, - }, - SetActive = function() - end, - Destroy = function() - end, - } - end, - } + local MockPluginToolbar = require(Framework.TestHelpers.Instances.MockPluginToolbar) - return toolbar + local function mockToolbar() + return MockPluginToolbar.new(nil, "") end it("should create and destroy without errors", function() @@ -55,4 +42,4 @@ return function() Roact.unmount(instance) end).to.throw() end) -end \ No newline at end of file +end diff --git a/BuiltInPlugins/PlayerEmulator/Packages/Framework/StudioUI/PluginToolbar.lua b/BuiltInPlugins/PlayerEmulator/Packages/Framework/StudioUI/PluginToolbar.lua index 463234b55a..fb2d51d932 100644 --- a/BuiltInPlugins/PlayerEmulator/Packages/Framework/StudioUI/PluginToolbar.lua +++ b/BuiltInPlugins/PlayerEmulator/Packages/Framework/StudioUI/PluginToolbar.lua @@ -42,6 +42,8 @@ function PluginToolbar:render() if children then return Roact.createFragment(children) end + + return nil end function PluginToolbar:willUnmount() diff --git a/BuiltInPlugins/PlayerEmulator/Packages/Framework/StudioUI/PluginToolbar/test.spec.lua b/BuiltInPlugins/PlayerEmulator/Packages/Framework/StudioUI/PluginToolbar/test.spec.lua index 4deb1abf05..3094a34fad 100644 --- a/BuiltInPlugins/PlayerEmulator/Packages/Framework/StudioUI/PluginToolbar/test.spec.lua +++ b/BuiltInPlugins/PlayerEmulator/Packages/Framework/StudioUI/PluginToolbar/test.spec.lua @@ -4,13 +4,10 @@ return function() local PluginToolbar = require(script.Parent) local ContextServices = require(Framework.ContextServices) - local function mockPlugin() - local plugin = { - CreateToolbar = function() - end, - } + local MockPlugin = require(Framework.TestHelpers.Instances.MockPlugin) - return ContextServices.Plugin.new(plugin) + local function mockPlugin() + return ContextServices.Plugin.new(MockPlugin.new()) end it("should create and destroy without errors", function() @@ -88,4 +85,4 @@ return function() Roact.unmount(instance) end) -end \ No newline at end of file +end diff --git a/BuiltInPlugins/PlayerEmulator/Packages/Framework/StudioUI/SearchBar.lua b/BuiltInPlugins/PlayerEmulator/Packages/Framework/StudioUI/SearchBar.lua index e480f2119e..3712dbed8b 100644 --- a/BuiltInPlugins/PlayerEmulator/Packages/Framework/StudioUI/SearchBar.lua +++ b/BuiltInPlugins/PlayerEmulator/Packages/Framework/StudioUI/SearchBar.lua @@ -1,9 +1,6 @@ --[[ A search bar component with a single line TextInput and button to request a search. - Required Props: - Theme Theme: A Theme ContextItem, which is provided via mapToProps. - Optional Props: number Width: how wide the search bar is in pixels. number ButtonWidth: how wide the search button is in pixels. @@ -17,7 +14,8 @@ boolean ShowSearchButton: Whether to show the search button at the right of the bar (default true). Style Style: The style with which to render this component. StyleModifier StyleModifier: The StyleModifier index into Style. - + Stylizer Stylizer: A Stylizer ContextItem, which is provided via mapToProps. + Theme Theme: A Theme ContextItem, which is provided via mapToProps. Style Values: Style BackgroundStyle: The style with which to render the background. @@ -26,13 +24,17 @@ number TextSize: The font size of the text in this link. Color3 TextColor: The color of the search term text. ]] - local Framework = script.Parent.Parent local Roact = require(Framework.Parent.Roact) local ContextServices = require(Framework.ContextServices) -local Typecheck = require(Framework.Util).Typecheck + local Util = require(Framework.Util) +local Typecheck = Util.Typecheck local StyleModifier = Util.StyleModifier +local FlagsList = Util.Flags.new({ + FFlagRefactorDevFrameworkTheme = {"RefactorDevFrameworkTheme"}, +}) + local UI = require(Framework.UI) local Button = UI.Button local Container = UI.Container @@ -205,7 +207,12 @@ function SearchBar:render() end local theme = props.Theme - local style = theme:getStyle("Framework", self) + local style + if FlagsList:get("FFlagRefactorDevFrameworkTheme") then + style = props.Stylizer + else + style = theme:getStyle("Framework", self) + end local backgroundStyle = style.BackgroundStyle local padding = style.Padding @@ -282,7 +289,8 @@ function SearchBar:render() end ContextServices.mapToProps(SearchBar, { - Theme = ContextServices.Theme, + Stylizer = FlagsList:get("FFlagRefactorDevFrameworkTheme") and ContextServices.Stylizer or nil, + Theme = (not FlagsList:get("FFlagRefactorDevFrameworkTheme")) and ContextServices.Theme or nil, }) return SearchBar diff --git a/BuiltInPlugins/PlayerEmulator/Packages/Framework/StudioUI/SearchBar/style.lua b/BuiltInPlugins/PlayerEmulator/Packages/Framework/StudioUI/SearchBar/style.lua index f77b14a209..6ed85a086b 100644 --- a/BuiltInPlugins/PlayerEmulator/Packages/Framework/StudioUI/SearchBar/style.lua +++ b/BuiltInPlugins/PlayerEmulator/Packages/Framework/StudioUI/SearchBar/style.lua @@ -4,8 +4,14 @@ local UI = require(Framework.UI) local Decoration = UI.Decoration local Util = require(Framework.Util) +local Cryo = Util.Cryo +local deepCopy = Util.deepCopy local Style = Util.Style local StyleModifier = Util.StyleModifier +local StyleKey = require(Framework.Style.StyleKey) +local FlagsList = Util.Flags.new({ + FFlagRefactorDevFrameworkTheme = {"RefactorDevFrameworkTheme"}, +}) local Common = require(Framework.StudioUI.StudioFrameworkStyles.Common) @@ -14,33 +20,51 @@ local RoundBox = require(UIFolderData.RoundBox.style) local FFlagDevFrameworkTextInputContainer = game:GetFastFlag("DevFrameworkTextInputContainer") -return function(theme, getColor) - local common = Common(theme, getColor) - local roundBox = RoundBox(theme, getColor) +local function buttonStyle(image, hoverImage, theme) + local hoverStyle - local function buttonStyle(image, hoverImage) - return Style.new({ - Foreground = Decoration.Image, - ForegroundStyle = { - AnchorPoint = Vector2.new(0.5, 0.5), - Position = UDim2.new(0.5, 0, 0.5, 0), - Color = Color3.fromRGB(184, 184, 184), - Image = image, - Size = UDim2.new(0.6, 0, 0.6, 0), - ScaleType = Enum.ScaleType.Fit - }, - [StyleModifier.Hover] = { - ForegroundStyle = { - Image = hoverImage, - Color = FFlagDevFrameworkTextInputContainer and theme:GetColor("DialogMainButton") or Color3.fromRGB(0, 162, 255) - }, - }, - }) + if FFlagDevFrameworkTextInputContainer then + if FlagsList:get("FFlagRefactorDevFrameworkTheme") then + hoverStyle = StyleKey.DialogMainButton + else + hoverStyle = theme:GetColor("DialogMainButton") + end + else + hoverStyle = Color3.fromRGB(0, 162, 255) end - local Default = Style.extend(common.MainText, common.Border, { - BackgroundColor = common.Background.Color, - BackgroundStyle = roundBox.Default, + local foregroundStyle = { + AnchorPoint = Vector2.new(0.5, 0.5), + Position = UDim2.new(0.5, 0, 0.5, 0), + Color = Color3.fromRGB(184, 184, 184), + Image = image, + Size = UDim2.new(0.6, 0, 0.6, 0), + ScaleType = Enum.ScaleType.Fit + } + + local style = { + Foreground = Decoration.Image, + ForegroundStyle = foregroundStyle, + [StyleModifier.Hover] = { + ForegroundStyle = Cryo.Dictionary.join(foregroundStyle, { + Image = hoverImage, + Color = hoverStyle + }), + }, + } + + if FlagsList:get("FFlagRefactorDevFrameworkTheme") then + return style + else + return Style.new(style) + end +end + +if FlagsList:get("FFlagRefactorDevFrameworkTheme") then + local roundBox = deepCopy(RoundBox) + return { + BackgroundColor = StyleKey.MainBackground, + BackgroundStyle = roundBox, Padding = FFlagDevFrameworkTextInputContainer and { Top = 5, Left = 10, @@ -52,16 +76,47 @@ return function(theme, getColor) Bottom = 0, Right = 10 }, + [StyleModifier.Hover] = { - BackgroundStyle = Style.extend(roundBox.Default, common.BorderHover) + BackgroundStyle = Cryo.Dictionary.join(roundBox, { + BorderColor = StyleKey.DialogMainButton, + }) }, Buttons = { Clear = buttonStyle("rbxasset://textures/StudioSharedUI/clear.png", "rbxasset://textures/StudioSharedUI/clear-hover.png"), Search = buttonStyle("rbxasset://textures/StudioSharedUI/search.png"), }, - }) - - return { - Default = Default, } -end +else + return function(theme, getColor) + local common = Common(theme, getColor) + local roundBox = RoundBox(theme, getColor) + + local Default = Style.extend(common.MainText, common.Border, { + BackgroundColor = common.Background.Color, + BackgroundStyle = roundBox.Default, + Padding = FFlagDevFrameworkTextInputContainer and { + Top = 5, + Left = 10, + Bottom = 5, + Right = 10 + } or { + Top = 0, + Left = 10, + Bottom = 0, + Right = 10 + }, + [StyleModifier.Hover] = { + BackgroundStyle = Style.extend(roundBox.Default, common.BorderHover) + }, + Buttons = { + Clear = buttonStyle("rbxasset://textures/StudioSharedUI/clear.png", "rbxasset://textures/StudioSharedUI/clear-hover.png", theme), + Search = buttonStyle("rbxasset://textures/StudioSharedUI/search.png", nil, theme), + }, + }) + + return { + Default = Default, + } + end +end \ No newline at end of file diff --git a/BuiltInPlugins/PlayerEmulator/Packages/Framework/StudioUI/StudioFrameworkStyles.lua b/BuiltInPlugins/PlayerEmulator/Packages/Framework/StudioUI/StudioFrameworkStyles.lua index 6a660eb6e0..1116523861 100644 --- a/BuiltInPlugins/PlayerEmulator/Packages/Framework/StudioUI/StudioFrameworkStyles.lua +++ b/BuiltInPlugins/PlayerEmulator/Packages/Framework/StudioUI/StudioFrameworkStyles.lua @@ -35,12 +35,21 @@ local UIFolderData = require(Framework.UI.UIFolderData) local StudioUIFolderData = require(Framework.StudioUI.StudioUIFolderData) local Util = require(Framework.Util) +local FlagsList = Util.Flags.new({ + FFlagRefactorDevFrameworkTheme = {"RefactorDevFrameworkTheme"}, +}) + local FrameworkStyles = UI.FrameworkStyles local StyleTable = Util.StyleTable local StudioFrameworkStyles = {} + function StudioFrameworkStyles.new(theme, getColor) + if FlagsList:get("FFlagRefactorDevFrameworkTheme") then + return {} + end + assert(theme, "StudioFrameworkStyles.new expects a 'theme' parameter.") assert(type(getColor) == "function", "StudioFrameworkStyles.new expects a 'getColor' function.") local frameworkStyles = FrameworkStyles.new() diff --git a/BuiltInPlugins/PlayerEmulator/Packages/Framework/StudioUI/StudioFrameworkStyles.spec.lua b/BuiltInPlugins/PlayerEmulator/Packages/Framework/StudioUI/StudioFrameworkStyles.spec.lua index 02a8255021..19347b0066 100644 --- a/BuiltInPlugins/PlayerEmulator/Packages/Framework/StudioUI/StudioFrameworkStyles.spec.lua +++ b/BuiltInPlugins/PlayerEmulator/Packages/Framework/StudioUI/StudioFrameworkStyles.spec.lua @@ -1,6 +1,16 @@ return function() local StudioFrameworkStyles = require(script.Parent.StudioFrameworkStyles) + local Framework = script.Parent.Parent + local Util = require(Framework.Util) + local FlagsList = Util.Flags.new({ + FFlagRefactorDevFrameworkTheme = {"RefactorDevFrameworkTheme"}, + }) + + if FlagsList:get("FFlagRefactorDevFrameworkTheme") then + return + end + describe("new", function() it("should expect a studio theme", function() expect(function() @@ -30,8 +40,8 @@ return function() for _, entry in pairs(styles) do expect(entry.Default).to.be.ok() - expect(next(entry.Default)).to.be.ok() + expect((next(entry.Default))).to.be.ok() end end) end) -end \ No newline at end of file +end diff --git a/BuiltInPlugins/PlayerEmulator/Packages/Framework/StudioUI/StudioFrameworkStyles/Common.lua b/BuiltInPlugins/PlayerEmulator/Packages/Framework/StudioUI/StudioFrameworkStyles/Common.lua index e595b72b01..d45a3c4d44 100644 --- a/BuiltInPlugins/PlayerEmulator/Packages/Framework/StudioUI/StudioFrameworkStyles/Common.lua +++ b/BuiltInPlugins/PlayerEmulator/Packages/Framework/StudioUI/StudioFrameworkStyles/Common.lua @@ -8,51 +8,68 @@ local Util = require(Framework.Util) local Style = Util.Style local StyleValue = Util.StyleValue -return function(theme, getColor) - local MainText = Style.new({ - Font = Enum.Font.SourceSans, - TextSize = 18, - TextColor = theme:GetColor("MainText"), - }) - - local Background = Style.new({ - Color = theme:GetColor("MainBackground"), - }) - - local Border = Style.new({ - BorderColor = theme:GetColor("Border"), - }) - - local BorderHover = Style.new({ - BorderColor = theme:GetColor("DialogMainButton") - }) - - local Scroller = Style.new({ - BackgroundTransparency = 1, - BorderSizePixel = 0, - BackgroundColor3 = theme:GetColor("MainBackground"), - - TopImage = "rbxasset://textures/StudioToolbox/ScrollBarTop.png", - MidImage = "rbxasset://textures/StudioToolbox/ScrollBarMiddle.png", - BottomImage = "rbxasset://textures/StudioToolbox/ScrollBarBottom.png", - - ScrollingEnabled = true, - ScrollingDirection = Enum.ScrollingDirection.Y, - ScrollBarThickness = 8, - ScrollBarImageTransparency = 0.5, - ScrollBarImageColor3 = StyleValue.new("ScrollbarColor", { - Light = Color3.fromRGB(25, 25, 25), - Dark = Color3.fromRGB(204, 204, 204), - }):get(theme.name), - VerticalScrollBarInset = Enum.ScrollBarInset.Always - }) +local StyleKey = require(Framework.Style.StyleKey) +local FlagsList = Util.Flags.new({ + FFlagRefactorDevFrameworkTheme = {"RefactorDevFrameworkTheme"}, +}) + +if FlagsList:get("FFlagRefactorDevFrameworkTheme") then + -- TODO: DEVTOOLS-4908 Refactor everything using MainText so that we can remove Common.lua completely return { - Default = Background, - MainText = MainText, - Background = Background, - Border = Border, - BorderHover = BorderHover, - Scroller = Scroller, + MainText = { + Font = Enum.Font.SourceSans, + TextSize = 18, + TextColor = StyleKey.MainText, + }, } -end +else + return function(theme, getColor) + local MainText = Style.new({ + Font = Enum.Font.SourceSans, + TextSize = 18, + TextColor = theme:GetColor("MainText"), + }) + + local Background = Style.new({ + Color = theme:GetColor("MainBackground"), + }) + + local Border = Style.new({ + BorderColor = theme:GetColor("Border"), + }) + + local BorderHover = Style.new({ + BorderColor = theme:GetColor("DialogMainButton"), + }) + + local Scroller = Style.new({ + BackgroundTransparency = 1, + BorderSizePixel = 0, + BackgroundColor3 = theme:GetColor("MainBackground"), + + TopImage = "rbxasset://textures/StudioToolbox/ScrollBarTop.png", + MidImage = "rbxasset://textures/StudioToolbox/ScrollBarMiddle.png", + BottomImage = "rbxasset://textures/StudioToolbox/ScrollBarBottom.png", + + ScrollingEnabled = true, + ScrollingDirection = Enum.ScrollingDirection.Y, + ScrollBarThickness = 8, + ScrollBarImageTransparency = 0.5, + ScrollBarImageColor3 = StyleValue.new("ScrollbarColor", { + Light = Color3.fromRGB(25, 25, 25), + Dark = Color3.fromRGB(204, 204, 204), + }):get(theme.name), + VerticalScrollBarInset = Enum.ScrollBarInset.Always + }) + + return { + Default = Background, + MainText = MainText, + Background = Background, + Border = Border, + BorderHover = BorderHover, + Scroller = Scroller, + } + end +end \ No newline at end of file diff --git a/BuiltInPlugins/PlayerEmulator/Packages/Framework/StudioUI/StyledDialog.lua b/BuiltInPlugins/PlayerEmulator/Packages/Framework/StudioUI/StyledDialog.lua index 66e1f41e39..407a5e5d7b 100644 --- a/BuiltInPlugins/PlayerEmulator/Packages/Framework/StudioUI/StyledDialog.lua +++ b/BuiltInPlugins/PlayerEmulator/Packages/Framework/StudioUI/StyledDialog.lua @@ -8,10 +8,9 @@ callback OnClose: A function which is fired when the X button attached to the widget. callback OnButtonPressed: A function which is called when any of the buttons - are pressed. - Theme Theme: A Theme ContextItem, which is provided via mapToProps. + are pressed. string Title: The title text displayed at the top of the widget. - + Optional Props: boolean Enabled: Whether the widget is currently visible. Vector2 MinSize: The minimum size of the widget, in pixels. @@ -20,16 +19,19 @@ boolean Resizable: Whether the widget can be resized. Style Style: a predefined kind of dialog to use. Enum.ZIndexBehavior ZIndexBehavior: The ZIndexBehavior of the widget. + Stylizer Stylizer: A Stylizer ContextItem, which is provided via mapToProps. + Theme Theme: A Theme ContextItem, which is provided via mapToProps. - Style Values: Color3 BackgroundColor3: Background color of the dialog. ]] - local Framework = script.Parent.Parent local ContextServices = require(Framework.ContextServices) local Roact = require(Framework.Parent.Roact) local Util = require(Framework.Util) +local FlagsList = Util.Flags.new({ + FFlagRefactorDevFrameworkTheme = {"RefactorDevFrameworkTheme"}, +}) local Button = require(Framework.UI.Button) local Container = require(Framework.UI.Container) @@ -43,7 +45,6 @@ local BUTTON_PADDING = 24 local BUTTON_EDGE_PADDING = 70 local CONTENT_PADDING = 24 - local StyledDialog = Roact.PureComponent:extend("StyledDialog") StyledDialog.defaultProps = { @@ -105,7 +106,12 @@ end function StyledDialog:render() local theme = self.props.Theme - local style = theme:getStyle("Framework", self) + local style + if FlagsList:get("FFlagRefactorDevFrameworkTheme") then + style = self.props.Stylizer + else + style = theme:getStyle("Framework", self) + end local backgroundColor = prioritize(self.props.BackgroundColor3, style.Background) local isEnabled = self.props.Enabled @@ -141,8 +147,10 @@ function StyledDialog:render() }) end ContextServices.mapToProps(StyledDialog, { - Theme = ContextServices.Theme, + Stylizer = FlagsList:get("FFlagRefactorDevFrameworkTheme") and ContextServices.Stylizer or nil, + Theme = (not FlagsList:get("FFlagRefactorDevFrameworkTheme")) and ContextServices.Theme or nil, }) + Typecheck.wrap(StyledDialog, script) return StyledDialog diff --git a/BuiltInPlugins/PlayerEmulator/Packages/Framework/StudioUI/StyledDialog/example.lua b/BuiltInPlugins/PlayerEmulator/Packages/Framework/StudioUI/StyledDialog/example.lua index 0a455e4157..9d869c3757 100644 --- a/BuiltInPlugins/PlayerEmulator/Packages/Framework/StudioUI/StyledDialog/example.lua +++ b/BuiltInPlugins/PlayerEmulator/Packages/Framework/StudioUI/StyledDialog/example.lua @@ -2,8 +2,6 @@ return function(plugin) local Framework = script.Parent.Parent.Parent local Roact = require(Framework.Parent.Roact) local ContextServices = require(Framework.ContextServices) - local Plugin = ContextServices.Plugin - local Theme = ContextServices.Theme local StudioUI = require(Framework.StudioUI) local Dialog = StudioUI.Dialog @@ -16,13 +14,26 @@ return function(plugin) local TestHelpers = require(Framework.TestHelpers) + local Util = require(Framework.Util) + local FlagsList = Util.Flags.new({ + FFlagRefactorDevFrameworkTheme = {"RefactorDevFrameworkTheme"}, + }) + + local FrameworkStyle = Framework.Style + local StudioTheme = require(FrameworkStyle.Themes.StudioTheme) + local pluginItem = ContextServices.Plugin.new(plugin) - local theme = ContextServices.Theme.new(function(theme, getColor) - return { - Framework = StudioFrameworkStyles.new(theme, getColor), - } - end) - + local theme + if FlagsList:get("FFlagRefactorDevFrameworkTheme") then + theme = StudioTheme.new() + else + theme = ContextServices.Theme.new(function(theme, getColor) + return { + Framework = StudioFrameworkStyles.new(theme, getColor), + } + end) + end + local function renderInPopup(isEnabled, onCloseFunc, children) -- This example was too many layers deep. -- This logic might be reusable across multiple examples @@ -83,7 +94,7 @@ return function(plugin) DefaultButton = Roact.createElement(Button, { Size = UDim2.new(1, 0, 0, 30), - LayoutIndex = 1, + LayoutOrder = 1, Style = "Round", Text = "Open Default Dialog", OnClick = function() @@ -116,10 +127,9 @@ return function(plugin) }), }), - AlertButton = Roact.createElement(Button, { Size = UDim2.new(1, 0, 0, 30), - LayoutIndex = 2, + LayoutOrder = 2, Style = "Round", Text = "Open Alert Dialog", OnClick = function() @@ -152,11 +162,10 @@ return function(plugin) }), }), }), - AcceptCancelButton = Roact.createElement(Button, { Size = UDim2.new(1, 0, 0, 30), - LayoutIndex = 3, + LayoutOrder = 3, Style = "Round", Text = "Open AcceptCancel Dialog", OnClick = function() diff --git a/BuiltInPlugins/PlayerEmulator/Packages/Framework/StudioUI/StyledDialog/renderExample.lua b/BuiltInPlugins/PlayerEmulator/Packages/Framework/StudioUI/StyledDialog/renderExample.lua index 56827aefc3..566cceb998 100644 --- a/BuiltInPlugins/PlayerEmulator/Packages/Framework/StudioUI/StyledDialog/renderExample.lua +++ b/BuiltInPlugins/PlayerEmulator/Packages/Framework/StudioUI/StyledDialog/renderExample.lua @@ -5,13 +5,24 @@ local UI = require(Framework.UI) local StudioUI = require(Framework.StudioUI) local Button = UI.Button local StyledDialog = StudioUI.StyledDialog +local StudioTheme = require(Framework.Style.Themes.StudioTheme) + +local Util = require(Framework.Util) +local FlagsList = Util.Flags.new({ + FFlagRefactorDevFrameworkTheme = {"RefactorDevFrameworkTheme"}, +}) local Example = Roact.PureComponent:extend("StyledDialogExample") function Example:render() -- push the same context items into the example local plugin = self.props.Plugin - local theme = self.props.Theme + local theme + if FlagsList:get("FFlagRefactorDevFrameworkTheme") then + theme = StudioTheme.new() + else + theme = self.props.Theme + end return Roact.createElement(Button, { Style = "Round", @@ -49,7 +60,8 @@ function Example:render() end ContextServices.mapToProps(Example, { Plugin = ContextServices.Plugin, - Theme = ContextServices.Theme, + Stylizer = FlagsList:get("FFlagRefactorDevFrameworkTheme") and ContextServices.Stylizer or nil, + Theme = (not FlagsList:get("FFlagRefactorDevFrameworkTheme")) and ContextServices.Theme or nil, }) return Example \ No newline at end of file diff --git a/BuiltInPlugins/PlayerEmulator/Packages/Framework/StudioUI/StyledDialog/style.lua b/BuiltInPlugins/PlayerEmulator/Packages/Framework/StudioUI/StyledDialog/style.lua index 1b92cbbfff..e2f1759519 100644 --- a/BuiltInPlugins/PlayerEmulator/Packages/Framework/StudioUI/StyledDialog/style.lua +++ b/BuiltInPlugins/PlayerEmulator/Packages/Framework/StudioUI/StyledDialog/style.lua @@ -1,36 +1,60 @@ local Framework = script.Parent.Parent.Parent +local StyleKey = require(Framework.Style.StyleKey) + local Util = require(Framework.Util) local Style = Util.Style +local FlagsList = Util.Flags.new({ + FFlagRefactorDevFrameworkTheme = {"RefactorDevFrameworkTheme"}, +}) -local UI = require(Framework.UI) -local Decoration = UI.Decoration - -return function(theme, getColor) - - local Default = Style.new({ - Background = theme:GetColor("MainBackground"), +if FlagsList:get("FFlagRefactorDevFrameworkTheme") then + return { + Background = StyleKey.MainBackground, Modal = false, Resizable = false, - }) - local Alert = Style.extend(Default, { - Buttons = { - { Style = "RoundPrimary" }, -- OK + ["&Alert"] = { + Buttons = { + { Style = "RoundPrimary" }, -- OK + }, + Modal = true, }, - Modal = true, - }) - local AcceptCancel = Style.extend(Default, { - Buttons = { - { Style = "RoundPrimary" }, -- OK - { Style = "Round" }, -- Cancel + ["&AcceptCancel"] = { + Buttons = { + { Style = "RoundPrimary" }, -- OK + { Style = "Round" }, -- Cancel + }, }, - }) - - return { - Default = Default, - Alert = Alert, - AcceptCancel = AcceptCancel, } -end +else + return function(theme, getColor) + + local Default = Style.new({ + Background = theme:GetColor("MainBackground"), + Modal = false, + Resizable = false, + }) + + local Alert = Style.extend(Default, { + Buttons = { + { Style = "RoundPrimary" }, -- OK + }, + Modal = true, + }) + + local AcceptCancel = Style.extend(Default, { + Buttons = { + { Style = "RoundPrimary" }, -- OK + { Style = "Round" }, -- Cancel + }, + }) + + return { + Default = Default, + Alert = Alert, + AcceptCancel = AcceptCancel, + } + end +end \ No newline at end of file diff --git a/BuiltInPlugins/PlayerEmulator/Packages/Framework/StudioUI/StyledDialog/test.spec.lua b/BuiltInPlugins/PlayerEmulator/Packages/Framework/StudioUI/StyledDialog/test.spec.lua index b0775e5956..2f892553ea 100644 --- a/BuiltInPlugins/PlayerEmulator/Packages/Framework/StudioUI/StyledDialog/test.spec.lua +++ b/BuiltInPlugins/PlayerEmulator/Packages/Framework/StudioUI/StyledDialog/test.spec.lua @@ -2,7 +2,6 @@ return function() local Framework = script.Parent.Parent.Parent local Roact = require(Framework.Parent.Roact) local StyledDialog = require(script.Parent) - local ContextServices = require(Framework.ContextServices) local TestHelpers = require(Framework.TestHelpers) it("should create and destroy without errors", function() @@ -60,4 +59,4 @@ return function() Roact.unmount(instance) end) end) -end \ No newline at end of file +end diff --git a/BuiltInPlugins/PlayerEmulator/Packages/Framework/StudioUI/TitledFrame.lua b/BuiltInPlugins/PlayerEmulator/Packages/Framework/StudioUI/TitledFrame.lua index 4f3f1d6e41..00d35130fd 100644 --- a/BuiltInPlugins/PlayerEmulator/Packages/Framework/StudioUI/TitledFrame.lua +++ b/BuiltInPlugins/PlayerEmulator/Packages/Framework/StudioUI/TitledFrame.lua @@ -4,12 +4,13 @@ Required Props: string Title: The title to the left of the content - Theme Theme: A Theme ContextItem, which is provided via mapToProps. Optional Props: number TitleWidth: The pixel pize of the padding between the title and content Enum.FillDirection FillDirection: The direction in which the content is filled. number LayoutOrder: The layoutOrder of this component. + Stylizer Stylizer: A Stylizer ContextItem, which is provided via mapToProps. + Theme Theme: A Theme ContextItem, which is provided via mapToProps. number ZIndex: The render index of this component. ]] local Framework = script.Parent.Parent @@ -18,6 +19,9 @@ local ContextServices = require(Framework.ContextServices) local Util = require(Framework.Util) local Typecheck = Util.Typecheck +local FlagsList = Util.Flags.new({ + FFlagRefactorDevFrameworkTheme = {"RefactorDevFrameworkTheme"}, +}) local FitFrame = require(Framework.Util.FitFrame) local FitFrameVertical = FitFrame.FitFrameVertical @@ -34,7 +38,12 @@ TitledFrame.defaultProps = { function TitledFrame:render() local props = self.props local theme = props.Theme - local style = theme:getStyle("Framework", self) + local style + if FlagsList:get("FFlagRefactorDevFrameworkTheme") then + style = props.Stylizer + else + style = theme:getStyle("Framework", self) + end local font = style.Font local padding = style.Padding @@ -82,7 +91,8 @@ function TitledFrame:render() end ContextServices.mapToProps(TitledFrame, { - Theme = ContextServices.Theme, + Stylizer = FlagsList:get("FFlagRefactorDevFrameworkTheme") and ContextServices.Stylizer or nil, + Theme = (not FlagsList:get("FFlagRefactorDevFrameworkTheme")) and ContextServices.Theme or nil, }) return TitledFrame diff --git a/BuiltInPlugins/PlayerEmulator/Packages/Framework/StudioUI/TitledFrame/renderExample.lua b/BuiltInPlugins/PlayerEmulator/Packages/Framework/StudioUI/TitledFrame/renderExample.lua index fa7eff2313..88acd338aa 100644 --- a/BuiltInPlugins/PlayerEmulator/Packages/Framework/StudioUI/TitledFrame/renderExample.lua +++ b/BuiltInPlugins/PlayerEmulator/Packages/Framework/StudioUI/TitledFrame/renderExample.lua @@ -4,6 +4,11 @@ local Roact = require(Framework.Parent.Roact) local StudioUI = require(Framework.StudioUI) local TitledFrame = StudioUI.TitledFrame +local Util = require(Framework.Util) +local FlagsList = Util.Flags.new({ + FFlagRefactorDevFrameworkTheme = {"RefactorDevFrameworkTheme"}, +}) + local Example = Roact.PureComponent:extend("StyledDialogExample") function Example:render() @@ -22,7 +27,8 @@ end ContextServices.mapToProps(Example, { Plugin = ContextServices.Plugin, - Theme = ContextServices.Theme, + Stylizer = FlagsList:get("FFlagRefactorDevFrameworkTheme") and ContextServices.Stylizer or nil, + Theme = (not FlagsList:get("FFlagRefactorDevFrameworkTheme")) and ContextServices.Theme or nil, }) return Example \ No newline at end of file diff --git a/BuiltInPlugins/PlayerEmulator/Packages/Framework/StudioUI/TitledFrame/style.lua b/BuiltInPlugins/PlayerEmulator/Packages/Framework/StudioUI/TitledFrame/style.lua index ac6b0a646b..bfaa3c423f 100644 --- a/BuiltInPlugins/PlayerEmulator/Packages/Framework/StudioUI/TitledFrame/style.lua +++ b/BuiltInPlugins/PlayerEmulator/Packages/Framework/StudioUI/TitledFrame/style.lua @@ -3,19 +3,32 @@ local Framework = script.Parent.Parent.Parent local StudioFrameworkStyles = Framework.StudioUI.StudioFrameworkStyles local Common = require(StudioFrameworkStyles.Common) +local StyleKey = require(Framework.Style.StyleKey) + local Util = require(Framework.Util) local Style = Util.Style +local FlagsList = Util.Flags.new({ + FFlagRefactorDevFrameworkTheme = {"RefactorDevFrameworkTheme"}, +}) -return function(theme, getColor) - local common = Common(theme, getColor) - - local Default = Style.extend(common.MainText, { - Padding = 10, - TextSize = 22, - TextColor = theme:GetColor("TitlebarText"), - }) - +if FlagsList:get("FFlagRefactorDevFrameworkTheme") then return { - Default = Default, + Padding = 10, + TextSize = 24, + TextColor = StyleKey.TitlebarText, } -end +else + return function(theme, getColor) + local common = Common(theme, getColor) + + local Default = Style.extend(common.MainText, { + Padding = 10, + TextSize = 22, + TextColor = theme:GetColor("TitlebarText"), + }) + + return { + Default = Default, + } + end +end \ No newline at end of file diff --git a/BuiltInPlugins/PlayerEmulator/Packages/Framework/StudioUI/TitledFrame/test.spec.lua b/BuiltInPlugins/PlayerEmulator/Packages/Framework/StudioUI/TitledFrame/test.spec.lua index b769b54068..f013a391bc 100644 --- a/BuiltInPlugins/PlayerEmulator/Packages/Framework/StudioUI/TitledFrame/test.spec.lua +++ b/BuiltInPlugins/PlayerEmulator/Packages/Framework/StudioUI/TitledFrame/test.spec.lua @@ -7,12 +7,24 @@ return function() local TitledFrame = require(script.Parent) local Theme = ContextServices.Theme + local StudioTheme = require(Framework.Style.Themes.StudioTheme) + + local Util = require(Framework.Util) + local FlagsList = Util.Flags.new({ + FFlagRefactorDevFrameworkTheme = {"RefactorDevFrameworkTheme"}, + }) + local function createTestTitledFrame(children, container) - local theme = Theme.new(function() - return { - Framework = FrameworkStyles.new(), - } - end) + local theme + if FlagsList:get("FFlagRefactorDevFrameworkTheme") then + theme = StudioTheme.mock() + else + theme = Theme.new(function() + return { + Framework = FrameworkStyles.new(), + } + end) + end return provide({theme}, { TitledFrame = Roact.createElement(TitledFrame, { Title = "Test", diff --git a/BuiltInPlugins/PlayerEmulator/Packages/Framework/StudioUI/createPluginWidget.lua b/BuiltInPlugins/PlayerEmulator/Packages/Framework/StudioUI/createPluginWidget.lua index c022693c9a..b9dd6d3759 100644 --- a/BuiltInPlugins/PlayerEmulator/Packages/Framework/StudioUI/createPluginWidget.lua +++ b/BuiltInPlugins/PlayerEmulator/Packages/Framework/StudioUI/createPluginWidget.lua @@ -9,8 +9,12 @@ ]] game:DefineFastFlag("FixDevFrameworkDockWidgetRestore", false) +game:DefineFastFlag("DevFrameworkPluginWidgetEnabledEvent2", false) +game:DefineFastFlag("DevFrameworkPluginWidgetUseSiblingZIndex", false) local FFlagFixDevFrameworkDockWidgetRestore = game:GetFastFlag("FixDevFrameworkDockWidgetRestore") +local FFlagDevFrameworkPluginWidgetEnabledEvent2 = game:GetFastFlag("DevFrameworkPluginWidgetEnabledEvent2") +local FFlagDevFrameworkPluginWidgetUseSiblingZIndex = game:GetFastFlag("DevFrameworkPluginWidgetUseSiblingZIndex") local Framework = script.Parent.Parent local Roact = require(Framework.Parent.Roact) @@ -29,7 +33,11 @@ local function createPluginWidget(componentName, createWidgetFunc) local widget = createWidgetFunc(props) widget.Name = title or "" - widget.ZIndexBehavior = props.ZIndexBehavior or Enum.ZIndexBehavior.Global + if FFlagDevFrameworkPluginWidgetUseSiblingZIndex then + widget.ZIndexBehavior = props.ZIndexBehavior or Enum.ZIndexBehavior.Sibling + else + widget.ZIndexBehavior = props.ZIndexBehavior or Enum.ZIndexBehavior.Global + end if widget:IsA("PluginGui") then widget:BindToClose(onClose) @@ -65,6 +73,17 @@ local function createPluginWidget(componentName, createWidgetFunc) end end + if FFlagDevFrameworkPluginWidgetEnabledEvent2 then + -- Connect to enabled changing *after* restore + -- Otherwise users of this will get 2 enabled changes: one from the onRestore, and the same from Roact.Change.Enabled + self.widgetEnabledChangedConnection = widget:GetPropertyChangedSignal("Enabled"):Connect(function() + local callback = self.props[Roact.Change.Enabled] + if callback and self.widget and self.widget.Enabled ~= self.props.Enabled then + callback(self.widget) + end + end) + end + self.focus = Focus.new(widget) self.widget = widget end @@ -105,6 +124,11 @@ local function createPluginWidget(componentName, createWidgetFunc) end function PluginWidget:willUnmount() + if self.widgetEnabledChangedConnection then + self.widgetEnabledChangedConnection:Disconnect() + self.widgetEnabledChangedConnection = nil + end + if self.windowFocusReleasedConnection then self.windowFocusReleasedConnection:Disconnect() self.windowFocusReleasedConnection = nil @@ -117,6 +141,7 @@ local function createPluginWidget(componentName, createWidgetFunc) if self.widget then self.widget:Destroy() + self.widget = nil end end diff --git a/BuiltInPlugins/PlayerEmulator/Packages/Framework/Style.lua b/BuiltInPlugins/PlayerEmulator/Packages/Framework/Style.lua new file mode 100644 index 0000000000..53914d2cc3 --- /dev/null +++ b/BuiltInPlugins/PlayerEmulator/Packages/Framework/Style.lua @@ -0,0 +1,17 @@ +local strict = require(script.Parent.Util.strict) + +return strict({ + Colors = require(script.Colors), + ComponentSymbols = require(script.ComponentSymbols), + createDefaultTheme = require(script.createDefaultTheme), + getRawComponentStyle = require(script.getRawComponentStyle), + StyleKey = require(script.StyleKey), + Stylizer = require(script.Stylizer), + + Themes = strict({ + BaseTheme = require(script.Themes.BaseTheme), + DarkTheme = require(script.Themes.DarkTheme), + LightTheme = require(script.Themes.LightTheme), + StudioTheme = require(script.Themes.StudioTheme), + }) +}) \ No newline at end of file diff --git a/BuiltInPlugins/PlayerEmulator/Packages/Framework/Style/Colors.lua b/BuiltInPlugins/PlayerEmulator/Packages/Framework/Style/Colors.lua new file mode 100644 index 0000000000..6bc14c4ba2 --- /dev/null +++ b/BuiltInPlugins/PlayerEmulator/Packages/Framework/Style/Colors.lua @@ -0,0 +1,22 @@ +return { + Gray_Light = Color3.fromRGB(204, 204, 204), + Gray = Color3.fromRGB(60, 60, 60), + Slate = Color3.fromRGB(46, 46, 46), + Carbon = Color3.fromRGB(34, 34, 34), + Blue = Color3.fromRGB(0, 162, 255), + Blue_Dark = Color3.fromRGB(0, 117, 189), + Blue_Light = Color3.fromRGB(53, 181, 255), + + Red = Color3.fromRGB(255, 0, 0), + White = Color3.fromRGB(255, 255, 255), + Black = Color3.fromRGB(0, 0, 0), + + -- TODO: DEVTOOLS-4869 - If we add lighter/darker functions to Color3, then refactor this to use that. + lighter = function(color3, alpha) + return color3:lerp(Color3.new(1, 1, 1), alpha) + end, + + darker = function(color3, alpha) + return color3:lerp(Color3.new(0, 0, 0), alpha) + end, +} \ No newline at end of file diff --git a/BuiltInPlugins/PlayerEmulator/Packages/Framework/Style/ComponentSymbols.lua b/BuiltInPlugins/PlayerEmulator/Packages/Framework/Style/ComponentSymbols.lua new file mode 100644 index 0000000000..aae77a0281 --- /dev/null +++ b/BuiltInPlugins/PlayerEmulator/Packages/Framework/Style/ComponentSymbols.lua @@ -0,0 +1,26 @@ +--[[ + Returns a table of unique values keyed on name for each component. + This file also creates Symbols for each DevFramework component. + + add(key) + Adds a value into the ComponentSymbols table for use in Stylizer. +]] + +local Framework = script.Parent.Parent +local tableCache = require(Framework.Util.tableCache) + +local UIFolderData = require(Framework.UI.UIFolderData) +local StudioUIFolderData = require(Framework.StudioUI.StudioUIFolderData) + +local ComponentSymbols = tableCache("ComponentSymbols") + +local function createSymbolsForFolder(folder) + for _, component in pairs(folder) do + ComponentSymbols:add(component.name, require) + end +end + +createSymbolsForFolder(UIFolderData) +createSymbolsForFolder(StudioUIFolderData) + +return ComponentSymbols \ No newline at end of file diff --git a/BuiltInPlugins/PlayerEmulator/Packages/Framework/Style/ComponentSymbols.spec.lua b/BuiltInPlugins/PlayerEmulator/Packages/Framework/Style/ComponentSymbols.spec.lua new file mode 100644 index 0000000000..dd14b8bdd9 --- /dev/null +++ b/BuiltInPlugins/PlayerEmulator/Packages/Framework/Style/ComponentSymbols.spec.lua @@ -0,0 +1,43 @@ +return function() + local ComponentSymbols = require(script.Parent.ComponentSymbols) + + local function cleanUpSymbols(symbolName) + for k,_ in pairs(ComponentSymbols) do + if typeof(k) == "table" then + ComponentSymbols[k] = nil + end + end + end + + describe("add", function() + afterEach(function() + cleanUpSymbols() + end) + + it("should coerce to the given name", function() + local symbol = ComponentSymbols:add("foo") + expect((tostring(symbol):find("foo"))).to.be.ok() + end) + + it("should not have duplicate entries", function() + local testA = ComponentSymbols:add("abc") + local testB = ComponentSymbols:add("abc") + expect(testA).to.equal(testB) + end) + + it("should get the same entry for the same lookup", function() + ComponentSymbols:add("abc") + ComponentSymbols:add("abc") + local testA = ComponentSymbols["abc"] + local testB = ComponentSymbols["abc"] + expect(testA).to.equal(testB) + end) + + it("should have ComponentSymbols as a metavalue", function() + ComponentSymbols:add("abc") + local testA = ComponentSymbols["abc"] + local mt = getmetatable(testA) + expect(mt).to.equal(ComponentSymbols) + end) + end) +end diff --git a/BuiltInPlugins/PlayerEmulator/Packages/Framework/Style/StyleKey.lua b/BuiltInPlugins/PlayerEmulator/Packages/Framework/Style/StyleKey.lua new file mode 100644 index 0000000000..3c6f027661 --- /dev/null +++ b/BuiltInPlugins/PlayerEmulator/Packages/Framework/Style/StyleKey.lua @@ -0,0 +1,19 @@ +--[[ + Returns a table of unique values which are given a StyleKey metatable to differentiate it as a StyleKey value. + The Stylizer refers to StyleKey and replaces them with the correct color value. +]] + +local Framework = script.Parent.Parent +local tableCache = require(Framework.Util.tableCache) + +local StyleKey = tableCache("StyleKey") + +setmetatable(StyleKey, { + __index = function(t, name) + local newStyleKey = StyleKey:add(name) + t[name] = newStyleKey + return newStyleKey + end +}) + +return StyleKey \ No newline at end of file diff --git a/BuiltInPlugins/PlayerEmulator/Packages/Framework/Style/StyleKey.spec.lua b/BuiltInPlugins/PlayerEmulator/Packages/Framework/Style/StyleKey.spec.lua new file mode 100644 index 0000000000..442c10e8ef --- /dev/null +++ b/BuiltInPlugins/PlayerEmulator/Packages/Framework/Style/StyleKey.spec.lua @@ -0,0 +1,34 @@ +return function() + local StyleKey = require(script.Parent.StyleKey) + + describe("add", function() + it("should coerce to the given name", function() + local symbol = StyleKey:add("foo") + + expect((tostring(symbol):find("foo"))).to.be.ok() + end) + + it("should create a new entry when there is no current entry found", function() + local testA = StyleKey["abc"] + expect(testA).never.to.equal(nil) + end) + + it("should not have duplicate entries", function() + local testA = StyleKey:add("abc") + local testB = StyleKey:add("abc") + expect(testA).to.equal(testB) + end) + + it("should get the same entry for the same lookup", function() + local testA = StyleKey["abc"] + local testB = StyleKey["abc"] + expect(testA).to.equal(testB) + end) + + it("should have StyleKey as a metavalue", function() + local testA = StyleKey["abc"] + local mt = getmetatable(testA) + expect(mt).to.equal(StyleKey) + end) + end) +end diff --git a/BuiltInPlugins/PlayerEmulator/Packages/Framework/Style/Stylizer.lua b/BuiltInPlugins/PlayerEmulator/Packages/Framework/Style/Stylizer.lua new file mode 100644 index 0000000000..228c4b165a --- /dev/null +++ b/BuiltInPlugins/PlayerEmulator/Packages/Framework/Style/Stylizer.lua @@ -0,0 +1,323 @@ +--!nocheck + +--[[ + Wraps theme styles and update logic into a ContextItem. + + Stylizer.new(t, themeProps) + Constructs a new Stylizer. + + Params: + table initialStyles: + The initial style table. + table themeProps: + A table of properties needed to change the theme. It should contain the following properties: + function getThemeName: Required. Returns the current theme name. + table themeChangedConnection: Optional. Signals when the theme changes. It should + contain a function :Connect(). Requires themeProps to contain `themesList`. + table themesList: Optional. Table of themes to use keyed on theme name. Is required + when themeChangedConnection is set. + + Stylizer:getStyleKeysTable(t) + Gets all StyleKey values in the first layer of the table t. + + Stylizer:convertStyleKeys(t, name, parent, styleKeysTable) + Converts t's (which is a Stylizer table)'s StyleKey values into corresponding values in the styleKeysTable parameter + and calculates each component/style's path string. + + Stylizer:getPathString() + Gets the path of the Stylizer in relation to the overall theme table. + + Example usage: + -- MakeTheme.lua + local Stylizer = ContextHelpers.Stylizer + local BaseTheme = Theme.BaseTheme + + local styleRoot = Stylizer.new(BaseTheme, themeProps) + styleRoot:extend({ + TextBox = { + Default = { + TextColor = Color3.new(0,0,0), + Font = Enum.Font.SourceSans, + }, + }, + }) + return styleRoot + + -- TextBox.lua + local TextBox = Roact.PureComponent:extend("TextBox") + + function TextBox:render() + local props = self.props + local style = props.Stylizer + + return Roact.createElement("TextBox", { + TextColor3 = style.TextColor, + Font = style.Font, + }) + end + + ContextServices.mapToProps(TextBox, { + Stylizer = ContextServices.Stylizer, + }) + + return TextBox +]] + +local Framework = script.Parent.Parent +local Roact = require(Framework.Parent.Roact) +local Cryo = require(Framework.Util.Cryo) + +local ComponentSymbols = require(Framework.Style.ComponentSymbols) +local StyleKey = require(Framework.Style.StyleKey) + +local Provider = require(Framework.ContextServices.Provider) +local ContextItem = require(Framework.ContextServices.ContextItem) + +local Util = require(Framework.Util) +local deepCopy = Util.deepCopy +local Signal = Util.Signal + +local Stylizer = ContextItem:extend("Stylizer") + +local function assertIfNotNil(value, condition, message) + if value ~= nil then + assert(condition, message) + end +end + +function Stylizer:getStyleKeysTable(t) + local styleKeysTable = {} + for k,v in pairs(t) do + if type(k) == "table" and getmetatable(k) == StyleKey and type(v) ~= "table" then + -- NOTE: StyleKeys need to be stringified + styleKeysTable[tostring(k)] = v + end + end + return styleKeysTable +end + +--[[ + Gets the classStyle (in form of ["&ClassName"]) within a component. +--]] +function Stylizer:__getClassStyle(className, currentStyle, componentSymbol) + local result = currentStyle[className] + + if not result then + -- Get class style that is embedded (as an ampersandedClassName) in the component's style table + local ampersandedClassName = "&"..tostring(className) + local componentStyle = currentStyle and currentStyle[componentSymbol] + result = componentStyle and componentStyle[ampersandedClassName] + if result and type(result) == "table" then + local componentStyleWithoutEmbeddedClass = Cryo.Dictionary.join(componentStyle, { + [ampersandedClassName] = Cryo.None, + }) + result = Cryo.Dictionary.join(componentStyleWithoutEmbeddedClass, result) + local mt = getmetatable(componentStyle[ampersandedClassName]) + result = setmetatable(result, mt) + end + end + + assert(result, + ("Stylizer:__getClassStyle copuld not find a Style named '%s' for component `%s`") + :format(className, tostring(componentSymbol)) + ) + + return result +end + +function Stylizer:__recalculateTheme(themeProps) + assert(type(themeProps.themesList) == "table", + "Stylizer.__recalculateTheme expects themeProps to contain a table `themesList` when themeChangedConnection is enabled") + + local themeName = themeProps.getThemeName() + if themeName == self.themeName then + return + end + + self.themeName = themeName + + if themeProps and themeProps.themesList then + self:extend(themeProps.themesList[themeName]) + self.valuesChanged:Fire(self) + end +end + +function Stylizer.new(initialStyles, themeProps) + assert(type(initialStyles) == "table", "Stylizer.new expects initialStyles parameter to be a table") + assert(type(themeProps) == "table", "Stylizer.new expects themeProps parameter to be a table") + assert(type(themeProps.getThemeName) == "function", + "Stylizer.new expects themeProps to contain a function `getThemeName`") + + local styleKeysTable = Stylizer:getStyleKeysTable(initialStyles) + local selfCopy = deepCopy(initialStyles) + selfCopy = Stylizer:convertStyleKeys(selfCopy, nil, nil, styleKeysTable) + + local self = { + __calculatedStyle = selfCopy, + __rawStyle = initialStyles, + valuesChanged = Signal.new(), + themeName = themeProps.getThemeName(), + themeChangedConnection = nil, + } + setmetatable(self, Stylizer) + + if themeProps.themeChangedConnection then + self.themeChangedConnection = themeProps.themeChangedConnection:Connect(function() + self:__recalculateTheme(themeProps) + end) + end + + return self +end + +function Stylizer:extend(...) + for _, v in ipairs({...}) do + local vCopy = deepCopy(v) + local joinedStyles = Cryo.Dictionary.join(self.__rawStyle, vCopy) + local styleKeysTable = self:getStyleKeysTable(joinedStyles) + + self.__rawStyle = deepCopy(joinedStyles) + self.__calculatedStyle = Stylizer:convertStyleKeys(joinedStyles, nil, nil, styleKeysTable) + end + return self +end + +function Stylizer:createProvider(root) + return Roact.createElement(Provider, { + ContextItem = self, + UpdateSignal = self.valuesChanged, + }, {root}) +end + +function Stylizer:destroy() + if self.themeChangedConnection then + self.themeChangedConnection:Disconnect() + end +end + +function Stylizer:convertStyleKeys(t, name, parent, styleKeysTable) + assert(t, "Style:convertStyleKeys expects 't' parameter") + assertIfNotNil(parent, (typeof(parent) == "table"), ("Style:convertStyleKeys expects 'parent' parameter to be a table, but got a %s"):format(typeof(table))) + assertIfNotNil(styleKeysTable, (typeof(styleKeysTable) == "table"), ("Style:convertStyleKeys expects 'styleKeysTable' parameter to be a table, but got a %s"):format(typeof(styleKeysTable))) + + local mt + if parent then + mt = { + __index = parent, + __styleName = name or '[unnamed style]', + } + else + mt = { + __index = Stylizer, + __styleName = name or "[Root Style]", + } + end + + -- Link input and parent styles + local this = setmetatable(t, mt) + + -- Process properties and create nested styles + for propName, v in pairs(t) do + local override + if type(v) == "table" then + if getmetatable(v) == StyleKey then + -- NOTE: StyleKeys need to be stringified + override = (parent and parent[v]) or (styleKeysTable and styleKeysTable[tostring(v)]) + or error(("StyleKey %s defines no value @ key %s"):format(v.name, propName)) + + elseif type(v.render) ~= "function" then + override = self:convertStyleKeys(v, propName, this, styleKeysTable) + end + + elseif type(v) == "function" then + local generated = v(this) or {} + if type(generated) == "table" then + override = self:convertStyleKeys(generated, propName, this, styleKeysTable) + end + end + + if override then + rawset(this, propName, override) + end + end + + return this +end + +function Stylizer:getPathString() + local path + local m = getmetatable(self) + while m and m.__styleName do + if path then + path = tostring(m.__styleName) .. "-->" .. tostring(path) + else + path = m.__styleName + end + m = getmetatable(m.__index) + end + return path or "" +end + +function Stylizer:getConsumerItem(target) + local style = target.props.Style + local currentStyle = self.__calculatedStyle + local componentSymbol = ComponentSymbols[target.__componentName] + + if not currentStyle then + assert(false, "Style:getConsumerItem() is unable to find the Style in _context of", target.__componentName) + return self + end + + local result + if style then + if type(style) == "table" then + result = setmetatable(style, { + __index = Stylizer + }) + + elseif type(style) == "string" then + result = self:__getClassStyle(style, currentStyle, componentSymbol) + end + end + + result = result or currentStyle[componentSymbol] or currentStyle or self + + local modifier = target.props.StyleModifier or target.state.StyleModifier + local modStyle = result[modifier] + if modifier and modStyle then + setmetatable(modStyle, { + __index = result, + }) + + for k, v in pairs(modStyle) do + if type(v) == "table" and result[k] then + setmetatable(v, { + __index = result[k], + }) + end + end + + result = modStyle + end + + if self.getUILibraryTheme then + result.getUILibraryTheme = self.getUILibraryTheme + end + + return result +end + +function Stylizer.mock(t, themeProps, callback) + local self = Stylizer.new(t, themeProps) + + if themeProps.themeChangedConnection then + self.themeChangedConnection:Disconnect() + self.themeChangedConnection = themeProps.themeChangedConnection:Connect(function() + callback() + self:__recalculateTheme(themeProps) + end) + end + + return self +end +return Stylizer diff --git a/BuiltInPlugins/PlayerEmulator/Packages/Framework/Style/Stylizer.spec.lua b/BuiltInPlugins/PlayerEmulator/Packages/Framework/Style/Stylizer.spec.lua new file mode 100644 index 0000000000..999039a5b1 --- /dev/null +++ b/BuiltInPlugins/PlayerEmulator/Packages/Framework/Style/Stylizer.spec.lua @@ -0,0 +1,539 @@ +return function() + local Stylizer = require(script.Parent.Stylizer) + local Framework = script.Parent.Parent + local Roact = require(Framework.Parent.Roact) + local provide = require(Framework.ContextServices.provide) + local mapToProps = require(Framework.ContextServices.mapToProps) + + local FrameworkStyle = require(Framework.Style) + local ui = FrameworkStyle.ComponentSymbols + local StyleKey = require(script.Parent.StyleKey) + + local Util = require(Framework.Util) + local Signal = Util.Signal + local StyleModifier = Util.StyleModifier + + local testSymbols = {} + + local function createTestThemedComponent(render) + local TestThemedComponent = Roact.PureComponent:extend("TestThemedComponent") + + function TestThemedComponent:render() + local style = self.props.Theme + if render then + render(style) + end + end + + mapToProps(TestThemedComponent, { + Theme = Stylizer, + }) + + return TestThemedComponent + end + + local function addSymbol(symbolName) + local symbol = ui:add(symbolName) + table.insert(testSymbols, symbol) + return symbol + end + + local function cleanUpSymbols() + for _,v in pairs(testSymbols) do + ui[v] = nil + end + end + + local function createDefaultStylizer(initialTable) + initialTable = initialTable or {} + return Stylizer.new(initialTable, { + getThemeName = function() end, + }) + end + + describe("new", function() + afterEach(function() + cleanUpSymbols() + end) + + it("should expect an initial table as a parameter", function() + expect(function() + Stylizer.new(nil, {}) + end).to.throw() + end) + + it("should expect an inial themeProps as a parameter", function() + expect(function() + Stylizer.new({}, nil) + end).to.throw() + end) + + it("should return a new Stylizer", function() + local stylizer = createDefaultStylizer() + expect(stylizer).to.be.ok() + stylizer:destroy() + end) + end) + + describe("extend", function() + it("should return a new Stylizer", function() + local stylizer = createDefaultStylizer() + stylizer = stylizer:extend({}) + expect(stylizer).to.be.ok() + stylizer:destroy() + end) + + it("should merge the table with existing values", function() + local oldValue = "old" + local addedValue = "add" + local overrideValue = "world" + local stylizer = createDefaultStylizer({ + old = oldValue, + override = "old", + }) + stylizer = stylizer:extend({ + added = addedValue, + override = overrideValue, + }) + + local result = stylizer.__calculatedStyle + expect(result.old).to.equal(oldValue) + expect(result.override).to.equal(overrideValue) + expect(result.added).to.equal(addedValue) + stylizer:destroy() + end) + end) + + it("should be providable as a ContextItem", function() + local stylizer = createDefaultStylizer() + local element = provide({stylizer}, { + Frame = Roact.createElement("Frame"), + }) + local instance = Roact.mount(element) + Roact.unmount(instance) + stylizer:destroy() + end) + + describe("destroy", function() + it("should disconnect from the theme changed signal", function() + local themeChanged = Signal.new() + local calls = 0 + local callback = function(theme, getColors) + calls = calls + 1 + return {} + end + local stylizer = Stylizer.mock({}, { + getThemeName = function() return "Light" end, + themesList = { ["Light"] = {} }, + themeChangedConnection = themeChanged, + }, callback) + stylizer:destroy() + themeChanged:Fire() + expect(calls).to.equal(0) + end) + end) + + describe("getStyleKeysTable", function() + it("should get a list of all StyleKey values in the first layer of the passed in table", function() + local styleKeyValue = "world" + local styleKeysTable = { + [StyleKey.hello] = styleKeyValue, + notAStyleKey = "no", + } + local result = Stylizer:getStyleKeysTable(styleKeysTable) + expect(result.notAStyleKey).to.never.be.ok() + expect(result[tostring(StyleKey.hello)]).to.equal(styleKeyValue) + end) + end) + + describe("convertStyleKeys", function() + it("should replace all StyleKey values with the correct value", function() + local redValue = "Mario" + local styleKeysTable = { + [StyleKey.Red] = redValue, + } + styleKeysTable = Stylizer:getStyleKeysTable(styleKeysTable) + local tableToConvert = { + itsAme = StyleKey.Red, + its = { + aMe = StyleKey.Red, + } + } + local result = Stylizer:convertStyleKeys(tableToConvert, nil, nil, styleKeysTable) + expect(result.itsAme).to.equal(redValue) + expect(result.its.aMe).to.equal(redValue) + end) + end) + + describe("getPathString", function() + afterEach(function() + cleanUpSymbols() + end) + + it("should get the correct path value for root styles", function() + local receivedValue + local themedComponent = createTestThemedComponent(function(style) + receivedValue = style:getPathString() + end) + addSymbol("TestThemedComponent") + local theme = createDefaultStylizer({}) + local element = provide({theme}, { + ThemedComponent = Roact.createElement(themedComponent, {}), + }) + local instance = Roact.mount(element) + Roact.unmount(instance) + expect(receivedValue).to.be.ok() + expect(receivedValue).to.equal("[Root Style]") + theme:destroy() + end) + + it("should get the correct path value for a component", function() + local receivedValue + local themedComponent = createTestThemedComponent(function(style) + receivedValue = style:getPathString() + end) + addSymbol("TestThemedComponent") + local theme = createDefaultStylizer({ + [ui.TestThemedComponent] = { + hello = "world", + }, + }) + local element = provide({theme}, { + ThemedComponent = Roact.createElement(themedComponent), + }) + local instance = Roact.mount(element) + Roact.unmount(instance) + expect(receivedValue).to.be.ok() + expect(receivedValue).to.equal("[Root Style]-->ComponentSymbols(TestThemedComponent)") + theme:destroy() + end) + + it("should get the correct path value for a Style in the Root", function() + local receivedValue + local themedComponent = createTestThemedComponent(function(style) + receivedValue = style:getPathString() + end) + addSymbol("TestThemedComponent") + local theme = createDefaultStylizer({ + [ui.TestThemedComponent] = { + hello = "world", + }, + override = { + test = "test", + } + }) + local element = provide({theme}, { + ThemedComponent = Roact.createElement(themedComponent, { + Style = "override", + }), + }) + local instance = Roact.mount(element) + Roact.unmount(instance) + expect(receivedValue).to.be.ok() + expect(receivedValue).to.equal("[Root Style]-->override") + theme:destroy() + end) + + it("should get the correct path value for an ampersand Style", function() + local receivedValue + local themedComponent = createTestThemedComponent(function(style) + receivedValue = style:getPathString() + end) + addSymbol("TestThemedComponent") + local theme = createDefaultStylizer({ + [ui.TestThemedComponent] = { + hello = "world", + ["&override"] = { + test = "test", + } + }, + }) + local element = provide({theme}, { + ThemedComponent = Roact.createElement(themedComponent, { + Style = "override", + }), + }) + local instance = Roact.mount(element) + Roact.unmount(instance) + expect(receivedValue).to.be.ok() + expect(receivedValue).to.equal("[Root Style]-->ComponentSymbols(TestThemedComponent)-->&override") + theme:destroy() + end) + end) + + describe("getConsumerItem", function() + afterEach(function() + cleanUpSymbols() + end) + + it("should get the corresponding component's style values", function() + local receivedValue + local themedComponent = createTestThemedComponent(function(style) + receivedValue = style + end) + local componentValue = "world" + addSymbol("TestThemedComponent") + addSymbol("doNotGet") + local theme = createDefaultStylizer({ + [ui.TestThemedComponent] = { + hello = componentValue, + }, + [ui.doNotGet] = { + ohNo = "no", + }, + }) + local element = provide({theme}, { + ThemedComponent = Roact.createElement(themedComponent), + }) + local instance = Roact.mount(element) + Roact.unmount(instance) + expect(receivedValue).to.be.ok() + expect(receivedValue.hello).to.equal(componentValue) + expect(receivedValue.ohNo).to.never.be.ok() + theme:destroy() + end) + + it("should replace StyleKey values with the correct value", function() + local receivedValue + local themedComponent = createTestThemedComponent(function(style) + receivedValue = style + end) + local blueValue = Color3.new(1, 1, 1) + local redValue = Color3.new(1, 0, 0) + addSymbol("TestThemedComponent") + local theme = createDefaultStylizer({ + [StyleKey.Blue] = blueValue, + + [ui.TestThemedComponent] = { + theSkyIs = StyleKey.Blue, + ["&override"] = { + theSkyIs = redValue, + } + } + }) + local element = provide({theme}, { + ThemedComponent = Roact.createElement(themedComponent, {}), + }) + local instance = Roact.mount(element) + Roact.unmount(instance) + expect(receivedValue.theSkyIs).to.equal(blueValue) + theme:destroy() + end) + + it("should be overridden with the correct ampersand when a string is passed into Style prop", function() + local receivedValue + local themedComponent = createTestThemedComponent(function(style) + receivedValue = style + end) + local blueValue = Color3.new(1, 1, 1) + local redValue = Color3.new(1, 0, 0) + addSymbol("TestThemedComponent") + local theme = createDefaultStylizer({ + [ui.TestThemedComponent] = { + theSkyIs = blueValue, + ["&override"] = { + theSkyIs = redValue, + } + } + }) + local element = provide({theme}, { + ThemedComponent = Roact.createElement(themedComponent, { + Style = "override", + }), + }) + local instance = Roact.mount(element) + Roact.unmount(instance) + expect(receivedValue.theSkyIs).to.equal(redValue) + theme:destroy() + end) + + it("should get the correct value for a Style in the Root", function() + local receivedValue + local themedComponent = createTestThemedComponent(function(style) + receivedValue = style + end) + addSymbol("TestThemedComponent") + local blueValue = Color3.new(1, 1, 1) + local theme = createDefaultStylizer({ + [ui.TestThemedComponent] = { + hello = "world", + }, + override = { + hello = blueValue, + } + }) + local element = provide({theme}, { + ThemedComponent = Roact.createElement(themedComponent, { + Style = "override", + }), + }) + local instance = Roact.mount(element) + Roact.unmount(instance) + expect(receivedValue).to.be.ok() + expect(receivedValue.hello).to.equal(blueValue) + theme:destroy() + end) + + it("should be overridden with the table passed into Style prop", function() + local receivedValue + local themedComponent = createTestThemedComponent(function(style) + receivedValue = style + end) + local blueValue = Color3.new(1, 1, 1) + local redValue = Color3.new(1, 0, 0) + local blackValue = Color3.new(0, 0, 0) + addSymbol("TestThemedComponent") + local theme = createDefaultStylizer({ + [ui.TestThemedComponent] = { + theSkyIs = blueValue, + ["&override"] = { + theSkyIs = redValue, + } + } + }) + local element = provide({theme}, { + ThemedComponent = Roact.createElement(themedComponent, { + Style = { + theSkyIs = blackValue + }, + }), + }) + local instance = Roact.mount(element) + Roact.unmount(instance) + expect(receivedValue.theSkyIs).to.equal(blackValue) + theme:destroy() + end) + + it("should be called each time the theme updates", function() + local themeChanged = Signal.new() + local calls = 0 + local callback = function(theme, getColors) + calls = calls + 1 + return {} + end + local stylizer = Stylizer.mock({}, { + getThemeName = function() return "Light" end, + themesList = { ["Light"] = {} }, + themeChangedConnection = themeChanged, + }, callback) + themeChanged:Fire() + expect(calls).to.equal(1) + themeChanged:Fire() + expect(calls).to.equal(2) + + stylizer:destroy() + end) + + describe("using StyleModifier prop", function() + afterEach(function() + cleanUpSymbols() + end) + + it("should take values from the current StyleModifier", function() + local receivedValue + local themedComponent = createTestThemedComponent(function(style) + receivedValue = style + end) + addSymbol("TestThemedComponent") + local theme = createDefaultStylizer({ + [ui.TestThemedComponent] = { + Value = "Test", + [StyleModifier.Hover] = { + Value = "HoverTest", + }, + } + }) + local element = provide({theme}, { + ThemedComponent = Roact.createElement(themedComponent, { + StyleModifier = StyleModifier.Hover, + }), + }) + local instance = Roact.mount(element) + Roact.unmount(instance) + expect(receivedValue.Value).to.equal("HoverTest") + theme:destroy() + end) + + it("should take values from ampersand Style and StyleModifier combined", function() + local receivedValue + local themedComponent = createTestThemedComponent(function(style) + receivedValue = style + end) + addSymbol("TestThemedComponent") + local theme = createDefaultStylizer({ + [ui.TestThemedComponent] = { + Value = "Test", + ["&Styled"] = { + [StyleModifier.Hover] = { + Value = "HoverTest", + }, + } + }, + }) + local element = provide({theme}, { + ThemedComponent = Roact.createElement(themedComponent, { + Style = "Styled", + StyleModifier = StyleModifier.Hover, + }), + }) + local instance = Roact.mount(element) + Roact.unmount(instance) + expect(receivedValue.Value).to.equal("HoverTest") + theme:destroy() + end) + + it("should take values from passed-in Style and StyleModifier combined", function() + local receivedValue + local themedComponent = createTestThemedComponent(function(style) + receivedValue = style + end) + addSymbol("TestThemedComponent") + local theme = createDefaultStylizer({ + [ui.TestThemedComponent] = { + Value = "Test", + }, + }) + local element = provide({theme}, { + ThemedComponent = Roact.createElement(themedComponent, { + Style = { + [StyleModifier.Hover] = { + Value = "HoverTest", + }, + }, + StyleModifier = StyleModifier.Hover, + }), + }) + local instance = Roact.mount(element) + Roact.unmount(instance) + expect(receivedValue.Value).to.equal("HoverTest") + theme:destroy() + end) + + it("should fall back to the style if no modified value is found", function() + local receivedValue + local themedComponent = createTestThemedComponent(function(style) + receivedValue = style + end) + local theme = createDefaultStylizer({ + [ui.TestThemedComponent] = { + Value = "Test", + OtherValue = "Test", + [StyleModifier.Hover] = { + Value = "HoverTest", + }, + }, + }) + local element = provide({theme}, { + ThemedComponent = Roact.createElement(themedComponent, { + StyleModifier = StyleModifier.Hover, + }), + }) + local instance = Roact.mount(element) + Roact.unmount(instance) + expect(receivedValue.Value).to.equal("HoverTest") + expect(receivedValue.OtherValue).to.equal("Test") + theme:destroy() + end) + end) + end) +end \ No newline at end of file diff --git a/BuiltInPlugins/PlayerEmulator/Packages/Framework/Style/Themes/BaseTheme.lua b/BuiltInPlugins/PlayerEmulator/Packages/Framework/Style/Themes/BaseTheme.lua new file mode 100644 index 0000000000..f94e37f1ff --- /dev/null +++ b/BuiltInPlugins/PlayerEmulator/Packages/Framework/Style/Themes/BaseTheme.lua @@ -0,0 +1,48 @@ +--[[ + Combines all style.lua tables in each component. + Note that component styles expect defined StyleKeys when used in + Stylizer. Combine this file with DarkTheme, LightTheme, or your own + theme color using Stylizer.new(DarkTheme) and Stylizer:extend(BaseTheme). +]] + +local Framework = script.Parent.Parent.Parent +local StudioUIFolderData = require(Framework.StudioUI.StudioUIFolderData) +local UIFolderData = require(Framework.UI.UIFolderData) + +local ComponentSymbols = require(Framework.Style.ComponentSymbols) +local StyleKey = require(Framework.Style.StyleKey) + +local StudioFrameworkStyles = Framework.StudioUI.StudioFrameworkStyles +local Common = require(StudioFrameworkStyles.Common) + +local Util = Framework.Util +local Cryo = require(Util.Cryo) +local FlagsList = require(Util.Flags).new({ + FFlagRefactorDevFrameworkTheme = {"RefactorDevFrameworkTheme"}, +}) + + +if (not FlagsList:get("FFlagRefactorDevFrameworkTheme")) then + return {} +end + +local styles = Cryo.Dictionary.join(Common.MainText, { + -- Common styles + Color = StyleKey.MainBackground, + BorderColor = StyleKey.Border, +}) + +local function createComponentStyles(folderData) + for _,folder in pairs(folderData) do + if folder.style then + assert(ComponentSymbols[folder.name] ~= nil, ("No Symbol was found for the component %s"):format(folder.name)) + local componentStyleFile = require(folder.style) + styles[ComponentSymbols[folder.name]] = componentStyleFile + end + end +end + +createComponentStyles(UIFolderData) +createComponentStyles(StudioUIFolderData) + +return styles \ No newline at end of file diff --git a/BuiltInPlugins/PlayerEmulator/Packages/Framework/Style/Themes/DarkTheme.lua b/BuiltInPlugins/PlayerEmulator/Packages/Framework/Style/Themes/DarkTheme.lua new file mode 100644 index 0000000000..81d39841bd --- /dev/null +++ b/BuiltInPlugins/PlayerEmulator/Packages/Framework/Style/Themes/DarkTheme.lua @@ -0,0 +1,54 @@ +local Framework = script.Parent.Parent.Parent +local Colors = require(Framework.Style.Colors) +local StyleKey = require(Framework.Style.StyleKey) + +return { + [StyleKey.Border] = Colors.Carbon, + [StyleKey.BrightText] = Colors.White, + [StyleKey.Button] = Colors.Gray, + [StyleKey.ButtonText] = Colors.Gray_Light, + [StyleKey.ButtonHover] = Colors.Gray, + [StyleKey.ButtonDisabled] = Colors.lighter(Colors.Black, 0.26), + [StyleKey.ButtonPressed] = Colors.lighter(Colors.Black, 0.16), + + [StyleKey.CategoryItem] = Color3.fromRGB(53, 53, 53), + + [StyleKey.DialogMainButton] = Colors.Blue, + [StyleKey.DialogMainButtonDisabled] = Colors.Blue, + [StyleKey.DialogMainButtonHover] = Colors.Blue, + [StyleKey.DialogMainButtonSelected] = Colors.Blue_Dark, + [StyleKey.DialogMainButtonText] = Colors.White, + [StyleKey.DialogMainButtonTextDisabled] = Color3.fromRGB(102, 102, 102), + [StyleKey.DimmedText] = Colors.lighter(Colors.Black, 0.4), + + [StyleKey.ErrorText] = Color3.fromRGB(255, 68, 68), + + [StyleKey.InputFieldBackground] = Color3.fromRGB(37, 37, 37), + + [StyleKey.LinkText] = Color3.fromRGB(60, 180, 255), + + [StyleKey.MainBackground] = Colors.Slate, + [StyleKey.MainButton] = Colors.Blue, + [StyleKey.MainText] = Colors.Gray_Light, + [StyleKey.MainTextDisabled] = Color3.fromRGB(85, 85, 85), + [StyleKey.Mid] = Color3.fromRGB(34, 34, 34), + + [StyleKey.RibbonTab] = Color3.fromRGB(37, 37, 37), + + [StyleKey.ScrollBarBackground] = Color3.fromRGB(41, 41, 41), + [StyleKey.ScrollBar] = Colors.lighter(Colors.Black, 0.22), + [StyleKey.SliderKnobColor] = Color3.fromRGB(85, 85, 85), + [StyleKey.SliderKnobImage] = "rbxasset://textures/DeveloperFramework/slider_knob.png", + [StyleKey.SliderBackground] = Color3.fromRGB(37, 37, 37), + [StyleKey.SubText] = Color3.fromRGB(170, 170, 170), + + [StyleKey.TitlebarText] = Color3.fromRGB(204, 204, 204), + [StyleKey.ToggleOnImage] = "rbxasset://textures/RoactStudioWidgets/toggle_on_dark.png", + [StyleKey.ToggleOffImage] = "rbxasset://textures/RoactStudioWidgets/toggle_off_dark.png", + [StyleKey.ToggleDisabledImage] = "rbxasset://textures/RoactStudioWidgets/toggle_disable_dark.png", + + [StyleKey.WarningText] = Color3.fromRGB(255, 141, 60), + + Font = Enum.Font.SourceSans, + TextSize = 18, +} \ No newline at end of file diff --git a/BuiltInPlugins/PlayerEmulator/Packages/Framework/Style/Themes/LightTheme.lua b/BuiltInPlugins/PlayerEmulator/Packages/Framework/Style/Themes/LightTheme.lua new file mode 100644 index 0000000000..93d7586ba8 --- /dev/null +++ b/BuiltInPlugins/PlayerEmulator/Packages/Framework/Style/Themes/LightTheme.lua @@ -0,0 +1,54 @@ +local Framework = script.Parent.Parent.Parent +local StyleKey = require(Framework.Style.StyleKey) +local Colors = require(Framework.Style.Colors) + +return { + [StyleKey.Border] = Color3.fromRGB(182, 182, 182), + [StyleKey.BrightText] = Colors.Black, + [StyleKey.Button] = Colors.White, + [StyleKey.ButtonText] = Colors.Black, + [StyleKey.ButtonHover] = Color3.fromRGB(228, 238, 254), + [StyleKey.ButtonDisabled] = Colors.White, + [StyleKey.ButtonPressed] = Color3.fromRGB(219, 219, 219), + + [StyleKey.CategoryItem] = Color3.fromRGB(233, 233, 233), + + [StyleKey.DialogMainButton] = Colors.Blue, + [StyleKey.DialogMainButtonDisabled] = Color3.fromRGB(153, 218, 255), + [StyleKey.DialogMainButtonHover] = Colors.Blue_Light, + [StyleKey.DialogMainButtonSelected] = Colors.Blue_Dark, + [StyleKey.DialogMainButtonText] = Colors.White, + [StyleKey.DialogMainButtonTextDisabled] = Color3.fromRGB(102, 102, 102), + [StyleKey.DimmedText] = Color3.fromRGB(136, 136, 136), + + [StyleKey.ErrorText] = Colors.Red, + + [StyleKey.InputFieldBackground] = Colors.White, + + [StyleKey.LinkText] = Colors.Blue_Light, + + [StyleKey.MainBackground] = Colors.White, + [StyleKey.MainButton] = Color3.fromRGB(228, 238, 254), + [StyleKey.MainText] = Colors.Black, + [StyleKey.MainTextDisabled] = Color3.fromRGB(120, 120, 120), + [StyleKey.Mid] = Color3.fromRGB(238, 238, 238), + + [StyleKey.RibbonTab] = Color3.fromRGB(243, 243, 243), + + [StyleKey.ScrollBarBackground] = Color3.fromRGB(238, 238, 238), + [StyleKey.ScrollBar] = Colors.White, + [StyleKey.SliderKnobColor] = Colors.White, + [StyleKey.SliderKnobImage] = "rbxasset://textures/DeveloperFramework/slider_knob_light.png", + [StyleKey.SliderBackground] = Color3.fromRGB(204, 204, 204), + [StyleKey.SubText] = Color3.fromRGB(170, 170, 170), + + [StyleKey.TitlebarText] = Colors.Black, + [StyleKey.ToggleOnImage] = "rbxasset://textures/RoactStudioWidgets/toggle_on_light.png", + [StyleKey.ToggleOffImage] = "rbxasset://textures/RoactStudioWidgets/toggle_off_light.png", + [StyleKey.ToggleDisabledImage] = "rbxasset://textures/RoactStudioWidgets/toggle_disable_light.png", + + [StyleKey.WarningText] = Color3.fromRGB(255, 128, 0), + + Font = Enum.Font.SourceSans, + TextSize = 18, +} \ No newline at end of file diff --git a/BuiltInPlugins/PlayerEmulator/Packages/Framework/Style/Themes/StudioTheme.lua b/BuiltInPlugins/PlayerEmulator/Packages/Framework/Style/Themes/StudioTheme.lua new file mode 100644 index 0000000000..9936f07430 --- /dev/null +++ b/BuiltInPlugins/PlayerEmulator/Packages/Framework/Style/Themes/StudioTheme.lua @@ -0,0 +1,49 @@ +--[[ + The Default theme for Studio. +]] + +local Framework = script.Parent.Parent.Parent +local DarkTheme = require(Framework.Style.Themes.DarkTheme) +local LightTheme = require(Framework.Style.Themes.LightTheme) +local createDefaultTheme = require(Framework.Style.createDefaultTheme) +local Cryo = require(Framework.Util).Cryo + +local getThemeName = function() + return settings().Studio.Theme.Name +end + +local StudioTheme = {} + +function StudioTheme.new(darkThemeOverride, lightThemeOverride) + local darkTheme = DarkTheme + if darkThemeOverride then + darkTheme = Cryo.Dictionary.join(DarkTheme, darkThemeOverride) + end + + local lightTheme = LightTheme + if lightThemeOverride then + lightTheme = Cryo.Dictionary.join(LightTheme, lightThemeOverride) + end + + local themeProps = { + getThemeName = getThemeName, + themesList = { + ["Dark"] = darkTheme, + ["Light"] = lightTheme, + }, + themeChangedConnection = settings().Studio.ThemeChanged, + } + return createDefaultTheme(themeProps) +end + +function StudioTheme.mock() + local themeProps = { + getThemeName = function() return "Dark" end, + themesList = { + Dark = DarkTheme, + }, + } + return createDefaultTheme(themeProps) +end + +return StudioTheme \ No newline at end of file diff --git a/BuiltInPlugins/PlayerEmulator/Packages/Framework/Style/Themes/StudioTheme.spec.lua b/BuiltInPlugins/PlayerEmulator/Packages/Framework/Style/Themes/StudioTheme.spec.lua new file mode 100644 index 0000000000..0392df2733 --- /dev/null +++ b/BuiltInPlugins/PlayerEmulator/Packages/Framework/Style/Themes/StudioTheme.spec.lua @@ -0,0 +1,8 @@ +return function() + local StudioTheme = require(script.Parent.StudioTheme) + + it("should create a base theme without issue", function() + local result = StudioTheme.mock() + expect(result).to.be.ok() + end) +end \ No newline at end of file diff --git a/BuiltInPlugins/PlayerEmulator/Packages/Framework/Style/createDefaultTheme.lua b/BuiltInPlugins/PlayerEmulator/Packages/Framework/Style/createDefaultTheme.lua new file mode 100644 index 0000000000..923e1ec9f7 --- /dev/null +++ b/BuiltInPlugins/PlayerEmulator/Packages/Framework/Style/createDefaultTheme.lua @@ -0,0 +1,17 @@ +--[[ + Creates a default theme with given props. +]] + +local Style = script.Parent + +local BaseTheme = require(Style.Themes.BaseTheme) +local Stylizer = require(Style.Stylizer) + +return function(themeProps) + assert(typeof(themeProps) == "table", "createDefaultTheme expects themeProps parameter to be a table") + assert(typeof(themeProps.themesList) == "table", "createDefaultTheme expects themeProps to contain a table `themesList`") + assert(typeof(themeProps.getThemeName) == "function", "createDefaultTheme expects themeProps to contain a function `getThemeName`") + + local style = Stylizer.new(themeProps.themesList[themeProps.getThemeName()], themeProps) + return style:extend(BaseTheme) +end \ No newline at end of file diff --git a/BuiltInPlugins/PlayerEmulator/Packages/Framework/Style/createDefaultTheme.spec.lua b/BuiltInPlugins/PlayerEmulator/Packages/Framework/Style/createDefaultTheme.spec.lua new file mode 100644 index 0000000000..8fbbba0cc0 --- /dev/null +++ b/BuiltInPlugins/PlayerEmulator/Packages/Framework/Style/createDefaultTheme.spec.lua @@ -0,0 +1,30 @@ +return function() + local createDefaultTheme = require(script.Parent.createDefaultTheme) + local DarkTheme = require(script.Parent.Themes.DarkTheme) + + local function callWithProps() + local themeProps = { + getThemeName = function() return "Dark" end, + themesList = { + Dark = DarkTheme, + }, + } + + return createDefaultTheme(themeProps) + end + + it("should create a base theme without issue", function() + local result = callWithProps() + expect(result).to.be.ok() + end) + + it("should be extendable", function() + local result = callWithProps() + local success, _ = pcall(function() + return result:extend({ + hello = "world", + }) + end) + expect(success).to.equal(true) + end) +end \ No newline at end of file diff --git a/BuiltInPlugins/PlayerEmulator/Packages/Framework/Style/getRawComponentStyle.lua b/BuiltInPlugins/PlayerEmulator/Packages/Framework/Style/getRawComponentStyle.lua new file mode 100644 index 0000000000..5c76de757c --- /dev/null +++ b/BuiltInPlugins/PlayerEmulator/Packages/Framework/Style/getRawComponentStyle.lua @@ -0,0 +1,17 @@ +--[[ + Gets the original raw, un-calcuated UI or StudioUI style table for a given component. +]] + +local Framework = script.Parent.Parent +local StudioUIFolderData = require(Framework.StudioUI.StudioUIFolderData) +local UIFolderData = require(Framework.UI.UIFolderData) + +return function(componentName) + local componentData = UIFolderData[componentName] or StudioUIFolderData[componentName] + local result + + if componentData.style then + result = require(componentData.style) + end + return result +end \ No newline at end of file diff --git a/BuiltInPlugins/PlayerEmulator/Packages/Framework/Style/getRawComponentStyle.spec.lua b/BuiltInPlugins/PlayerEmulator/Packages/Framework/Style/getRawComponentStyle.spec.lua new file mode 100644 index 0000000000..c8d9020535 --- /dev/null +++ b/BuiltInPlugins/PlayerEmulator/Packages/Framework/Style/getRawComponentStyle.spec.lua @@ -0,0 +1,22 @@ +return function() + local Framework = script.Parent.Parent + local getRawComponentStyle = require(script.Parent.getRawComponentStyle) + + it("should get the style table for the correct UI component", function() + local result = getRawComponentStyle("Button") + + local styleFile = Framework.UI.Button:FindFirstChild("style") + local styleTable = require(styleFile) + + expect(styleTable).to.equal(result) + end) + + it("should get the style table for the correct StudioUI component", function() + local result = getRawComponentStyle("SearchBar") + + local styleFile = Framework.StudioUI.SearchBar:FindFirstChild("style") + local styleTable = require(styleFile) + + expect(styleTable).to.equal(result) + end) +end \ No newline at end of file diff --git a/BuiltInPlugins/PlayerEmulator/Packages/Framework/TestHelpers.lua b/BuiltInPlugins/PlayerEmulator/Packages/Framework/TestHelpers.lua index 075bd0fb21..4ae630e7b4 100644 --- a/BuiltInPlugins/PlayerEmulator/Packages/Framework/TestHelpers.lua +++ b/BuiltInPlugins/PlayerEmulator/Packages/Framework/TestHelpers.lua @@ -1,6 +1,11 @@ local strict = require(script.Parent.Util.strict) return strict({ + Instances = require(script.Instances), + + makeSettableValue = require(script.makeSettableValue), provideMockContext = require(script.provideMockContext), runFrameworkTests = require(script.runFrameworkTests), -}) \ No newline at end of file + setEquals = require(script.setEquals), + testImmutability = require(script.testImmutability), +}) diff --git a/BuiltInPlugins/PlayerEmulator/Packages/Framework/TestHelpers/Instances.lua b/BuiltInPlugins/PlayerEmulator/Packages/Framework/TestHelpers/Instances.lua new file mode 100644 index 0000000000..c005f97da5 --- /dev/null +++ b/BuiltInPlugins/PlayerEmulator/Packages/Framework/TestHelpers/Instances.lua @@ -0,0 +1,10 @@ +local strict = require(script.Parent.Parent.Util.strict) + +return strict({ + MockAnalyticsService = require(script.MockAnalyticsService), + MockMouse = require(script.MockMouse), + MockPlugin = require(script.MockPlugin), + MockPluginToolbar = require(script.MockPluginToolbar), + MockPluginToolbarButton = require(script.MockPluginToolbarButton), + MockSelectionService = require(script.MockSelectionService), +}) diff --git a/BuiltInPlugins/PlayerEmulator/Packages/Framework/TestHelpers/Instances/MockAnalyticsService.lua b/BuiltInPlugins/PlayerEmulator/Packages/Framework/TestHelpers/Instances/MockAnalyticsService.lua new file mode 100644 index 0000000000..7da446b5cc --- /dev/null +++ b/BuiltInPlugins/PlayerEmulator/Packages/Framework/TestHelpers/Instances/MockAnalyticsService.lua @@ -0,0 +1,41 @@ +local MockAnalyticsService = {} +MockAnalyticsService.__index = MockAnalyticsService + +function MockAnalyticsService.new() + local self = setmetatable({ + eventCount = 0, + lastEvent = nil, + + _sessionId = "", + }, MockAnalyticsService) + + return self +end + +function MockAnalyticsService:Destroy() +end + +function MockAnalyticsService:GetSessionId() + return self._sessionId +end + +function MockAnalyticsService:SendEventDeferred(target, context, evt, argsTable) + local event = { + target = target, + ctx = context, + evt = evt, + } + + assert(type(argsTable) == "table", "expected table, argsTable was " .. type(argsTable)) + for k, v in pairs(argsTable) do + if event[k] ~= nil then + warn("Overriding base keyword " .. k .. " in via argsTable in SendEventDeferred()." ) + end + event[k] = v + end + + self.lastEvent = event + self.eventCount = self.eventCount + 1 +end + +return MockAnalyticsService diff --git a/BuiltInPlugins/PlayerEmulator/Packages/Framework/TestHelpers/Instances/MockMouse.lua b/BuiltInPlugins/PlayerEmulator/Packages/Framework/TestHelpers/Instances/MockMouse.lua new file mode 100644 index 0000000000..c3178b260d --- /dev/null +++ b/BuiltInPlugins/PlayerEmulator/Packages/Framework/TestHelpers/Instances/MockMouse.lua @@ -0,0 +1,23 @@ +local DevFrameworkRoot = script.Parent.Parent.Parent +local Signal = require(DevFrameworkRoot.Util.Signal) + +local MockMouse = {} +MockMouse.__index = MockMouse + +function MockMouse.new() + return setmetatable({ + Icon = "rbxasset://SystemCursors/Arrow", + + Origin = CFrame.new(), + UnitRay = Ray.new(Vector3.new(), Vector3.new()), + Target = nil, + + WheelForward = Signal.new(), + WheelBackward = Signal.new(), + }, MockMouse) +end + +function MockMouse:Destroy() +end + +return MockMouse diff --git a/BuiltInPlugins/PlayerEmulator/Packages/Framework/TestHelpers/Instances/MockPlugin.lua b/BuiltInPlugins/PlayerEmulator/Packages/Framework/TestHelpers/Instances/MockPlugin.lua new file mode 100644 index 0000000000..38b2fcabac --- /dev/null +++ b/BuiltInPlugins/PlayerEmulator/Packages/Framework/TestHelpers/Instances/MockPlugin.lua @@ -0,0 +1,133 @@ +local DevFrameworkRoot = script.Parent.Parent.Parent +local Signal = require(DevFrameworkRoot.Util.Signal) + +local MockMouse = require(script.Parent.MockMouse) +local MockPluginToolbar = require(script.Parent.MockPluginToolbar) + +local MockPlugin = {} +MockPlugin.__index = MockPlugin + +local function createScreenGui() + local screen = Instance.new("ScreenGui", game.CoreGui) + screen.Name = "PluginMockGui" + screen.ZIndexBehavior = Enum.ZIndexBehavior.Sibling + return screen +end + +--[[ + id : string? + mockedPlugins : {[MockPlugin] : boolean}? + Optional set for interfacing with other plugins created in a unit test. + E.g. when calling MockPlugin:Activate(), all other plugins in the mockedPlugins set get deactivated + For tests that don't need this functionality, leave mockedPlugins as nil +]] +function MockPlugin.new(id, mockedPlugins) + local self = setmetatable({ + _id = id or "", + Name = id or "MockPlugin", + + Deactivation = Signal.new(), + Unloading = Signal.new(), + + _activated = false, + _activatedWithExclusiveMouse = false, + + _mouse = MockMouse.new(), + + _toolbars = {}, + + subWindows = {}, + }, MockPlugin) + + if mockedPlugins then + self._mockedPlugins = mockedPlugins + self._mockedPlugins[self] = true + end + + return self +end + +function MockPlugin:Destroy() + for _, toolbar in pairs(self._toolbars) do + toolbar:Destroy() + end + self._toolbars = {} + + if self._mouse then + self._mouse:Destroy() + self._mouse = nil + end + + if self._mockedPlugins then + self._mockedPlugins[self] = nil + self._mockedPlugins = nil + end +end + +function MockPlugin:CreateToolbar(id) + if self._toolbars[id] then + return self._toolbars[id] + end + + local toolbar = MockPluginToolbar.new(self, id) + self._toolbars[id] = toolbar + return toolbar +end + +function MockPlugin:IsActivated() + return self._activated +end + +function MockPlugin:IsActivatedWithExclusiveMouse() + return self._activatedWithExclusiveMouse +end + +function MockPlugin:Activate(exclusiveMouse) + if self._mockedPlugins then + for mockedPlugin, _ in pairs(self._mockedPlugins) do + if mockedPlugin._activated then + mockedPlugin:Deactivate() + end + end + end + + self._activated = true + self._activatedWithExclusiveMouse = exclusiveMouse and true or false +end + +function MockPlugin:Deactivate() + if not self._activated then + return + end + self._activated = false + self._activatedWithExclusiveMouse = false + self.Deactivation:Fire() +end + +function MockPlugin:GetMouse() + return self._mouse +end + +function MockPlugin:GetSubWindow(index) + local now = tick() + local timeout = now + 1 + while not self.subWindows[index] do + wait() + if tick() > now + timeout then + error("Sub-window has not been created") + end + end + return self.subWindows[index] +end + +function MockPlugin:CreateDockWidgetPluginGui(_, ...) + local gui = createScreenGui() + table.insert(self.subWindows, gui) + return gui +end + +function MockPlugin:CreateQWidgetPluginGui(title, ...) + return self:CreateDockWidgetPluginGui(title, ...) +end + +return MockPlugin diff --git a/BuiltInPlugins/PlayerEmulator/Packages/Framework/TestHelpers/Instances/MockPlugin.spec.lua b/BuiltInPlugins/PlayerEmulator/Packages/Framework/TestHelpers/Instances/MockPlugin.spec.lua new file mode 100644 index 0000000000..7123f1382d --- /dev/null +++ b/BuiltInPlugins/PlayerEmulator/Packages/Framework/TestHelpers/Instances/MockPlugin.spec.lua @@ -0,0 +1,78 @@ +local MockPlugin = require(script.Parent.MockPlugin) + +return function() + it("should have a mouse", function() + local plugin = MockPlugin.new() + expect(plugin:GetMouse()).to.be.ok() + end) + + it("should create toolbars", function() + local plugin = MockPlugin.new() + local toolbar = plugin:CreateToolbar("Foo") + + expect(toolbar).to.be.ok() + expect(toolbar._plugin).to.equal(plugin) + expect(toolbar._id).to.equal("Foo") + expect(toolbar.Text).to.equal("Foo") + + toolbar:Destroy() + plugin:Destroy() + end) + + describe("activation", function() + it("should work", function() + local plugin = MockPlugin.new() + expect(plugin:IsActivated()).to.equal(false) + expect(plugin:IsActivatedWithExclusiveMouse()).to.equal(false) + + plugin:Activate() + expect(plugin:IsActivated()).to.equal(true) + expect(plugin:IsActivatedWithExclusiveMouse()).to.equal(false) + + plugin:Activate(true) + expect(plugin:IsActivated()).to.equal(true) + expect(plugin:IsActivatedWithExclusiveMouse()).to.equal(true) + + plugin:Deactivate() + expect(plugin:IsActivated()).to.equal(false) + expect(plugin:IsActivatedWithExclusiveMouse()).to.equal(false) + end) + + it("should fire a signal when deactivated", function() + local signalFired = false + + local plugin = MockPlugin.new() + local con = plugin.Deactivation:Connect(function() + signalFired = true + end) + + -- Plugin starts deactivated so this should do nothing + plugin:Deactivate() + expect(signalFired).to.equal(false) + + plugin:Activate() + plugin:Deactivate() + expect(signalFired).to.equal(true) + + con:Disconnect() + end) + + it("should deactivate other plugins if mockedPlugins set is given", function() + local mockedPlugins = {} + + local p1 = MockPlugin.new("", mockedPlugins) + local p2 = MockPlugin.new("", mockedPlugins) + + expect(p1:IsActivated()).to.equal(false) + expect(p2:IsActivated()).to.equal(false) + + p1:Activate() + expect(p1:IsActivated()).to.equal(true) + expect(p2:IsActivated()).to.equal(false) + + p2:Activate() + expect(p1:IsActivated()).to.equal(false) + expect(p2:IsActivated()).to.equal(true) + end) + end) +end diff --git a/BuiltInPlugins/PlayerEmulator/Packages/Framework/TestHelpers/Instances/MockPluginToolbar.lua b/BuiltInPlugins/PlayerEmulator/Packages/Framework/TestHelpers/Instances/MockPluginToolbar.lua new file mode 100644 index 0000000000..6548cea079 --- /dev/null +++ b/BuiltInPlugins/PlayerEmulator/Packages/Framework/TestHelpers/Instances/MockPluginToolbar.lua @@ -0,0 +1,63 @@ +local MockPluginToolbarButton = require(script.Parent.MockPluginToolbarButton) + +local MockPluginToolbar = {} +MockPluginToolbar.__index = MockPluginToolbar + +function MockPluginToolbar.new(plugin, id) + local self = { + _plugin = plugin, + _id = id, + + Name = id, + Text = id, + + _buttons = {}, + } + + setmetatable(self, MockPluginToolbar) + return self +end + +function MockPluginToolbar:Destroy() + self._plugin = nil + for _, button in pairs(self._buttons) do + button:Destroy() + end + self._buttons = {} +end + +function MockPluginToolbar:CreateButton(id, tooltip, icon, text) + local hasId = id and #id > 0 + local hasTooltip = tooltip and #tooltip > 0 + local hasIcon = icon and #icon > 0 + local hasText = text and #text > 0 + + local useLegacyBehavior = hasTooltip or hasIcon or hasText + + local finalId + local finalText + + if useLegacyBehavior then + finalId = hasId and id or tooltip + finalText = hasText and text or (hasId and id or tooltip) + else + finalId = id + finalText = id + + assert(#finalId > 0, ("Toolbar %s tried to create a button with empty id"):format(self._id)) + end + + assert(not self._buttons[finalId], ("Toolbar %s already has a button with id %s"):format(self._id, finalId)) + local button = MockPluginToolbarButton.new(self._plugin, self, finalId) + self._buttons[finalId] = button + + button.Text = finalText + if useLegacyBehavior then + button.Tooltip = tooltip or "" + button.Icon = icon or "" + end + + return button +end + +return MockPluginToolbar diff --git a/BuiltInPlugins/PlayerEmulator/Packages/Framework/TestHelpers/Instances/MockPluginToolbar.spec.lua b/BuiltInPlugins/PlayerEmulator/Packages/Framework/TestHelpers/Instances/MockPluginToolbar.spec.lua new file mode 100644 index 0000000000..94d7e3c353 --- /dev/null +++ b/BuiltInPlugins/PlayerEmulator/Packages/Framework/TestHelpers/Instances/MockPluginToolbar.spec.lua @@ -0,0 +1,38 @@ +local MockPluginToolbar = require(script.Parent.MockPluginToolbar) + +return function() + describe("CreateButton", function() + it("should support legacy API", function() + local toolbar = MockPluginToolbar.new(nil, "") + + local button = toolbar:CreateButton("id", "tooltip", "icon", "text") + expect(button._toolbar).to.equal(toolbar) + expect(button._id).to.equal("id") + expect(button.Tooltip).to.equal("tooltip") + expect(button.Icon).to.equal("icon") + expect(button.Text).to.equal("text") + button:Destroy() + + button = toolbar:CreateButton("", "foo") + expect(button._id).to.equal("foo") + expect(button.Tooltip).to.equal("foo") + expect(button.Icon).to.equal("") + expect(button.Text).to.equal("foo") + button:Destroy() + + toolbar:Destroy() + end) + + it("should support new API", function() + local toolbar = MockPluginToolbar.new(nil, "") + local button = toolbar:CreateButton("foo_id") + expect(button._toolbar).to.equal(toolbar) + expect(button._id).to.equal("foo_id") + expect(button.Tooltip).to.equal("") + expect(button.Icon).to.equal("") + expect(button.Text).to.equal("foo_id") + button:Destroy() + toolbar:Destroy() + end) + end) +end diff --git a/BuiltInPlugins/PlayerEmulator/Packages/Framework/TestHelpers/Instances/MockPluginToolbarButton.lua b/BuiltInPlugins/PlayerEmulator/Packages/Framework/TestHelpers/Instances/MockPluginToolbarButton.lua new file mode 100644 index 0000000000..29c148490a --- /dev/null +++ b/BuiltInPlugins/PlayerEmulator/Packages/Framework/TestHelpers/Instances/MockPluginToolbarButton.lua @@ -0,0 +1,42 @@ +local DevFrameworkRoot = script.Parent.Parent.Parent +local Signal = require(DevFrameworkRoot.Util.Signal) + +local MockPluginToolbarButton = {} +MockPluginToolbarButton.__index = MockPluginToolbarButton + +function MockPluginToolbarButton.new(plugin, toolbar, id) + local self = { + _plugin = plugin, + _toolbar = toolbar, + _id = id, + + Name = id, + Tooltip = "", + Icon = "", + Text = "", + Enabled = true, + Active = false, + ClickableWhenViewportHidden = true, + + Click = Signal.new(), + } + setmetatable(self, MockPluginToolbarButton) + + return self +end + +function MockPluginToolbarButton:Destroy() + self._toolbar = nil + self._plugin = nil +end + +function MockPluginToolbarButton:SetActive(newActive) + if self._plugin and self.Active and not newActive then + self._plugin:Deactivate() + end + + self.Active = newActive +end + +return MockPluginToolbarButton + diff --git a/BuiltInPlugins/PlayerEmulator/Packages/Framework/TestHelpers/Instances/MockPluginToolbarButton.spec.lua b/BuiltInPlugins/PlayerEmulator/Packages/Framework/TestHelpers/Instances/MockPluginToolbarButton.spec.lua new file mode 100644 index 0000000000..806bd9ab06 --- /dev/null +++ b/BuiltInPlugins/PlayerEmulator/Packages/Framework/TestHelpers/Instances/MockPluginToolbarButton.spec.lua @@ -0,0 +1,22 @@ +local MockPlugin = require(script.Parent.MockPlugin) + +return function() + it("should deactivate the parent plugin when deactivating", function() + local plugin = MockPlugin.new() + local toolbar = plugin:CreateToolbar("") + local button = toolbar:CreateButton("foo") + + plugin:Activate() + -- Only catch when going from active -> not active + button:SetActive(false) + expect(plugin:IsActivated()).to.equal(true) + + button:SetActive(true) + expect(plugin:IsActivated()).to.equal(true) + + button:SetActive(false) + expect(plugin:IsActivated()).to.equal(false) + + plugin:Destroy() + end) +end diff --git a/BuiltInPlugins/PlayerEmulator/Packages/Framework/TestHelpers/Instances/MockSelectionService.lua b/BuiltInPlugins/PlayerEmulator/Packages/Framework/TestHelpers/Instances/MockSelectionService.lua new file mode 100644 index 0000000000..94aafdb058 --- /dev/null +++ b/BuiltInPlugins/PlayerEmulator/Packages/Framework/TestHelpers/Instances/MockSelectionService.lua @@ -0,0 +1,28 @@ +local DevFrameworkRoot = script.Parent.Parent.Parent +local Signal = require(DevFrameworkRoot.Util.Signal) + +local MockSelectionService = {} +MockSelectionService.__index = MockSelectionService + +function MockSelectionService.new() + local self = setmetatable({ + _selection = {}, + SelectionChanged = Signal.new(), + }, MockSelectionService) + + return self +end + +function MockSelectionService:Destroy() +end + +function MockSelectionService:Get() + return self._selection +end + +function MockSelectionService:Set(selection) + self._selection = selection + self.SelectionChanged:Fire() +end + +return MockSelectionService diff --git a/BuiltInPlugins/PlayerEmulator/Packages/Framework/TestHelpers/Instances/MockStudioService.lua b/BuiltInPlugins/PlayerEmulator/Packages/Framework/TestHelpers/Instances/MockStudioService.lua new file mode 100644 index 0000000000..1c0b6ae981 --- /dev/null +++ b/BuiltInPlugins/PlayerEmulator/Packages/Framework/TestHelpers/Instances/MockStudioService.lua @@ -0,0 +1,19 @@ +local MockStudioService = {} +MockStudioService.__index = MockStudioService + +function MockStudioService.new() + local self = setmetatable({ + _localUserId = 0, + }, MockStudioService) + + return self +end + +function MockStudioService:Destroy() +end + +function MockStudioService:GetUserId() + return self._localUserId +end + +return MockStudioService diff --git a/BuiltInPlugins/PlayerEmulator/Packages/Framework/TestHelpers/makeSettableValue.lua b/BuiltInPlugins/PlayerEmulator/Packages/Framework/TestHelpers/makeSettableValue.lua new file mode 100644 index 0000000000..cb5210e281 --- /dev/null +++ b/BuiltInPlugins/PlayerEmulator/Packages/Framework/TestHelpers/makeSettableValue.lua @@ -0,0 +1,21 @@ +local DevFrameworkRoot = script.Parent.Parent +local Signal = require(DevFrameworkRoot.Util.Signal) + +--[[ +Wrapper to encapsulate a value with get/set methods and a changed signal +]] +return function(initialValue) + local value = initialValue + local changed = Signal.new() + + return { + get = function() + return value + end, + set = function(newValue) + value = newValue + changed:Fire() + end, + changed = changed, + } +end diff --git a/BuiltInPlugins/PlayerEmulator/Packages/Framework/TestHelpers/makeSettableValue.spec.lua b/BuiltInPlugins/PlayerEmulator/Packages/Framework/TestHelpers/makeSettableValue.spec.lua new file mode 100644 index 0000000000..4db840c64b --- /dev/null +++ b/BuiltInPlugins/PlayerEmulator/Packages/Framework/TestHelpers/makeSettableValue.spec.lua @@ -0,0 +1,40 @@ +local makeSettableValue = require(script.Parent.makeSettableValue) + +return function() + it("should store the initial value", function() + local v = makeSettableValue("foo") + expect(v.get()).to.equal("foo") + + v = makeSettableValue(12345) + expect(v.get()).to.equal(12345) + end) + + it("should update the stored value", function() + local v = makeSettableValue("foo") + + v.set("bar") + expect(v.get()).to.equal("bar") + + v.set(13579) + expect(v.get()).to.equal(13579) + end) + + it("should fire a changed signal", function() + local signalFired = false + + local v = makeSettableValue("foo") + local con = v.changed:Connect(function() + signalFired = true + end) + + expect(signalFired).to.equal(false) + v.set("bar") + expect(signalFired).to.equal(true) + + signalFired = false + v.set("baz") + expect(signalFired).to.equal(true) + + con:Disconnect() + end) +end diff --git a/BuiltInPlugins/PlayerEmulator/Packages/Framework/TestHelpers/provideMockContext.lua b/BuiltInPlugins/PlayerEmulator/Packages/Framework/TestHelpers/provideMockContext.lua index 25a3151d5e..10633a0dc7 100644 --- a/BuiltInPlugins/PlayerEmulator/Packages/Framework/TestHelpers/provideMockContext.lua +++ b/BuiltInPlugins/PlayerEmulator/Packages/Framework/TestHelpers/provideMockContext.lua @@ -27,20 +27,28 @@ local DevFrameworkRoot = script.Parent.Parent local ContextServices = require(DevFrameworkRoot.ContextServices) local StudioFrameworkStyles = require(DevFrameworkRoot.StudioUI).StudioFrameworkStyles -local mockPlugin = require(DevFrameworkRoot.TestHelpers.Services.mockPlugin) +local MockPlugin = require(DevFrameworkRoot.TestHelpers.Instances.MockPlugin) local Rodux = require(DevFrameworkRoot.Parent.Rodux) - +local StudioTheme = require(DevFrameworkRoot.Style.Themes.StudioTheme) +local Util = require(DevFrameworkRoot.Util) +local FlagsList = Util.Flags.new({ + FFlagRefactorDevFrameworkTheme = {"RefactorDevFrameworkTheme"}, +}) -- contextItemsList : (table, optional) a list of ContextItems to include in the stack. Will override any duplicates. -- children : (table, required) a map of children like you would pass into Roact.createElement's children return function(contextItemsList, children) if contextItemsList then assert(type(contextItemsList) == "table", "Expected contextItemsList to be a table.") - assert(type(next(contextItemsList)) == "number" or type(next(contextItemsList)) == "nil", + assert(type((next(contextItemsList))) == "number" or type((next(contextItemsList))) == "nil", "Expected contextItemsList to be an array.") end assert(type(children) == "table", "Expected children to be a table.") - assert(type(next(children)) == "string", "Expected children to be a map of components.") + assert(type((next(children))) == "string", "Expected children to be a map of components.") + + -- Multiple items use the plugin in some way + -- Create 1 mock plugin and use it in each + local mockPlugin = MockPlugin.new() -- create a list of default mocks local contextItems = {} @@ -55,9 +63,7 @@ return function(contextItemsList, children) table.insert(contextItems, localization) -- Mouse - local mouse = ContextServices.Mouse.new({ - Icon = "rbxasset://SystemCursors/Arrow", - }) + local mouse = ContextServices.Mouse.new(mockPlugin:GetMouse()) table.insert(contextItems, mouse) -- Navigation @@ -69,11 +75,11 @@ return function(contextItemsList, children) table.insert(contextItems, analytics) -- Plugin - local plugin = ContextServices.Plugin.new(mockPlugin.new()) + local plugin = ContextServices.Plugin.new(mockPlugin) table.insert(contextItems, plugin) -- PluginActions - local pluginActions = ContextServices.PluginActions.new(mockPlugin.new(), {}) + local pluginActions = ContextServices.PluginActions.new(mockPlugin, {}) table.insert(contextItems, pluginActions) -- Store @@ -83,19 +89,24 @@ return function(contextItemsList, children) table.insert(contextItems, store) -- Theme - local theme = ContextServices.Theme.mock(function(theme, getColor) - return { - Framework = StudioFrameworkStyles.new(theme, getColor), - } - end, function() - return { - Name = Enum.UITheme.Light.Name, - - GetColor = function(_, _) - return Color3.new() - end, - } - end) + local theme + if FlagsList:get("FFlagRefactorDevFrameworkTheme") then + theme = StudioTheme.mock() + else + theme = ContextServices.Theme.mock(function(theme, getColor) + return { + Framework = StudioFrameworkStyles.new(theme, getColor), + } + end, function() + return { + Name = "Light", + + GetColor = function(_, _) + return Color3.new() + end, + } + end) + end table.insert(contextItems, theme) @@ -109,4 +120,4 @@ return function(contextItemsList, children) -- render the components inside the provided context stack return ContextServices.provide(contextItems, children) -end \ No newline at end of file +end diff --git a/BuiltInPlugins/PlayerEmulator/Packages/Framework/TestHelpers/provideMockContext.spec.lua b/BuiltInPlugins/PlayerEmulator/Packages/Framework/TestHelpers/provideMockContext.spec.lua index af4c975042..d2500676da 100644 --- a/BuiltInPlugins/PlayerEmulator/Packages/Framework/TestHelpers/provideMockContext.spec.lua +++ b/BuiltInPlugins/PlayerEmulator/Packages/Framework/TestHelpers/provideMockContext.spec.lua @@ -4,7 +4,10 @@ local provideMockContext = require(script.Parent.provideMockContext) local ContextServices = require(Framework.ContextServices) local Provider = require(Framework.ContextServices.Provider) local ContextItem = require(Framework.ContextServices.ContextItem) - +local Util = require(Framework.Util) +local FlagsList = Util.Flags.new({ + FFlagRefactorDevFrameworkTheme = {"RefactorDevFrameworkTheme"}, +}) return function() it("should work without arguments", function() local element = provideMockContext({}, { @@ -37,10 +40,15 @@ return function() function testComponent:render() wasRendered = true - local theme = self.props.Theme:get("Framework") local localization = self.props.Localization local plugin = self.props.Plugin:get() local mouse = self.props.Mouse:get() + local theme + if FlagsList:get("FFlagRefactorDevFrameworkTheme") then + theme = self.props.Stylizer + else + theme = self.props.Theme:get("Framework") + end expect(theme).to.never.equal(nil) expect(localization).to.never.equal(nil) @@ -51,7 +59,8 @@ return function() end ContextServices.mapToProps(testComponent, { - Theme = ContextServices.Theme, + Stylizer = FlagsList:get("FFlagRefactorDevFrameworkTheme") and ContextServices.Stylizer or nil, + Theme = (not FlagsList:get("FFlagRefactorDevFrameworkTheme")) and ContextServices.Theme or nil, Localization = ContextServices.Localization, Mouse = ContextServices.Mouse, Plugin = ContextServices.Plugin, diff --git a/BuiltInPlugins/PlayerEmulator/Packages/Framework/TestHelpers/setEquals.lua b/BuiltInPlugins/PlayerEmulator/Packages/Framework/TestHelpers/setEquals.lua new file mode 100644 index 0000000000..31e25dbc20 --- /dev/null +++ b/BuiltInPlugins/PlayerEmulator/Packages/Framework/TestHelpers/setEquals.lua @@ -0,0 +1,16 @@ +-- Helper to verify that 2 sets have exactly the same keys +return function(result, expected) + for k in pairs(result) do + if not expected[k] then + return false + end + end + + for k in pairs(expected) do + if not result[k] then + return false + end + end + + return true +end diff --git a/BuiltInPlugins/PlayerEmulator/Packages/Framework/TestHelpers/testImmutability.lua b/BuiltInPlugins/PlayerEmulator/Packages/Framework/TestHelpers/testImmutability.lua new file mode 100644 index 0000000000..c56fb0bf24 --- /dev/null +++ b/BuiltInPlugins/PlayerEmulator/Packages/Framework/TestHelpers/testImmutability.lua @@ -0,0 +1,74 @@ +--[[ + A function that every reducer should use to check that valid actions do not mutate the previous state. + + This function takes a snapshot of the state before and after an action is applied. + Then it checks if any fields from the original data have been mutated. + + The final output of the reducer is ultimately irrelevant for this test. + All that matters is that the original data is preserved and unmodified. +]] +local DevFrameworkRoot = script.Parent.Parent +local deepJoin = require(DevFrameworkRoot.Util.deepJoin) + +local function allFieldsAreUnchanged(tableA, tableB) + -- if there's some mistake, escape + if type(tableA) ~= "table" or type(tableB) ~= "table" then + if not tableA then + tableA = "nil" + end + if not tableB then + tableB = "nil" + end + error(string.format("Expected to compare two tables, got %s and %s", tostring(tableA), tostring(tableB))) + end + + -- count all the keys in B + local expectedNumKeysB = 0 + for _, _ in pairs(tableB) do + expectedNumKeysB = expectedNumKeysB + 1 + end + + -- check that all keys are equal in type and value + local expectedNumKeysA = 0 + for key, vA in pairs(tableA) do + expectedNumKeysA = expectedNumKeysA + 1 + local vB = tableB[key] + + if type(vA) == "table" then + -- if there's a child, verify that all its values are unmutated + allFieldsAreUnchanged(vA, vB) + else + if vA ~= vB then + error(string.format("the field \"%s\" no longer matches", key)) + end + end + end + + -- make sure that no keys haven't gone missing + if expectedNumKeysA ~= expectedNumKeysB then + return error(string.format("Number of keys mismatch : %d vs %d", expectedNumKeysA, expectedNumKeysB)) + end + + -- if we've made it here, these tables are separate yet equal matches + return true +end + + +return function(reducer, action, previousState) + assert(type(reducer) == "function", "Expected a reducer to test") + assert(type(action) == "table", "Expected an action to test") + if previousState ~= nil then + assert(type(previousState) == "table", "Expected previousState to be a table") + end + + -- copy the originalState + local originalState = reducer(previousState, { type = "__nil__" }) + local originalStateCopy = deepJoin(originalState, {}) + assert(allFieldsAreUnchanged(originalState, originalStateCopy), "deepJoin mutates fields") + + -- run the state through a reducer, disregard the output + reducer(originalState, action) + + -- check that originalState still matches the originalStateCopy + return allFieldsAreUnchanged(originalState, originalStateCopy) +end diff --git a/BuiltInPlugins/PlayerEmulator/Packages/Framework/TestHelpers/testImmutability.spec.lua b/BuiltInPlugins/PlayerEmulator/Packages/Framework/TestHelpers/testImmutability.spec.lua new file mode 100644 index 0000000000..e3eee1ea0e --- /dev/null +++ b/BuiltInPlugins/PlayerEmulator/Packages/Framework/TestHelpers/testImmutability.spec.lua @@ -0,0 +1,169 @@ +local DevFrameworkRoot = script.Parent.Parent + +local Rodux = require(DevFrameworkRoot.Parent.Rodux) +local Cryo = require(DevFrameworkRoot.Util.Cryo) + +local Action = require(DevFrameworkRoot.Util.Action) + +local testImmutability = require(script.Parent.testImmutability) + +return function() + it("should error on invalid input", function() + -- expected fields + -- reducer : (Rodux reducer) + -- action : (Action) + + local function createTestReducer() + local defaultState = { + foo = "bar", + } + local testReducer = Rodux.createReducer(defaultState, { + emptyAction = function(state, _) + return Cryo.Dictionary.join(state, {}) + end, + }) + + return testReducer + end + local emptyAction = Action("emptyAction", function() + return {} + end) + + -- if everything is fine, return true + expect(testImmutability(createTestReducer(), emptyAction)).to.equal(true) + + -- invalid reducer + expect(function() + testImmutability("", emptyAction) + end).to.throw() + + -- invalid action + expect(function() + testImmutability(createTestReducer(), "hello") + end).to.throw() + end) + + it("should return true if the originalState is unchanged by the supplied action", function() + -- create a proper reducer + local r = Rodux.createReducer({ value = "foo" }, { + setValue = function(state, action) + local newValue = action.value + return Cryo.Dictionary.join(state, { + value = newValue + }) + end + }) + local setValueAction = Action("setValue", function(v) + return { value = v } + end) + local emptyAction = Action("emptyAction", function() + return {} + end) + + -- get the default state + local defaultState = r(nil, emptyAction) + expect(defaultState.value).to.equal("foo") + + -- show that this action can modify this state + local newState = r(nil, setValueAction("bar")) + expect(newState.value).to.equal("bar") + + -- even if the state can be modified by the action, + -- the original state table should not reflect those changes. + expect(testImmutability(r, setValueAction("test"))).to.equal(true) + end) + + it("should throw an error if the originalState has been modified in any way", function() + local r = Rodux.createReducer({ value = "foo" }, { + setValue = function(state, action) + -- erroneously modify the old state + local newValue = action.value + state.value = newValue + return state + end + }) + local setValueAction = Action("setValue", function(v) + return { value = v } + end) + + expect(function() + testImmutability(r, setValueAction("test")) + end).to.throw() + end) + + it("should catch changes in nested tables", function() + local r = Rodux.createReducer({ + value = { + children = { "foo", "bar", "cat" }, + }, + }, { + setChildren = function(state, action) + -- reuse the table from the old state + local newValue = {} + newValue.value = {} + newValue.value.children = state.value.children + + -- erroneously mutate the old data + newValue.value.children[1] = "fooo" + return newValue + end + }) + local setChildrenAction = Action("setChildren", function() + return {} + end) + + expect(function() + testImmutability(r, setChildrenAction("test")) + end).to.throw() + end) + + it("should return true when the result is two empty tables", function() + local emptyAction = Action("emptyAction", function() return {} end) + local testReducer = Rodux.createReducer({ + tA = {}, + tB = {}, + }, { + emptyAction = function(state, action) + return Cryo.Dictionary.join(state, {}) + end, + }) + + expect(testImmutability(testReducer, emptyAction)).to.equal(true) + end) + + it("should allow you pass a table in as a previous state", function() + -- create a state that holds non-default information + local previousState = { + tA = { + name = "test" + } + } + + -- create a reducer whose default state lacks enough information + -- to work on more advanced actions. Let's assume another action sets + -- the field 'tA', and changeNameAction works with its contents. + local testReducer = Rodux.createReducer({}, { + changeNameAction = function(state, action) + local target = action.target + local newName = action.name + + return Cryo.Dictionary.join(state, { + [target] = Cryo.Dictionary.join(state[target], { + name = newName + }), + }) + end, + }) + + -- create an action that extends work done by other actions + local changeNameAction = Action("changeNameAction", function(target, name) + return { + target = target, + name = name + } + end) + + expect(testImmutability(testReducer, changeNameAction, + previousState)).to.equal(true) + end) +end diff --git a/BuiltInPlugins/PlayerEmulator/Packages/Framework/UI/Box.lua b/BuiltInPlugins/PlayerEmulator/Packages/Framework/UI/Box.lua index e609f28176..1e13b22d73 100644 --- a/BuiltInPlugins/PlayerEmulator/Packages/Framework/UI/Box.lua +++ b/BuiltInPlugins/PlayerEmulator/Packages/Framework/UI/Box.lua @@ -1,10 +1,9 @@ --[[ A simple, solid color Decoration. - Required Props: - Theme Theme: A Theme ContextItem, which is provided via mapToProps. - Optional Props: + Theme Theme: A Theme ContextItem, which is provided via mapToProps. + Stylizer Stylizer: A Stylizer ContextItem, which is provided via mapToProps. Style Style: The style with which to render this component. StyleModifier StyleModifier: The StyleModifier index into Style. @@ -14,11 +13,15 @@ Color3 BorderColor: The color of the border. number BorderSize: the size of the border. ]] - local Framework = script.Parent.Parent local Roact = require(Framework.Parent.Roact) local ContextServices = require(Framework.ContextServices) -local Typecheck = require(Framework.Util).Typecheck +local Util = require(Framework.Util) +local Typecheck = Util.Typecheck + +local FlagsList = Util.Flags.new({ + FFlagRefactorDevFrameworkTheme = {"RefactorDevFrameworkTheme"}, +}) local Box = Roact.PureComponent:extend("Box") Typecheck.wrap(Box, script) @@ -26,7 +29,12 @@ Typecheck.wrap(Box, script) function Box:render() local props = self.props local theme = props.Theme - local style = theme:getStyle("Framework", self) + local style + if FlagsList:get("FFlagRefactorDevFrameworkTheme") then + style = props.Stylizer + else + style = theme:getStyle("Framework", self) + end local color = style.Color local transparency = style.Transparency @@ -43,7 +51,8 @@ function Box:render() end ContextServices.mapToProps(Box, { - Theme = ContextServices.Theme, + Stylizer = FlagsList:get("FFlagRefactorDevFrameworkTheme") and ContextServices.Stylizer or nil, + Theme = (not FlagsList:get("FFlagRefactorDevFrameworkTheme")) and ContextServices.Theme or nil, }) return Box diff --git a/BuiltInPlugins/PlayerEmulator/Packages/Framework/UI/Box/style.lua b/BuiltInPlugins/PlayerEmulator/Packages/Framework/UI/Box/style.lua index 295019b26c..2f3b83232e 100644 --- a/BuiltInPlugins/PlayerEmulator/Packages/Framework/UI/Box/style.lua +++ b/BuiltInPlugins/PlayerEmulator/Packages/Framework/UI/Box/style.lua @@ -1,20 +1,33 @@ local Framework = script.Parent.Parent.Parent -local Util = require(Framework.Util) -local Style = Util.Style +local StyleKey = require(Framework.Style.StyleKey) local StudioFrameworkStyles = Framework.StudioUI.StudioFrameworkStyles local Common = require(StudioFrameworkStyles.Common) -return function(theme, getColor) - local common = Common(theme, getColor) +local Util = require(Framework.Util) +local Style = Util.Style +local FlagsList = Util.Flags.new({ + FFlagRefactorDevFrameworkTheme = {"RefactorDevFrameworkTheme"}, +}) - local Default = Style.extend(common.Background, { +if FlagsList:get("FFlagRefactorDevFrameworkTheme") then + return { + Color = StyleKey.MainBackground, Transparency = 0, BorderSize = 0, - }) - - return { - Default = Default, } -end +else + return function(theme, getColor) + local common = Common(theme, getColor) + + local Default = Style.extend(common.Background, { + Transparency = 0, + BorderSize = 0, + }) + + return { + Default = Default, + } + end +end \ No newline at end of file diff --git a/BuiltInPlugins/PlayerEmulator/Packages/Framework/UI/Box/test.spec.lua b/BuiltInPlugins/PlayerEmulator/Packages/Framework/UI/Box/test.spec.lua index cbcaf15f51..edc3b7c984 100644 --- a/BuiltInPlugins/PlayerEmulator/Packages/Framework/UI/Box/test.spec.lua +++ b/BuiltInPlugins/PlayerEmulator/Packages/Framework/UI/Box/test.spec.lua @@ -6,16 +6,35 @@ return function() local provide = ContextServices.provide local FrameworkStyles = require(Framework.UI.FrameworkStyles) local Box = require(script.Parent) + local TestHelpers = require(Framework.TestHelpers) + + local StudioTheme = require(Framework.Style.Themes.StudioTheme) + + local Util = require(Framework.Util) + local FlagsList = Util.Flags.new({ + FFlagRefactorDevFrameworkTheme = {"RefactorDevFrameworkTheme"}, + }) local function createTestBoxDecoration() - local theme = Theme.new(function() - return { - Framework = FrameworkStyles.new(), - } - end) - return provide({theme}, { - BoxDecoration = Roact.createElement(Box), - }) + if FlagsList:get("FFlagRefactorDevFrameworkTheme") then + return TestHelpers.provideMockContext(nil, { + BoxDecoration = Roact.createElement(Box), + }) + else + local theme + if FlagsList:get("FFlagRefactorDevFrameworkTheme") then + theme = StudioTheme.mock() + else + theme = Theme.new(function() + return { + Framework = FrameworkStyles.new(), + } + end) + end + return provide({theme}, { + BoxDecoration = Roact.createElement(Box), + }) + end end it("should create and destroy without errors", function() diff --git a/BuiltInPlugins/PlayerEmulator/Packages/Framework/UI/BulletList.lua b/BuiltInPlugins/PlayerEmulator/Packages/Framework/UI/BulletList.lua index 41fca31eb2..72cb7b973e 100644 --- a/BuiltInPlugins/PlayerEmulator/Packages/Framework/UI/BulletList.lua +++ b/BuiltInPlugins/PlayerEmulator/Packages/Framework/UI/BulletList.lua @@ -2,16 +2,16 @@ An array of strings and/or elements displayed as a bulleted list. Required Props: - Theme Theme: A Theme ContextItem, which is provided via mapToProps. array[any] Items: The item to display after each bullet point. Should be an array of strings and/or elements. Strings will be measured to determine the item size. Elements must specify their own size. Optional Props: + Theme Theme: A Theme ContextItem, which is provided via mapToProps. number LayoutOrder: Order in which the element is placed. - Style Style: The style with which to render this component. StyleModifier StyleModifier: The StyleModifier index into Style. boolean TextWrapped: Sets text wrapped. boolean TextTruncate: Sets text truncated. + Stylizer Stylizer: A Stylizer ContextItem, which is provided via mapToProps. Style Values: Enum.Font Font: The font used to render the text. @@ -22,15 +22,19 @@ Color3 TextColor: The color of the text. number TextSize: The size of the text. ]] - local TextService = game:GetService("TextService") local Framework = script.Parent.Parent local Roact = require(Framework.Parent.Roact) local Cryo = require(Framework.Parent.Cryo) local ContextServices = require(Framework.ContextServices) +local Util = require(Framework.Util) local t = require(Framework.Util.Typecheck.t) -local Typecheck = require(Framework.Util).Typecheck +local Typecheck = Util.Typecheck + +local FlagsList = Util.Flags.new({ + FFlagRefactorDevFrameworkTheme = {"RefactorDevFrameworkTheme"}, +}) local BulletList = Roact.PureComponent:extend("BulletList") Typecheck.wrap(BulletList, script) @@ -48,7 +52,13 @@ function BulletList:init() local items = props.Items local theme = props.Theme - local style = theme:getStyle("Framework", self) + local style + if FlagsList:get("FFlagRefactorDevFrameworkTheme") then + style = props.Stylizer + else + style = theme:getStyle("Framework", self) + end + local textSize = style.TextSize local font = style.Font local padding = style.Padding @@ -98,8 +108,12 @@ function BulletList:didUpdate() end function BulletList:calculateItemOffset() - local theme = self.props.Theme - local style = theme:getStyle("Framework", self) + local style + if FlagsList:get("FFlagRefactorDevFrameworkTheme") then + style = self.props.Stylizer + else + style = self.props.Theme:getStyle("Framework", self) + end local itemOffset = style.ItemOffset local markerSize = style.MarkerSize @@ -115,7 +129,12 @@ function BulletList:render() local textTruncate = props.TextTruncate local theme = props.Theme - local style = theme:getStyle("Framework", self) + local style + if FlagsList:get("FFlagRefactorDevFrameworkTheme") then + style = props.Stylizer + else + style = theme:getStyle("Framework", self) + end local font = style.Font local markerImage = style.MarkerImage local markerSize = style.MarkerSize @@ -209,7 +228,8 @@ function BulletList:render() end ContextServices.mapToProps(BulletList, { - Theme = ContextServices.Theme, + Stylizer = FlagsList:get("FFlagRefactorDevFrameworkTheme") and ContextServices.Stylizer or nil, + Theme = (not FlagsList:get("FFlagRefactorDevFrameworkTheme")) and ContextServices.Theme or nil, }) return BulletList diff --git a/BuiltInPlugins/PlayerEmulator/Packages/Framework/UI/BulletList/style.lua b/BuiltInPlugins/PlayerEmulator/Packages/Framework/UI/BulletList/style.lua index 931ce726e0..c1b02cd2cb 100644 --- a/BuiltInPlugins/PlayerEmulator/Packages/Framework/UI/BulletList/style.lua +++ b/BuiltInPlugins/PlayerEmulator/Packages/Framework/UI/BulletList/style.lua @@ -6,17 +6,30 @@ local Style = Util.Style local StudioFrameworkStyles = Framework.StudioUI.StudioFrameworkStyles local Common = require(StudioFrameworkStyles.Common) -return function(theme, getColor) - local common = Common(theme, getColor) +local FlagsList = Util.Flags.new({ + FFlagRefactorDevFrameworkTheme = {"RefactorDevFrameworkTheme"}, +}) - local Default = Style.extend(common.MainText, { +if FlagsList:get("FFlagRefactorDevFrameworkTheme") then + return { ItemOffset = 12, MarkerImage = "rbxasset://textures/StudioSharedUI/dot.png", MarkerSize = 4, Padding = 6, - }) - - return { - Default = Default, } +else + return function(theme, getColor) + local common = Common(theme, getColor) + + local Default = Style.extend(common.MainText, { + ItemOffset = 12, + MarkerImage = "rbxasset://textures/StudioSharedUI/dot.png", + MarkerSize = 4, + Padding = 6, + }) + + return { + Default = Default, + } + end end diff --git a/BuiltInPlugins/PlayerEmulator/Packages/Framework/UI/BulletList/test.spec.lua b/BuiltInPlugins/PlayerEmulator/Packages/Framework/UI/BulletList/test.spec.lua index 9e8c93c941..c8d912bf3a 100644 --- a/BuiltInPlugins/PlayerEmulator/Packages/Framework/UI/BulletList/test.spec.lua +++ b/BuiltInPlugins/PlayerEmulator/Packages/Framework/UI/BulletList/test.spec.lua @@ -31,7 +31,7 @@ return function() Roact.update(instance, createTestBulletList({ Items = {"one", "two", "three", "four"} - }, container)) + })) local size2 = container:FindFirstChild("BulletList", true).Size @@ -51,7 +51,7 @@ return function() Roact.update(instance, createTestBulletList({ Items = {"one", "two"} - }, container)) + })) local size2 = container:FindFirstChild("BulletList", true).Size @@ -86,4 +86,4 @@ return function() Roact.unmount(instance) end) end) -end \ No newline at end of file +end diff --git a/BuiltInPlugins/PlayerEmulator/Packages/Framework/UI/Button.lua b/BuiltInPlugins/PlayerEmulator/Packages/Framework/UI/Button.lua index c99a7396cc..13a5c5fcf2 100644 --- a/BuiltInPlugins/PlayerEmulator/Packages/Framework/UI/Button.lua +++ b/BuiltInPlugins/PlayerEmulator/Packages/Framework/UI/Button.lua @@ -4,7 +4,6 @@ Required Props: callback OnClick: A callback for when the user clicks this button. - Theme Theme: A Theme ContextItem, which is provided via mapToProps. Optional Props: string Text: The text to display in this button. @@ -15,6 +14,8 @@ Vector2 AnchorPoint: The pivot point of this component's Position prop. number ZIndex: The render index of this component. number LayoutOrder: The layout order of this component in a list. + Stylizer Stylizer: A Stylizer ContextItem, which is provided via mapToProps. + Theme Theme: A Theme ContextItem, which is provided via mapToProps. Style Values: Component Background: The background to render for this Button. @@ -39,6 +40,10 @@ local Util = require(Framework.Util) local StyleModifier = Util.StyleModifier local Typecheck = Util.Typecheck +local FlagsList = Util.Flags.new({ + FFlagRefactorDevFrameworkTheme = {"RefactorDevFrameworkTheme"}, +}) + local Button = Roact.PureComponent:extend("Button") Typecheck.wrap(Button, script) @@ -66,7 +71,12 @@ function Button:render() local theme = props.Theme local styleModifier = props.StyleModifier or state.StyleModifier - local style = theme:getStyle("Framework", self) + local style + if FlagsList:get("FFlagRefactorDevFrameworkTheme") then + style = props.Stylizer + else + style = theme:getStyle("Framework", self) + end local background = style.Background local backgroundStyle = style.BackgroundStyle local foreground = style.Foreground @@ -124,7 +134,8 @@ function Button:render() end ContextServices.mapToProps(Button, { - Theme = ContextServices.Theme, + Stylizer = FlagsList:get("FFlagRefactorDevFrameworkTheme") and ContextServices.Stylizer or nil, + Theme = (not FlagsList:get("FFlagRefactorDevFrameworkTheme")) and ContextServices.Theme or nil, }) return Button diff --git a/BuiltInPlugins/PlayerEmulator/Packages/Framework/UI/Button/example.lua b/BuiltInPlugins/PlayerEmulator/Packages/Framework/UI/Button/example.lua index a59c7667c8..e077e09ef3 100644 --- a/BuiltInPlugins/PlayerEmulator/Packages/Framework/UI/Button/example.lua +++ b/BuiltInPlugins/PlayerEmulator/Packages/Framework/UI/Button/example.lua @@ -17,58 +17,99 @@ return function(plugin) local HoverArea = UI.HoverArea local Util = require(Framework.Util) + local Cryo = Util.Cryo local StyleTable = Util.StyleTable local Style = Util.Style local StyleModifier = Util.StyleModifier + local FlagsList = Util.Flags.new({ + FFlagRefactorDevFrameworkTheme = {"RefactorDevFrameworkTheme"}, + }) + + local FrameworkStyle = Framework.Style + local ui = require(FrameworkStyle).ComponentSymbols + local StudioTheme = require(FrameworkStyle.Themes.StudioTheme) + local BaseTheme = require(FrameworkStyle.Themes.BaseTheme) + local StyleKey = require(FrameworkStyle.StyleKey) local pluginItem = Plugin.new(plugin) local mouse = Mouse.new(plugin:GetMouse()) - local theme = Theme.new(function(theme, getColor) - local studioStyles = StudioFrameworkStyles.new(theme, getColor) + local theme + if FlagsList:get("FFlagRefactorDevFrameworkTheme") then + theme = StudioTheme.new() + theme:extend({ + [ui.Button] = Cryo.Dictionary.join(BaseTheme[ui.Button], { + ["&Close"] = { + Foreground = Decoration.Image, + ForegroundStyle = { + Image = "rbxasset://textures/ui/CloseButton.png", + }, + [StyleModifier.Hover] = { + ForegroundStyle = { + Image = "rbxasset://textures/ui/CloseButton_dn.png", + }, + }, + }, + }), + + [ui.Image] = Cryo.Dictionary.join(BaseTheme[ui.Image], { + ["&Settings"] = { + Image = "rbxasset://textures/AnimationEditor/btn_manage.png", + Color = StyleKey.MainText, + }, - local button = StyleTable.new("Button", function() - -- Defining a new button style that uses images - local Close = Style.new({ - Foreground = Decoration.Image, - ForegroundStyle = { - Image = "rbxasset://textures/ui/CloseButton.png", + ["&SettingsPrimary"] = { + Color = StyleKey.DialogMainButtonText, }, - [StyleModifier.Hover] = { + }), + }) + else + theme = Theme.new(function(theme, getColor) + local studioStyles = StudioFrameworkStyles.new(theme, getColor) + + local button = StyleTable.new("Button", function() + -- Defining a new button style that uses images + local Close = Style.new({ + Foreground = Decoration.Image, ForegroundStyle = { - Image = "rbxasset://textures/ui/CloseButton_dn.png", + Image = "rbxasset://textures/ui/CloseButton.png", }, - }, - }) + [StyleModifier.Hover] = { + ForegroundStyle = { + Image = "rbxasset://textures/ui/CloseButton_dn.png", + }, + }, + }) - return { - Close = Close, - } - end) + return { + Close = Close, + } + end) - local image = StyleTable.new("Image", function() - local Settings = Style.extend(studioStyles.Image.Default, { - Image = "rbxasset://textures/AnimationEditor/btn_manage.png", - Color = theme:GetColor("MainText"), - }) + local image = StyleTable.new("Image", function() + local Settings = Style.extend(studioStyles.Image.Default, { + Image = "rbxasset://textures/AnimationEditor/btn_manage.png", + Color = theme:GetColor("MainText"), + }) - local SettingsPrimary = Style.extend(Settings, { - Color = theme:GetColor("DialogMainButtonText"), - }) + local SettingsPrimary = Style.extend(Settings, { + Color = theme:GetColor("DialogMainButtonText"), + }) + + return { + Settings = Settings, + SettingsPrimary = SettingsPrimary, + } + end) return { - Settings = Settings, - SettingsPrimary = SettingsPrimary, + Framework = StyleTable.extend(studioStyles, { + Button = button, + Image = image, + }) } end) - - return { - Framework = StyleTable.extend(studioStyles, { - Button = button, - Image = image, - }) - } - end) + end -- Mount and display a dialog local ExampleButtons = Roact.PureComponent:extend("ExampleButtons") diff --git a/BuiltInPlugins/PlayerEmulator/Packages/Framework/UI/Button/style.lua b/BuiltInPlugins/PlayerEmulator/Packages/Framework/UI/Button/style.lua index d957ec285b..b371b39e99 100644 --- a/BuiltInPlugins/PlayerEmulator/Packages/Framework/UI/Button/style.lua +++ b/BuiltInPlugins/PlayerEmulator/Packages/Framework/UI/Button/style.lua @@ -1,74 +1,136 @@ local Framework = script.Parent.Parent.Parent local Util = require(Framework.Util) +local Cryo = Util.Cryo +local deepCopy = Util.deepCopy local Style = Util.Style local StyleModifier = Util.StyleModifier +local StyleKey = require(Framework.Style.StyleKey) + local UI = require(Framework.UI) local Decoration = UI.Decoration +local FlagsList = Util.Flags.new({ + FFlagRefactorDevFrameworkTheme = {"RefactorDevFrameworkTheme"}, +}) + local StudioFrameworkStyles = Framework.StudioUI.StudioFrameworkStyles local Common = require(StudioFrameworkStyles.Common) local UIFolderData = require(Framework.UI.UIFolderData) local RoundBox = require(UIFolderData.RoundBox.style) -return function(theme, getColor) - local common = Common(theme, getColor) - local roundBox = RoundBox(theme, getColor) - - local Default = Style.extend(common.MainText, { +if FlagsList:get("FFlagRefactorDevFrameworkTheme") then + local roundBox = deepCopy(RoundBox) + return { Padding = 0, TextXAlignment = Enum.TextXAlignment.Center, TextYAlignment = Enum.TextYAlignment.Center, - TextColor = theme:GetColor("ButtonText"), + TextColor = StyleKey.ButtonText, Background = Decoration.Box, - BackgroundStyle = Style.extend(roundBox.Default, { - Color = theme:GetColor("Button"), + BackgroundStyle = Cryo.Dictionary.join(roundBox, { + Color = StyleKey.Button, }), + [StyleModifier.Hover] = { - BackgroundStyle = { - Color = theme:GetColor("Button", "Hover"), - }, + BackgroundStyle = Cryo.Dictionary.join(roundBox, { + Color = StyleKey.ButtonHover, + }), }, [StyleModifier.Disabled] = { BackgroundStyle = { - Color = theme:GetColor("Button", "Disabled"), + Color = StyleKey.ButtonDisabled, }, - TextColor = theme:GetColor("ButtonText", "Disabled"), + TextColor = StyleKey.ButtonDisabled, }, [StyleModifier.Pressed] = { BackgroundStyle = { - Color = theme:GetColor("Button", "Pressed"), + Color = StyleKey.ButtonHover, }, }, - }) - local Round = Style.extend(Default, { - Background = Decoration.RoundBox, - }) + ["&Round"] = { + Background = Decoration.RoundBox, + }, - local RoundPrimary = Style.extend(Round, { - TextColor = theme:GetColor("DialogMainButtonText"), - BackgroundStyle = Style.extend(roundBox.Default, { - Color = theme:GetColor("DialogMainButton"), - }), - [StyleModifier.Hover] = { - BackgroundStyle = { - Color = theme:GetColor("DialogMainButton", "Hover"), + ["&RoundPrimary"] = { + Background = Decoration.RoundBox, + BackgroundStyle = Cryo.Dictionary.join(roundBox, { + Color = StyleKey.DialogMainButton, + }), + TextColor = StyleKey.DialogMainButtonText, + [StyleModifier.Hover] = { + BackgroundStyle = { + Color = StyleKey.DialogMainButtonHover, + }, }, - }, - [StyleModifier.Disabled] = { - BackgroundStyle = { - Color = theme:GetColor("DialogMainButton", "Disabled"), + [StyleModifier.Disabled] = { + BackgroundStyle = { + Color = StyleKey.DialogMainButtonDisabled, + }, + TextColor = StyleKey.DialogMainButtonTextDisabled, }, - TextColor = theme:GetColor("DialogMainButtonText", "Disabled"), }, - }) - - return { - Default = Default, - Round = Round, - RoundPrimary = RoundPrimary, } -end +else + return function(theme, getColor) + local common = Common(theme, getColor) + local roundBox = RoundBox(theme, getColor) + + local Default = Style.extend(common.MainText, { + Padding = 0, + TextXAlignment = Enum.TextXAlignment.Center, + TextYAlignment = Enum.TextYAlignment.Center, + TextColor = theme:GetColor("ButtonText"), + Background = Decoration.Box, + BackgroundStyle = Style.extend(roundBox.Default, { + Color = theme:GetColor("Button"), + }), + [StyleModifier.Hover] = { + BackgroundStyle = { + Color = theme:GetColor("Button", "Hover"), + }, + }, + [StyleModifier.Disabled] = { + BackgroundStyle = { + Color = theme:GetColor("Button", "Disabled"), + }, + TextColor = theme:GetColor("ButtonText", "Disabled"), + }, + [StyleModifier.Pressed] = { + BackgroundStyle = { + Color = theme:GetColor("Button", "Pressed"), + }, + }, + }) + + local Round = Style.extend(Default, { + Background = Decoration.RoundBox, + }) + + local RoundPrimary = Style.extend(Round, { + TextColor = theme:GetColor("DialogMainButtonText"), + BackgroundStyle = Style.extend(roundBox.Default, { + Color = theme:GetColor("DialogMainButton"), + }), + [StyleModifier.Hover] = { + BackgroundStyle = { + Color = theme:GetColor("DialogMainButton", "Hover"), + }, + }, + [StyleModifier.Disabled] = { + BackgroundStyle = { + Color = theme:GetColor("DialogMainButton", "Disabled"), + }, + TextColor = theme:GetColor("DialogMainButtonText", "Disabled"), + }, + }) + + return { + Default = Default, + Round = Round, + RoundPrimary = RoundPrimary, + } + end +end \ No newline at end of file diff --git a/BuiltInPlugins/PlayerEmulator/Packages/Framework/UI/Button/test.spec.lua b/BuiltInPlugins/PlayerEmulator/Packages/Framework/UI/Button/test.spec.lua index a44d2547c4..cafabfaa40 100644 --- a/BuiltInPlugins/PlayerEmulator/Packages/Framework/UI/Button/test.spec.lua +++ b/BuiltInPlugins/PlayerEmulator/Packages/Framework/UI/Button/test.spec.lua @@ -8,12 +8,24 @@ return function() local Button = require(script.Parent) local Box = require(Framework.UI.Box) + local StudioTheme = require(Framework.Style.Themes.StudioTheme) + + local Util = require(Framework.Util) + local FlagsList = Util.Flags.new({ + FFlagRefactorDevFrameworkTheme = {"RefactorDevFrameworkTheme"}, + }) + local function createTestButton(props, children) - local theme = Theme.new(function() - return { - Framework = FrameworkStyles.new(), - } - end) + local theme + if FlagsList:get("FFlagRefactorDevFrameworkTheme") then + theme = StudioTheme.mock() + else + theme = Theme.new(function() + return { + Framework = FrameworkStyles.new(), + } + end) + end return provide({theme}, { Button = Roact.createElement(Button, props, children), }) diff --git a/BuiltInPlugins/PlayerEmulator/Packages/Framework/UI/Container/example.lua b/BuiltInPlugins/PlayerEmulator/Packages/Framework/UI/Container/example.lua index 6ffd2d3ef8..6741545ce5 100644 --- a/BuiltInPlugins/PlayerEmulator/Packages/Framework/UI/Container/example.lua +++ b/BuiltInPlugins/PlayerEmulator/Packages/Framework/UI/Container/example.lua @@ -11,47 +11,81 @@ return function(plugin) local Container = UI.Container local Decoration = UI.Decoration + local FrameworkStyle = Framework.Style + local ui = require(FrameworkStyle).ComponentSymbols + local StudioTheme = require(FrameworkStyle.Themes.StudioTheme) + local BaseTheme = require(FrameworkStyle.Themes.BaseTheme) + local Util = require(Framework.Util) + local Cryo = Util.Cryo local StyleTable = Util.StyleTable local Style = Util.Style + local FlagsList = Util.Flags.new({ + FFlagRefactorDevFrameworkTheme = {"RefactorDevFrameworkTheme"}, + }) local pluginItem = Plugin.new(plugin) - local theme = Theme.new(function(theme, getColor) - local box = StyleTable.new("Box", function() - local Black = Style.new({ - Color = Color3.new(0, 0, 0), + local theme + if FlagsList:get("FFlagRefactorDevFrameworkTheme") then + theme = StudioTheme.new() + theme:extend({ + [ui.Box] = Cryo.Dictionary.join(BaseTheme[ui.Box], { Transparency = 0, BorderSize = 0, - }) - local Red = Style.extend(Black, { - Color = Color3.new(0.3, 0, 0), - }) + ["&Black"] = { + Color = Color3.new(0, 0, 0), + }, - return { - Black = Black, - Red = Red, - } - end) + ["&Red"] = { + Color = Color3.new(0.3, 0, 0), + }, + }), - local image = StyleTable.new("Image", function() - local WarningIcon = Style.new({ - Image = "rbxasset://textures/ui/ErrorIcon.png", - }) + [ui.Image] = Cryo.Dictionary.join(BaseTheme[ui.Image], { + ["&WarningIcon"] = { + Image = "rbxasset://textures/ui/ErrorIcon.png", + }, + }), + }) + else + theme = Theme.new(function(theme, getColor) + local box = StyleTable.new("Box", function() + local Black = Style.new({ + Color = Color3.new(0, 0, 0), + Transparency = 0, + BorderSize = 0, + }) + + local Red = Style.extend(Black, { + Color = Color3.new(0.3, 0, 0), + }) + + return { + Black = Black, + Red = Red, + } + end) + + local image = StyleTable.new("Image", function() + local WarningIcon = Style.new({ + Image = "rbxasset://textures/ui/ErrorIcon.png", + }) + + return { + WarningIcon = WarningIcon, + } + end) return { - WarningIcon = WarningIcon, + Framework = StyleTable.extend(FrameworkStyles.new(), { + Box = box, + Image = image, + }) } end) - - return { - Framework = StyleTable.extend(FrameworkStyles.new(), { - Box = box, - Image = image, - }) - } - end) + end -- Mount and display a dialog local ExampleContainer = Roact.PureComponent:extend("ExampleContainer") diff --git a/BuiltInPlugins/PlayerEmulator/Packages/Framework/UI/Container/test.spec.lua b/BuiltInPlugins/PlayerEmulator/Packages/Framework/UI/Container/test.spec.lua index 77f5d1aacc..1417100db0 100644 --- a/BuiltInPlugins/PlayerEmulator/Packages/Framework/UI/Container/test.spec.lua +++ b/BuiltInPlugins/PlayerEmulator/Packages/Framework/UI/Container/test.spec.lua @@ -8,12 +8,24 @@ return function() local Container = require(script.Parent) local Box = require(Framework.UI.Box) + local StudioTheme = require(Framework.Style.Themes.StudioTheme) + + local Util = require(Framework.Util) + local FlagsList = Util.Flags.new({ + FFlagRefactorDevFrameworkTheme = {"RefactorDevFrameworkTheme"}, + }) + local function createTestContainer(props, children) - local theme = Theme.new(function() - return { - Framework = FrameworkStyles.new(), - } - end) + local theme + if FlagsList:get("FFlagRefactorDevFrameworkTheme") then + theme = StudioTheme.mock() + else + theme = Theme.new(function() + return { + Framework = FrameworkStyles.new(), + } + end) + end return provide({theme}, { Container = Roact.createElement(Container, props, children), }) diff --git a/BuiltInPlugins/PlayerEmulator/Packages/Framework/UI/DropShadow.lua b/BuiltInPlugins/PlayerEmulator/Packages/Framework/UI/DropShadow.lua index e5e9ec09e8..d9815b84a1 100644 --- a/BuiltInPlugins/PlayerEmulator/Packages/Framework/UI/DropShadow.lua +++ b/BuiltInPlugins/PlayerEmulator/Packages/Framework/UI/DropShadow.lua @@ -2,13 +2,12 @@ A rectangular drop shadow that appears at the edges of an element. The children of the DropShadow appear within padding equal to the value of Radius. - Required Props: - Theme Theme: A Theme ContextItem, which is provided via mapToProps. - Optional Props: Style Style: The style with which to render this component. StyleModifier StyleModifier: The StyleModifier index into Style. number ZIndex: The render index of the shadow - should be behind the element it shadows. + Stylizer Stylizer: A Stylizer ContextItem, which is provided via mapToProps. + Theme Theme: A Theme ContextItem, which is provided via mapToProps. Style Values: Color3 Color: The color of the shadow. @@ -19,12 +18,16 @@ number Radius: The radius of the shadow, in pixels. number Transparency: The transparency of the shadow (ranges from 0 to 1). ]] - local Framework = script.Parent.Parent local Roact = require(Framework.Parent.Roact) local ContextServices = require(Framework.ContextServices) +local Util = require(Framework.Util) local t = require(Framework.Util.Typecheck.t) -local Typecheck = require(Framework.Util).Typecheck +local Typecheck = Util.Typecheck + +local FlagsList = Util.Flags.new({ + FFlagRefactorDevFrameworkTheme = {"RefactorDevFrameworkTheme"}, +}) local DropShadow = Roact.PureComponent:extend("DropShadow") Typecheck.wrap(DropShadow, script) @@ -34,7 +37,12 @@ function DropShadow:render() local zIndex = props.ZIndex local theme = props.Theme - local style = theme:getStyle("Framework", self) + local style + if FlagsList:get("FFlagRefactorDevFrameworkTheme") then + style = props.Stylizer + else + style = theme:getStyle("Framework", self) + end local color = style.Color local offset = style.Offset or Vector2.new() @@ -80,7 +88,8 @@ function DropShadow:render() end ContextServices.mapToProps(DropShadow, { - Theme = ContextServices.Theme, + Stylizer = FlagsList:get("FFlagRefactorDevFrameworkTheme") and ContextServices.Stylizer or nil, + Theme = (not FlagsList:get("FFlagRefactorDevFrameworkTheme")) and ContextServices.Theme or nil, }) return DropShadow diff --git a/BuiltInPlugins/PlayerEmulator/Packages/Framework/UI/DropShadow/style.lua b/BuiltInPlugins/PlayerEmulator/Packages/Framework/UI/DropShadow/style.lua index 4280b47efd..0caf0a4aad 100644 --- a/BuiltInPlugins/PlayerEmulator/Packages/Framework/UI/DropShadow/style.lua +++ b/BuiltInPlugins/PlayerEmulator/Packages/Framework/UI/DropShadow/style.lua @@ -3,18 +3,35 @@ local Framework = script.Parent.Parent.Parent local Util = require(Framework.Util) local Style = Util.Style -return function(theme, getColor) +local StyleKey = require(Framework.Style.StyleKey) - local Default = Style.new({ - Color = theme:GetColor("Border"), +local FlagsList = Util.Flags.new({ + FFlagRefactorDevFrameworkTheme = {"RefactorDevFrameworkTheme"}, +}) + +if FlagsList:get("FFlagRefactorDevFrameworkTheme") then + return { + Color = StyleKey.Border, Image = "rbxasset://textures/StudioSharedUI/dropShadow.png", ImageSize = 16, Offset = Vector2.new(), Radius = 6, - Transparency = 0 - }) - - return { - Default = Default, + Transparency = 0, } -end +else + return function(theme, getColor) + + local Default = Style.new({ + Color = theme:GetColor("Border"), + Image = "rbxasset://textures/StudioSharedUI/dropShadow.png", + ImageSize = 16, + Offset = Vector2.new(), + Radius = 6, + Transparency = 0 + }) + + return { + Default = Default, + } + end +end \ No newline at end of file diff --git a/BuiltInPlugins/PlayerEmulator/Packages/Framework/UI/DropdownMenu.lua b/BuiltInPlugins/PlayerEmulator/Packages/Framework/UI/DropdownMenu.lua index ce29ef7881..b1e6b3c757 100644 --- a/BuiltInPlugins/PlayerEmulator/Packages/Framework/UI/DropdownMenu.lua +++ b/BuiltInPlugins/PlayerEmulator/Packages/Framework/UI/DropdownMenu.lua @@ -8,14 +8,15 @@ boolean Hide: Whether the menu is hidden table Items: An array of each item that should appear in the dropdown. callback OnItemActivated: A callback for when the user selects a dropdown entry. - Theme Theme: a Theme object supplied by mapToProps() Focus Focus: a Focus object supplied by mapToProps() Optional Props: + Theme Theme: a Theme object supplied by mapToProps() string PlaceholderText: A placeholder to display if there is no item selected. callback OnRenderItem: A function used to render a dropdown menu item. callback OnFocusLost: A function called when the focus on the menu is lost. number SelectedIndex: The currently selected item index. + Stylizer Stylizer: A Stylizer ContextItem, which is provided via mapToProps. Style Style: The style with which to render this component. Style Values: @@ -25,13 +26,14 @@ number Width: The width of the menu area. number MaxHeight: The maximum height of the menu area. ]] - local Framework = script.Parent.Parent local Roact = require(Framework.Parent.Roact) local ContextServices = require(Framework.ContextServices) + local Util = require(Framework.Util) local prioritize = Util.prioritize local Typecheck = Util.Typecheck + local UI = Framework.UI local Container = require(UI.Container) local CaptureFocus = require(UI.CaptureFocus) @@ -40,6 +42,11 @@ local Button = require(UI.Button) local RoundBox = require(UI.RoundBox) local TextLabel = require(UI.TextLabel) +local FlagsList = Util.Flags.new({ + FFlagRefactorDevFrameworkTheme = {"RefactorDevFrameworkTheme"}, + FFlagRefactorDevFrameworkContextItems = {"RefactorDevFrameworkContextItems"}, +}) + local DropdownMenu = Roact.PureComponent:extend("DropdownMenu") Typecheck.wrap(DropdownMenu, script) @@ -86,7 +93,12 @@ function DropdownMenu:init() -- calculate the size and position of the dropdown local height = state.menuContentSize.Y - local style = props.Theme:getStyle("Framework", self) + local style + if FlagsList:get("FFlagRefactorDevFrameworkTheme") then + style = props.Stylizer + else + style = props.Theme:getStyle("Framework", self) + end local maxHeight = style.MaxHeight local sourcePosition = state.absolutePosition @@ -168,10 +180,14 @@ local function defaultOnRenderItem(item, index, activated) end function DropdownMenu:renderMenu() - local state = self.state local props = self.props - local style = props.Theme:getStyle("Framework", self) + local style + if FlagsList:get("FFlagRefactorDevFrameworkTheme") then + style = props.Stylizer + else + style = props.Theme:getStyle("Framework", self) + end local items = props.Items local onRenderItem = prioritize(props.OnRenderItem, defaultOnRenderItem) @@ -186,7 +202,7 @@ function DropdownMenu:renderMenu() local width = style.Width local offset = prioritize(style.Offset, Vector2.new(0, 0)) - local pluginGui = props.Focus:getTarget() + local pluginGui = FlagsList:get("FFlagRefactorDevFrameworkContextItems") and props.Focus:get() or props.Focus:getTarget() local menuPositionAndSize = self.getPositionAndSize(pluginGui, width, offset) local x = menuPositionAndSize.X @@ -257,7 +273,8 @@ end ContextServices.mapToProps(DropdownMenu, { Focus = ContextServices.Focus, - Theme = ContextServices.Theme, + Stylizer = FlagsList:get("FFlagRefactorDevFrameworkTheme") and ContextServices.Stylizer or nil, + Theme = (not FlagsList:get("FFlagRefactorDevFrameworkTheme")) and ContextServices.Theme or nil, }) return DropdownMenu diff --git a/BuiltInPlugins/PlayerEmulator/Packages/Framework/UI/DropdownMenu/style.lua b/BuiltInPlugins/PlayerEmulator/Packages/Framework/UI/DropdownMenu/style.lua index 96e8578c53..1b189d6ba8 100644 --- a/BuiltInPlugins/PlayerEmulator/Packages/Framework/UI/DropdownMenu/style.lua +++ b/BuiltInPlugins/PlayerEmulator/Packages/Framework/UI/DropdownMenu/style.lua @@ -3,24 +3,47 @@ local Framework = script.Parent.Parent.Parent local StudioFrameworkStyles = Framework.StudioUI.StudioFrameworkStyles local Common = require(StudioFrameworkStyles.Common) local UIFolderData = require(Framework.UI.UIFolderData) -local Util = require(Framework.Util) local RoundBox = require(UIFolderData.RoundBox.style) + +local Util = require(Framework.Util) +local deepCopy = Util.deepCopy local Style = Util.Style -return function(theme, getColor) - local common = Common(theme, getColor) - local roundBox = RoundBox(theme, getColor) +local StyleKey = require(Framework.Style.StyleKey) +local FlagsList = Util.Flags.new({ + FFlagRefactorDevFrameworkTheme = {"RefactorDevFrameworkTheme"}, +}) + +if FlagsList:get("FFlagRefactorDevFrameworkTheme") then + local roundBox = deepCopy(RoundBox) return { - Default = Style.extend(common.Border, { - BackgroundStyle = roundBox.Default, - Width = 240, - MaxHeight = 240, - Offset = Vector2.new(0, 0), - Text = Style.extend(common.MainText, { - TextXAlignment = Enum.TextXAlignment.Left, - }) - }) + BackgroundStyle = roundBox, + BorderColor = StyleKey.Border, + Width = 240, + MaxHeight = 240, + Offset = Vector2.new(0, 0), + Text = { + TextSize = 18, + TextXAlignment = Enum.TextXAlignment.Left, + }, } +else + return function(theme, getColor) + local common = Common(theme, getColor) + local roundBox = RoundBox(theme, getColor) + return { + Default = Style.extend(common.Border, { + BackgroundStyle = roundBox.Default, + Width = 240, + MaxHeight = 240, + Offset = Vector2.new(0, 0), + Text = { + TextSize = 18, + TextXAlignment = Enum.TextXAlignment.Left, + }, + }) + } + end end \ No newline at end of file diff --git a/BuiltInPlugins/PlayerEmulator/Packages/Framework/UI/FakeLoadingBar/test.spec.lua b/BuiltInPlugins/PlayerEmulator/Packages/Framework/UI/FakeLoadingBar/test.spec.lua index bea92510ff..1306596adc 100644 --- a/BuiltInPlugins/PlayerEmulator/Packages/Framework/UI/FakeLoadingBar/test.spec.lua +++ b/BuiltInPlugins/PlayerEmulator/Packages/Framework/UI/FakeLoadingBar/test.spec.lua @@ -7,12 +7,24 @@ return function() local FrameworkStyles = require(Framework.UI.FrameworkStyles) local FakeLoadingBar = require(script.Parent) + local StudioTheme = require(Framework.Style.Themes.StudioTheme) + + local Util = require(Framework.Util) + local FlagsList = Util.Flags.new({ + FFlagRefactorDevFrameworkTheme = {"RefactorDevFrameworkTheme"}, + }) + local function createTestFakeLoadingBar(props, children) - local theme = Theme.new(function() - return { - Framework = FrameworkStyles.new(), - } - end) + local theme + if FlagsList:get("FFlagRefactorDevFrameworkTheme") then + theme = StudioTheme.mock() + else + theme = Theme.new(function() + return { + Framework = FrameworkStyles.new(), + } + end) + end return provide({theme}, { FakeLoadingBar = Roact.createElement(FakeLoadingBar, props, children), }) diff --git a/BuiltInPlugins/PlayerEmulator/Packages/Framework/UI/FrameworkStyles.spec.lua b/BuiltInPlugins/PlayerEmulator/Packages/Framework/UI/FrameworkStyles.spec.lua index ed89aed938..7986ecd5a5 100644 --- a/BuiltInPlugins/PlayerEmulator/Packages/Framework/UI/FrameworkStyles.spec.lua +++ b/BuiltInPlugins/PlayerEmulator/Packages/Framework/UI/FrameworkStyles.spec.lua @@ -9,8 +9,8 @@ return function() for _, entry in pairs(styles) do expect(entry.Default).to.be.ok() - expect(next(entry.Default)).never.to.be.ok() + expect((next(entry.Default))).never.to.be.ok() end end) end) -end \ No newline at end of file +end diff --git a/BuiltInPlugins/PlayerEmulator/Packages/Framework/UI/Image.lua b/BuiltInPlugins/PlayerEmulator/Packages/Framework/UI/Image.lua index dd0635a462..4a602ce8af 100644 --- a/BuiltInPlugins/PlayerEmulator/Packages/Framework/UI/Image.lua +++ b/BuiltInPlugins/PlayerEmulator/Packages/Framework/UI/Image.lua @@ -1,12 +1,11 @@ --[[ A Decoration image. - Required Props: - Theme Theme: A Theme ContextItem, which is provided via mapToProps. - Optional Props: + Theme Theme: A Theme ContextItem, which is provided via mapToProps. Style Style: The style with which to render this component. StyleModifier StyleModifier: The StyleModifier index into Style. + Stylizer Stylizer: A Stylizer ContextItem, which is provided via mapToProps. Style Values: Vector2 AnchorPoint: The anchor point of the image. @@ -26,6 +25,11 @@ local Framework = script.Parent.Parent local Roact = require(Framework.Parent.Roact) local ContextServices = require(Framework.ContextServices) local Typecheck = require(Framework.Util).Typecheck +local Util = require(Framework.Util) + +local FlagsList = Util.Flags.new({ + FFlagRefactorDevFrameworkTheme = {"RefactorDevFrameworkTheme"}, +}) local Image = Roact.PureComponent:extend("Image") Typecheck.wrap(Image, script) @@ -33,7 +37,12 @@ Typecheck.wrap(Image, script) function Image:render() local props = self.props local theme = props.Theme - local style = theme:getStyle("Framework", self) + local style + if FlagsList:get("FFlagRefactorDevFrameworkTheme") then + style = props.Stylizer + else + style = theme:getStyle("Framework", self) + end local color = style.Color local transparency = style.Transparency @@ -65,7 +74,8 @@ function Image:render() end ContextServices.mapToProps(Image, { - Theme = ContextServices.Theme, + Stylizer = FlagsList:get("FFlagRefactorDevFrameworkTheme") and ContextServices.Stylizer or nil, + Theme = (not FlagsList:get("FFlagRefactorDevFrameworkTheme")) and ContextServices.Theme or nil, }) return Image diff --git a/BuiltInPlugins/PlayerEmulator/Packages/Framework/UI/Image/style.lua b/BuiltInPlugins/PlayerEmulator/Packages/Framework/UI/Image/style.lua index 8f8ad3191f..a100295d01 100644 --- a/BuiltInPlugins/PlayerEmulator/Packages/Framework/UI/Image/style.lua +++ b/BuiltInPlugins/PlayerEmulator/Packages/Framework/UI/Image/style.lua @@ -3,12 +3,22 @@ local Framework = script.Parent.Parent.Parent local Util = require(Framework.Util) local Style = Util.Style -return function(theme, getColor) - local Default = Style.new({ - Color = Color3.new(1, 1, 1), -- Full white so image is uncolored - }) +local FlagsList = Util.Flags.new({ + FFlagRefactorDevFrameworkTheme = {"RefactorDevFrameworkTheme"}, +}) +if FlagsList:get("FFlagRefactorDevFrameworkTheme") then return { - Default = Default, + Color = Color3.new(1, 1, 1), -- Full white so image is uncolored } -end +else + return function(theme, getColor) + local Default = Style.new({ + Color = Color3.new(1, 1, 1), -- Full white so image is uncolored + }) + + return { + Default = Default, + } + end +end \ No newline at end of file diff --git a/BuiltInPlugins/PlayerEmulator/Packages/Framework/UI/Image/test.spec.lua b/BuiltInPlugins/PlayerEmulator/Packages/Framework/UI/Image/test.spec.lua index a8fd8f1c5f..6860be79dd 100644 --- a/BuiltInPlugins/PlayerEmulator/Packages/Framework/UI/Image/test.spec.lua +++ b/BuiltInPlugins/PlayerEmulator/Packages/Framework/UI/Image/test.spec.lua @@ -7,12 +7,24 @@ return function() local FrameworkStyles = require(Framework.UI.FrameworkStyles) local Image = require(script.Parent) + local StudioTheme = require(Framework.Style.Themes.StudioTheme) + + local Util = require(Framework.Util) + local FlagsList = Util.Flags.new({ + FFlagRefactorDevFrameworkTheme = {"RefactorDevFrameworkTheme"}, + }) + local function createTestImageDecoration() - local theme = Theme.new(function() - return { - Framework = FrameworkStyles.new(), - } - end) + local theme + if FlagsList:get("FFlagRefactorDevFrameworkTheme") then + theme = StudioTheme.mock() + else + theme = Theme.new(function() + return { + Framework = FrameworkStyles.new(), + } + end) + end return provide({theme}, { Image = Roact.createElement(Image), }) diff --git a/BuiltInPlugins/PlayerEmulator/Packages/Framework/UI/InfiniteScrollingFrame.lua b/BuiltInPlugins/PlayerEmulator/Packages/Framework/UI/InfiniteScrollingFrame.lua index fa700bd024..8e3a1d96cb 100644 --- a/BuiltInPlugins/PlayerEmulator/Packages/Framework/UI/InfiniteScrollingFrame.lua +++ b/BuiltInPlugins/PlayerEmulator/Packages/Framework/UI/InfiniteScrollingFrame.lua @@ -3,13 +3,14 @@ A thin wrapper around the infinite-scroller library with DeveloperFramework theming and naming conventions. Required Props: - Theme Theme: the theme supplied from mapToProps() array[any] Items: The items to scroll through. callback RenderItem: Callback to render each item that should be visible. The items should have LayoutOrder set. Optional Props: + Theme Theme: the theme supplied from mapToProps() Style Style: a style table supplied from props and theme:getStyle() + Stylizer Stylizer: A Stylizer ContextItem, which is provided via mapToProps. UDim2 Position: The position of the scrolling frame. UDim2 Size: The size of the scrolling frame. integer LayoutOrder: The order this component will display in a UILayout. @@ -35,11 +36,11 @@ integer ScrollBarThickness: The horizontal width of the scrollbar. boolean ScrollingEnabled: Whether scrolling in this frame will change the CanvasPosition. ]] - local Framework = script.Parent.Parent local Util = require(Framework.Util) local FlagsList = Util.Flags.new({ FFlagStudioDevFrameworkPackage = {"StudioDevFrameworkPackage"}, + FFlagRefactorDevFrameworkTheme = {"RefactorDevFrameworkTheme"}, }) local Roact = require(Framework.Parent.Roact) @@ -95,9 +96,17 @@ function InfiniteScrollingFrame:init() self.getInfiniteScrollingFrameProps = function(props, style) -- After filtering out parent's props and DeveloperFramework-specific props (such as Style and Theme), -- what is left are infinite-scroller props + local updatedProps + if FlagsList:get("FFlagRefactorDevFrameworkTheme") then + updatedProps = Cryo.Dictionary.join(props, { + Stylizer = Cryo.None + }) + else + updatedProps = props + end return Cryo.Dictionary.join( style, - props, + updatedProps, self.propFilters.containerProps, self.propFilters.wrapperProps, { @@ -128,7 +137,13 @@ end function InfiniteScrollingFrame:render() local props = self.props local theme = props.Theme - local style = theme:getStyle("Framework", self) + + local style + if FlagsList:get("FFlagRefactorDevFrameworkTheme") then + style = props.Stylizer + else + style = theme:getStyle("Framework", self) + end local position = props.Position local size = props.Size @@ -147,7 +162,8 @@ function InfiniteScrollingFrame:render() end ContextServices.mapToProps(InfiniteScrollingFrame, { - Theme = ContextServices.Theme, + Stylizer = FlagsList:get("FFlagRefactorDevFrameworkTheme") and ContextServices.Stylizer or nil, + Theme = (not FlagsList:get("FFlagRefactorDevFrameworkTheme")) and ContextServices.Theme or nil, }) return InfiniteScrollingFrame \ No newline at end of file diff --git a/BuiltInPlugins/PlayerEmulator/Packages/Framework/UI/InfiniteScrollingFrame/style.lua b/BuiltInPlugins/PlayerEmulator/Packages/Framework/UI/InfiniteScrollingFrame/style.lua index 2ce6539f13..0d8c13bdd1 100644 --- a/BuiltInPlugins/PlayerEmulator/Packages/Framework/UI/InfiniteScrollingFrame/style.lua +++ b/BuiltInPlugins/PlayerEmulator/Packages/Framework/UI/InfiniteScrollingFrame/style.lua @@ -3,12 +3,38 @@ local Framework = script.Parent.Parent.Parent local StudioFrameworkStyles = Framework.StudioUI.StudioFrameworkStyles local Common = require(StudioFrameworkStyles.Common) -return function(theme, getColor) - local common = Common(theme, getColor) +local StyleKey = require(Framework.Style.StyleKey) - local Default = common.Scroller +local Util = require(Framework.Util) +local FlagsList = Util.Flags.new({ + FFlagRefactorDevFrameworkTheme = {"RefactorDevFrameworkTheme"}, +}) +if FlagsList:get("FFlagRefactorDevFrameworkTheme") then return { - Default = Default, + BackgroundTransparency = 1, + BorderSizePixel = 0, + BackgroundColor3 = StyleKey.MainBackground, + + TopImage = "rbxasset://textures/StudioToolbox/ScrollBarTop.png", + MidImage = "rbxasset://textures/StudioToolbox/ScrollBarMiddle.png", + BottomImage = "rbxasset://textures/StudioToolbox/ScrollBarBottom.png", + + ScrollingEnabled = true, + ScrollingDirection = Enum.ScrollingDirection.Y, + ScrollBarThickness = 8, + ScrollBarImageTransparency = 0.5, + ScrollBarImageColor3 = StyleKey.ScrollBar, + VerticalScrollBarInset = Enum.ScrollBarInset.Always } +else + return function(theme, getColor) + local common = Common(theme, getColor) + + local Default = common.Scroller + + return { + Default = Default, + } + end end diff --git a/BuiltInPlugins/PlayerEmulator/Packages/Framework/UI/InstanceTreeView.lua b/BuiltInPlugins/PlayerEmulator/Packages/Framework/UI/InstanceTreeView.lua index e2582b8bad..0d6c9d37a0 100644 --- a/BuiltInPlugins/PlayerEmulator/Packages/Framework/UI/InstanceTreeView.lua +++ b/BuiltInPlugins/PlayerEmulator/Packages/Framework/UI/InstanceTreeView.lua @@ -2,7 +2,6 @@ Displays the hierarchy of an instance. Required Props: - Theme Theme: The theme supplied from mapToProps() UDim2 Size: The size of the component table Instances: The instance which this tree should display at root table Expansion: Which items should be expanded - Set @@ -11,8 +10,10 @@ callback OnSelectionChange: Called when a node is selected or not - (newSelection: Set) => void Optional Props: + Theme Theme: The theme supplied from mapToProps() callback SortChildren: A comparator function to sort two items in the tree - SortChildren(left: Item, right: Item) => boolean Style Style: a style table supplied from props and theme:getStyle() + Stylizer Stylizer: A Stylizer ContextItem, which is provided via mapToProps. Style Values: table TreeView: Style values for the underlying tree view. @@ -30,6 +31,11 @@ local Cryo = require(Framework.Parent.Cryo) local UI = Framework.UI local TreeView = require(UI.TreeView) local InstanceTreeRow = require(script.InstanceTreeRow) +local Util = require(Framework.Util) + +local FlagsList = Util.Flags.new({ + FFlagRefactorDevFrameworkTheme = {"RefactorDevFrameworkTheme"}, +}) local InstanceTreeView = Roact.PureComponent:extend("InstanceTreeView") Typecheck.wrap(InstanceTreeView, script) @@ -55,7 +61,12 @@ function InstanceTreeView:init() self.renderRow = function(row) local props = self.props local theme = props.Theme - local style = theme:getStyle("Framework", self) + local style + if FlagsList:get("FFlagRefactorDevFrameworkTheme") then + style = props.Stylizer + else + style = theme:getStyle("Framework", self) + end local isSelected = props.Selection[row.item] local isExpanded = props.Expansion[row.item] @@ -81,7 +92,12 @@ end function InstanceTreeView:render() local props = self.props local theme = props.Theme - local style = theme:getStyle("Framework", self) + local style + if FlagsList:get("FFlagRefactorDevFrameworkTheme") then + style = props.Stylizer + else + style = theme:getStyle("Framework", self) + end return Roact.createElement(TreeView, { RootItems = props.Instances, @@ -95,7 +111,8 @@ function InstanceTreeView:render() end ContextServices.mapToProps(InstanceTreeView, { - Theme = ContextServices.Theme, + Stylizer = FlagsList:get("FFlagRefactorDevFrameworkTheme") and ContextServices.Stylizer or nil, + Theme = (not FlagsList:get("FFlagRefactorDevFrameworkTheme")) and ContextServices.Theme or nil, }) return InstanceTreeView \ No newline at end of file diff --git a/BuiltInPlugins/PlayerEmulator/Packages/Framework/UI/InstanceTreeView/InstanceTreeRow.lua b/BuiltInPlugins/PlayerEmulator/Packages/Framework/UI/InstanceTreeView/InstanceTreeRow.lua index 8000e57785..6110490a54 100644 --- a/BuiltInPlugins/PlayerEmulator/Packages/Framework/UI/InstanceTreeView/InstanceTreeRow.lua +++ b/BuiltInPlugins/PlayerEmulator/Packages/Framework/UI/InstanceTreeView/InstanceTreeRow.lua @@ -25,7 +25,7 @@ local TextLabel = require(UI.TextLabel) Typecheck.wrap(InstanceTreeRow, script) local function getClassIcon(instance) - local StudioService = game:GetService("StudioService") + local StudioService = game:GetService("StudioService") local className = instance.ClassName if instance.IsA then if instance:IsA("JointInstance") and className == "ManualWeld" or className == "ManualGlue" then @@ -73,9 +73,10 @@ function InstanceTreeRow:render() local arrowSize = style.Arrow.Size local padding = style.IconPadding local iconInfo = getClassIcon(item) - local iconSize = iconInfo.ImageRectSize.X + -- Default iconSize to (0, 0) as ImageRectSize is unavailable in Roblox CLI + local iconSize = iconInfo.ImageRectSize or Vector2.new() local labelOffset = indent + arrowSize + 2 * padding - local textOffset = iconSize + 3 * padding + local textOffset = iconSize.X + 3 * padding return Roact.createElement(Container, { Size = UDim2.new(1, -indent, 0, style.RowHeight), @@ -105,10 +106,10 @@ function InstanceTreeRow:render() Size = UDim2.new(1, -arrowSize, 1, 0), }, { Icon = Roact.createElement("ImageLabel", { - Size = UDim2.fromOffset(iconSize, iconInfo.ImageRectSize.Y), + Size = UDim2.fromOffset(iconSize.X, iconSize.Y), BackgroundTransparency = 1, Image = iconInfo.Image, - ImageRectSize = iconInfo.ImageRectSize, + ImageRectSize = iconSize, ImageRectOffset = iconInfo.ImageRectOffset, Position = UDim2.new(0, padding, 0.5, 0), AnchorPoint = Vector2.new(0, 0.5) diff --git a/BuiltInPlugins/PlayerEmulator/Packages/Framework/UI/InstanceTreeView/style.lua b/BuiltInPlugins/PlayerEmulator/Packages/Framework/UI/InstanceTreeView/style.lua index bff5e0d59f..582d52069b 100644 --- a/BuiltInPlugins/PlayerEmulator/Packages/Framework/UI/InstanceTreeView/style.lua +++ b/BuiltInPlugins/PlayerEmulator/Packages/Framework/UI/InstanceTreeView/style.lua @@ -1,20 +1,27 @@ local Framework = script.Parent.Parent.Parent local Util = require(Framework.Util) +local Cryo = Util.Cryo +local deepCopy = Util.deepCopy local Style = Util.Style + +local StyleKey = require(Framework.Style.StyleKey) + local UIFolderData = require(Framework.UI.UIFolderData) local TreeView = require(UIFolderData.TreeView.style) local StudioFrameworkStyles = Framework.StudioUI.StudioFrameworkStyles local Common = require(StudioFrameworkStyles.Common) -return function(theme, getColor) - local common = Common(theme, getColor) - local treeView = TreeView(theme, getColor) +local FlagsList = Util.Flags.new({ + FFlagRefactorDevFrameworkTheme = {"RefactorDevFrameworkTheme"}, +}) - local Default = Style.new({ - Text = Style.extend(common.MainText, {}), - TreeView = Style.extend(treeView.Default, {}), +if FlagsList:get("FFlagRefactorDevFrameworkTheme") then + local treeView = deepCopy(TreeView) + return { + Text = Common.MainText, + TreeView = treeView, Indent = 20, RowHeight = 24, Arrow = { @@ -22,25 +29,57 @@ return function(theme, getColor) Size = 12, ExpandedOffset = Vector2.new(24, 0), CollapsedOffset = Vector2.new(12, 0), - Color = theme:GetColor("MainText") + Color = StyleKey.MainText, }, IconPadding = 5, - HoverColor = theme:GetColor("Button", "Hover"), - SelectedColor = theme:GetColor("DialogMainButton"), - SelectedTextColor = theme:GetColor("DialogMainButtonText") - }) - - local Compact = Style.extend(Default, { - Text = Style.extend(common.MainText, { - TextSize = 14 - }), - IconPadding = 3, - RowHeight = 20, - Indent = 16 - }) + HoverColor = StyleKey.ButtonHover, + SelectedColor = StyleKey.DialogMainButton, + SelectedTextColor = StyleKey.DialogMainButtonText, - return { - Default = Default, - Compact = Compact + ["&Compact"] = { + Text = Cryo.Dictionary.join(Common.MainText, { + TextSize = 14, + }), + IconPadding = 3, + RowHeight = 20, + Indent = 16 + } } -end +else + return function(theme, getColor) + local common = Common(theme, getColor) + local treeView = TreeView(theme, getColor) + + local Default = Style.new({ + Text = Style.extend(common.MainText, {}), + TreeView = Style.extend(treeView.Default, {}), + Indent = 20, + RowHeight = 24, + Arrow = { + Image = "rbxasset://textures/StudioSharedUI/arrowSpritesheet.png", + Size = 12, + ExpandedOffset = Vector2.new(24, 0), + CollapsedOffset = Vector2.new(12, 0), + Color = theme:GetColor("MainText") + }, + IconPadding = 5, + HoverColor = theme:GetColor("Button", "Hover"), + SelectedColor = theme:GetColor("DialogMainButton"), + SelectedTextColor = theme:GetColor("DialogMainButtonText") + }) + + local Compact = Style.extend(Default, { + Text = Style.extend(common.MainText, { + TextSize = 14 + }), + IconPadding = 3, + RowHeight = 20, + Indent = 16 + }) + + return { + Default = Default, + Compact = Compact + } + end +end \ No newline at end of file diff --git a/BuiltInPlugins/PlayerEmulator/Packages/Framework/UI/LinkText.lua b/BuiltInPlugins/PlayerEmulator/Packages/Framework/UI/LinkText.lua index bd62ad201f..aa7ce4d917 100644 --- a/BuiltInPlugins/PlayerEmulator/Packages/Framework/UI/LinkText.lua +++ b/BuiltInPlugins/PlayerEmulator/Packages/Framework/UI/LinkText.lua @@ -4,9 +4,10 @@ Required Props: callback OnClick: A callback for when the user clicks this link. - Theme Theme: A Theme ContextItem, which is provided via mapToProps. Optional Props: + Theme Theme: A Theme ContextItem, which is provided via mapToProps. + Stylizer Stylizer: A Stylizer ContextItem, which is provided via mapToProps. string Text: The text to display in this link. Style Style: The style with which to render this component. StyleModifier StyleModifier: The StyleModifier index into Style. @@ -30,9 +31,13 @@ local Framework = script.Parent.Parent local Roact = require(Framework.Parent.Roact) local ContextServices = require(Framework.ContextServices) local HoverArea = require(Framework.UI.HoverArea) + local Util = require(Framework.Util) local StyleModifier = Util.StyleModifier local Typecheck = Util.Typecheck +local FlagsList = Util.Flags.new({ + FFlagRefactorDevFrameworkTheme = {"RefactorDevFrameworkTheme"}, +}) local Button = require(Framework.UI.Button) @@ -73,7 +78,12 @@ function LinkText:render() local state = self.state local theme = props.Theme local styleModifier = state.StyleModifier - local style = theme:getStyle("Framework", self) + local style + if FlagsList:get("FFlagRefactorDevFrameworkTheme") then + style = props.Stylizer + else + style = theme:getStyle("Framework", self) + end local size = props.Size @@ -166,7 +176,8 @@ function LinkText:render() end ContextServices.mapToProps(LinkText, { - Theme = ContextServices.Theme, + Stylizer = FlagsList:get("FFlagRefactorDevFrameworkTheme") and ContextServices.Stylizer or nil, + Theme = (not FlagsList:get("FFlagRefactorDevFrameworkTheme")) and ContextServices.Theme or nil, }) return LinkText diff --git a/BuiltInPlugins/PlayerEmulator/Packages/Framework/UI/LinkText/example.lua b/BuiltInPlugins/PlayerEmulator/Packages/Framework/UI/LinkText/example.lua index 4f03c023cc..2f8bc4927c 100644 --- a/BuiltInPlugins/PlayerEmulator/Packages/Framework/UI/LinkText/example.lua +++ b/BuiltInPlugins/PlayerEmulator/Packages/Framework/UI/LinkText/example.lua @@ -10,20 +10,32 @@ return function(plugin) local Dialog = StudioUI.Dialog local StudioFrameworkStyles = StudioUI.StudioFrameworkStyles + local StudioTheme = require(Framework.Style.Themes.StudioTheme) + local UI = require(Framework.UI) local Container = UI.Container local LinkText = UI.LinkText local Decoration = UI.Decoration + local Util = require(Framework.Util) + local FlagsList = Util.Flags.new({ + FFlagRefactorDevFrameworkTheme = {"RefactorDevFrameworkTheme"}, + }) + local pluginItem = Plugin.new(plugin) local mouse = Mouse.new(plugin:GetMouse()) - local theme = Theme.new(function(theme, getColor) - local studioStyles = StudioFrameworkStyles.new(theme, getColor) - return { - Framework = studioStyles, - } - end) + local theme + if FlagsList:get("FFlagRefactorDevFrameworkTheme") then + theme = StudioTheme.new() + else + theme = Theme.new(function(theme, getColor) + local studioStyles = StudioFrameworkStyles.new(theme, getColor) + return { + Framework = studioStyles, + } + end) + end -- Mount and display a dialog local ExampleLinkText = Roact.PureComponent:extend("ExampleLinkText") diff --git a/BuiltInPlugins/PlayerEmulator/Packages/Framework/UI/LinkText/style.lua b/BuiltInPlugins/PlayerEmulator/Packages/Framework/UI/LinkText/style.lua index 7cd57e28ca..da9dac1c20 100644 --- a/BuiltInPlugins/PlayerEmulator/Packages/Framework/UI/LinkText/style.lua +++ b/BuiltInPlugins/PlayerEmulator/Packages/Framework/UI/LinkText/style.lua @@ -1,19 +1,30 @@ local Framework = script.Parent.Parent.Parent +local StyleKey = require(Framework.Style.StyleKey) + local Util = require(Framework.Util) local Style = Util.Style +local FlagsList = Util.Flags.new({ + FFlagRefactorDevFrameworkTheme = {"RefactorDevFrameworkTheme"}, +}) local StudioFrameworkStyles = Framework.StudioUI.StudioFrameworkStyles local Common = require(StudioFrameworkStyles.Common) -return function(theme, getColor) - local common = Common(theme, getColor) - - local Default = Style.extend(common.MainText, { - TextColor = theme:GetColor("LinkText"), - }) - +if FlagsList:get("FFlagRefactorDevFrameworkTheme") then return { - Default = Default, + TextColor = StyleKey.LinkText, } -end +else + return function(theme, getColor) + local common = Common(theme, getColor) + + local Default = Style.extend(common.MainText, { + TextColor = theme:GetColor("LinkText"), + }) + + return { + Default = Default, + } + end +end \ No newline at end of file diff --git a/BuiltInPlugins/PlayerEmulator/Packages/Framework/UI/LinkText/test.spec.lua b/BuiltInPlugins/PlayerEmulator/Packages/Framework/UI/LinkText/test.spec.lua index ba7d3b4e2b..37cecf30b1 100644 --- a/BuiltInPlugins/PlayerEmulator/Packages/Framework/UI/LinkText/test.spec.lua +++ b/BuiltInPlugins/PlayerEmulator/Packages/Framework/UI/LinkText/test.spec.lua @@ -8,13 +8,25 @@ return function() local FrameworkStyles = require(Framework.UI.FrameworkStyles) local LinkText = require(script.Parent) + local StudioTheme = require(Framework.Style.Themes.StudioTheme) + + local Util = require(Framework.Util) + local FlagsList = Util.Flags.new({ + FFlagRefactorDevFrameworkTheme = {"RefactorDevFrameworkTheme"}, + }) + local function createTestLinkText(props) local mouse = Mouse.new({}) - local theme = Theme.new(function() - return { - Framework = FrameworkStyles.new(), - } - end) + local theme + if FlagsList:get("FFlagRefactorDevFrameworkTheme") then + theme = StudioTheme.mock() + else + theme = Theme.new(function() + return { + Framework = FrameworkStyles.new(), + } + end) + end return provide({theme, mouse}, { LinkText = Roact.createElement(LinkText, props), }) diff --git a/BuiltInPlugins/PlayerEmulator/Packages/Framework/UI/LoadingBar.lua b/BuiltInPlugins/PlayerEmulator/Packages/Framework/UI/LoadingBar.lua index 6f964d35d9..837d5a034b 100644 --- a/BuiltInPlugins/PlayerEmulator/Packages/Framework/UI/LoadingBar.lua +++ b/BuiltInPlugins/PlayerEmulator/Packages/Framework/UI/LoadingBar.lua @@ -5,12 +5,13 @@ Required Props: number Progress: The progress of the load, between 0 and 1. - Theme Theme: A Theme ContextItem, which is provided via mapToProps. Optional Props: - Style Style: The style with which to render this component. + Theme Theme: A Theme ContextItem, which is provided via mapToProps. + Stylizer Stylizer: A Stylizer ContextItem, which is provided via mapToProps. StyleModifier StyleModifier: The StyleModifier index into Style. UDim2 Size: The size of this component. + Style Style: The style with which to render this component. UDim2 Position: The position of this component. Vector2 AnchorPoint: The pivot point of this component's Position prop. number ZIndex: The render index of this component. @@ -27,7 +28,12 @@ local Framework = script.Parent.Parent local Roact = require(Framework.Parent.Roact) local ContextServices = require(Framework.ContextServices) local Container = require(Framework.UI.Container) -local Typecheck = require(Framework.Util).Typecheck + +local Util = require(Framework.Util) +local Typecheck = Util.Typecheck +local FlagsList = Util.Flags.new({ + FFlagRefactorDevFrameworkTheme = {"RefactorDevFrameworkTheme"}, +}) local LoadingBar = Roact.PureComponent:extend("LoadingBar") Typecheck.wrap(LoadingBar, script) @@ -39,7 +45,12 @@ end function LoadingBar:render() local props = self.props local theme = props.Theme - local style = theme:getStyle("Framework", self) + local style + if FlagsList:get("FFlagRefactorDevFrameworkTheme") then + style = props.Stylizer + else + style = theme:getStyle("Framework", self) + end local progress = props.Progress progress = math.clamp(progress, 0, 1) @@ -73,7 +84,8 @@ function LoadingBar:render() end ContextServices.mapToProps(LoadingBar, { - Theme = ContextServices.Theme, + Stylizer = FlagsList:get("FFlagRefactorDevFrameworkTheme") and ContextServices.Stylizer or nil, + Theme = (not FlagsList:get("FFlagRefactorDevFrameworkTheme")) and ContextServices.Theme or nil, }) return LoadingBar diff --git a/BuiltInPlugins/PlayerEmulator/Packages/Framework/UI/LoadingBar/example.lua b/BuiltInPlugins/PlayerEmulator/Packages/Framework/UI/LoadingBar/example.lua index a01c4950f7..2fde53e3ce 100644 --- a/BuiltInPlugins/PlayerEmulator/Packages/Framework/UI/LoadingBar/example.lua +++ b/BuiltInPlugins/PlayerEmulator/Packages/Framework/UI/LoadingBar/example.lua @@ -15,14 +15,26 @@ return function(plugin) local Decoration = UI.Decoration local Button = UI.Button + local StudioTheme = require(Framework.Style.Themes.StudioTheme) + + local Util = require(Framework.Util) + local FlagsList = Util.Flags.new({ + FFlagRefactorDevFrameworkTheme = {"RefactorDevFrameworkTheme"}, + }) + local pluginItem = Plugin.new(plugin) - local theme = Theme.new(function(theme, getColor) - local studioStyles = StudioFrameworkStyles.new(theme, getColor) - return { - Framework = studioStyles, - } - end) + local theme + if FlagsList:get("FFlagRefactorDevFrameworkTheme") then + theme = StudioTheme.new() + else + theme = Theme.new(function(theme, getColor) + local studioStyles = StudioFrameworkStyles.new(theme, getColor) + return { + Framework = studioStyles, + } + end) + end -- Mount and display a dialog local ExampleLoadingBar = Roact.PureComponent:extend("ExampleLoadingBar") @@ -64,6 +76,11 @@ return function(plugin) local progressText = ("%i%%"):format(progress * 100) + local textColor + if (not FlagsList:get("FFlagRefactorDevFrameworkTheme")) then + textColor = theme:get("Framework").Button.Default.TextColor + end + return ContextServices.provide({pluginItem, theme}, { Main = Roact.createElement(Dialog, { Enabled = enabled, @@ -99,7 +116,7 @@ return function(plugin) Size = UDim2.fromOffset(120, 16), BackgroundTransparency = 1, Text = progressText, - TextColor3 = theme:get("Framework").Button.Default.TextColor, + TextColor3 = textColor, Font = Enum.Font.SourceSans, TextSize = 16, }), diff --git a/BuiltInPlugins/PlayerEmulator/Packages/Framework/UI/LoadingBar/style.lua b/BuiltInPlugins/PlayerEmulator/Packages/Framework/UI/LoadingBar/style.lua index 9159121f37..38307c9f06 100644 --- a/BuiltInPlugins/PlayerEmulator/Packages/Framework/UI/LoadingBar/style.lua +++ b/BuiltInPlugins/PlayerEmulator/Packages/Framework/UI/LoadingBar/style.lua @@ -1,7 +1,14 @@ local Framework = script.Parent.Parent.Parent +local StyleKey = require(Framework.Style.StyleKey) + local Util = require(Framework.Util) +local Cryo = Util.Cryo +local deepCopy = Util.deepCopy local Style = Util.Style +local FlagsList = Util.Flags.new({ + FFlagRefactorDevFrameworkTheme = {"RefactorDevFrameworkTheme"}, +}) local UI = require(Framework.UI) local Decoration = UI.Decoration @@ -9,21 +16,35 @@ local Decoration = UI.Decoration local UIFolderData = require(Framework.UI.UIFolderData) local RoundBox = require(UIFolderData.RoundBox.style) -return function(theme, getColor) - local roundBox = RoundBox(theme, getColor) - - local Default = Style.new({ +if FlagsList:get("FFlagRefactorDevFrameworkTheme") then + local roundBox = deepCopy(RoundBox) + return { Background = Decoration.RoundBox, Foreground = Decoration.RoundBox, - BackgroundStyle = Style.extend(roundBox.Default, { - Color = theme:GetColor("Button"), + BackgroundStyle = Cryo.Dictionary.join(roundBox, { + Color = StyleKey.Button, }), - ForegroundStyle = Style.extend(roundBox.Default, { - Color = theme:GetColor("DialogMainButton", "Selected"), + ForegroundStyle = Cryo.Dictionary.join(roundBox, { + Color = StyleKey.DialogMainButtonSelected, }), - }) - - return { - Default = Default, } -end +else + return function(theme, getColor) + local roundBox = RoundBox(theme, getColor) + + local Default = Style.new({ + Background = Decoration.RoundBox, + Foreground = Decoration.RoundBox, + BackgroundStyle = Style.extend(roundBox.Default, { + Color = theme:GetColor("Button"), + }), + ForegroundStyle = Style.extend(roundBox.Default, { + Color = theme:GetColor("DialogMainButton", "Selected"), + }), + }) + + return { + Default = Default, + } + end +end \ No newline at end of file diff --git a/BuiltInPlugins/PlayerEmulator/Packages/Framework/UI/LoadingBar/test.spec.lua b/BuiltInPlugins/PlayerEmulator/Packages/Framework/UI/LoadingBar/test.spec.lua index b2e7fed4ab..c898cde0f4 100644 --- a/BuiltInPlugins/PlayerEmulator/Packages/Framework/UI/LoadingBar/test.spec.lua +++ b/BuiltInPlugins/PlayerEmulator/Packages/Framework/UI/LoadingBar/test.spec.lua @@ -8,12 +8,24 @@ return function() local LoadingBar = require(script.Parent) local Box = require(Framework.UI.Box) + local StudioTheme = require(Framework.Style.Themes.StudioTheme) + + local Util = require(Framework.Util) + local FlagsList = Util.Flags.new({ + FFlagRefactorDevFrameworkTheme = {"RefactorDevFrameworkTheme"}, + }) + local function createTestLoadingBar(props, children) - local theme = Theme.new(function() - return { - Framework = FrameworkStyles.new(), - } - end) + local theme + if FlagsList:get("FFlagRefactorDevFrameworkTheme") then + theme = StudioTheme.mock() + else + theme = Theme.new(function() + return { + Framework = FrameworkStyles.new(), + } + end) + end return provide({theme}, { LoadingBar = Roact.createElement(LoadingBar, props, children), }) diff --git a/BuiltInPlugins/PlayerEmulator/Packages/Framework/UI/LoadingIndicator.lua b/BuiltInPlugins/PlayerEmulator/Packages/Framework/UI/LoadingIndicator.lua index 38135ca2fb..61d4c063d9 100644 --- a/BuiltInPlugins/PlayerEmulator/Packages/Framework/UI/LoadingIndicator.lua +++ b/BuiltInPlugins/PlayerEmulator/Packages/Framework/UI/LoadingIndicator.lua @@ -1,10 +1,8 @@ --[[ Loading Indicator of 3 rectangles which cyclically increase and then decrease in height while changing color. - Required Props: - Theme Theme: A Theme ContextItem, which is provided via mapToProps. - Optional Props: + Theme Theme: A Theme ContextItem, which is provided via mapToProps. Vector2 AnchorPoint: an offset for positioning number LayoutOrder: The layout order of this component in a UILayout. UDim2 Position: The position of the component. Defaults to zero. @@ -12,18 +10,23 @@ Style Style: The style with which to render this component. StyleModifier StyleModifier: The StyleModifier index into Style. number ZIndex: The render index of this component. + Stylizer Stylizer: A Stylizer ContextItem, which is provided via mapToProps. Style Values: Color3 StartColor: The starting color of the blocks. Color3 EndColor: The color of the blocks as they are at their maximum height. ]] - local RunService = game:GetService("RunService") local Framework = script.Parent.Parent local Roact = require(Framework.Parent.Roact) local ContextServices = require(Framework.ContextServices) -local Typecheck = require(Framework.Util).Typecheck + +local Util = require(Framework.Util) +local Typecheck = Util.Typecheck +local FlagsList = Util.Flags.new({ + FFlagRefactorDevFrameworkTheme = {"RefactorDevFrameworkTheme"}, +}) local LoadingIndicator = Roact.PureComponent:extend("LoadingIndicator") Typecheck.wrap(LoadingIndicator, script) @@ -95,7 +98,12 @@ function LoadingIndicator:render() local layoutOrder = props.LayoutOrder local theme = props.Theme - local style = theme:getStyle("Framework", self) + local style + if FlagsList:get("FFlagRefactorDevFrameworkTheme") then + style = props.Stylizer + else + style = theme:getStyle("Framework", self) + end local startColor = style.StartColor local endColor = style.EndColor @@ -142,7 +150,8 @@ function LoadingIndicator:render() end ContextServices.mapToProps(LoadingIndicator, { - Theme = ContextServices.Theme, + Stylizer = FlagsList:get("FFlagRefactorDevFrameworkTheme") and ContextServices.Stylizer or nil, + Theme = (not FlagsList:get("FFlagRefactorDevFrameworkTheme")) and ContextServices.Theme or nil, }) return LoadingIndicator diff --git a/BuiltInPlugins/PlayerEmulator/Packages/Framework/UI/LoadingIndicator/style.lua b/BuiltInPlugins/PlayerEmulator/Packages/Framework/UI/LoadingIndicator/style.lua index feedb15ead..d0ec23c9c3 100644 --- a/BuiltInPlugins/PlayerEmulator/Packages/Framework/UI/LoadingIndicator/style.lua +++ b/BuiltInPlugins/PlayerEmulator/Packages/Framework/UI/LoadingIndicator/style.lua @@ -1,16 +1,27 @@ local Framework = script.Parent.Parent.Parent +local StyleKey = require(Framework.Style.StyleKey) + local Util = require(Framework.Util) local Style = Util.Style +local FlagsList = Util.Flags.new({ + FFlagRefactorDevFrameworkTheme = {"RefactorDevFrameworkTheme"}, +}) -return function(theme, getColor) - - local Default = Style.new({ - StartColor = theme:GetColor("DimmedText"), - EndColor = theme:GetColor("DialogMainButton", "Selected") - }) - +if FlagsList:get("FFlagRefactorDevFrameworkTheme") then return { - Default = Default, + StartColor = StyleKey.DimmedText, + EndColor = StyleKey.DialogMainButtonSelected, } -end +else + return function(theme, getColor) + local Default = Style.new({ + StartColor = theme:GetColor("DimmedText"), + EndColor = theme:GetColor("DialogMainButton", "Selected") + }) + + return { + Default = Default, + } + end +end \ No newline at end of file diff --git a/BuiltInPlugins/PlayerEmulator/Packages/Framework/UI/RadioButton.lua b/BuiltInPlugins/PlayerEmulator/Packages/Framework/UI/RadioButton.lua index 11680f3214..14ccde4cfe 100644 --- a/BuiltInPlugins/PlayerEmulator/Packages/Framework/UI/RadioButton.lua +++ b/BuiltInPlugins/PlayerEmulator/Packages/Framework/UI/RadioButton.lua @@ -4,15 +4,16 @@ Required Props: string Key: The key that will be sent back to the OnClick function. string Text: The text to display. - Theme Theme: A Theme ContextItem, which is provided via mapToProps. Optional Props: boolean Disabled: Whether or not the radio button is disabled. OnClick will not work when disabled. number LayoutOrder: The layout order of the frame. callback OnClick: paramters(string key). Fires when the button is activated and returns back the Key. boolean Selected: Whether or not the radio button is selected. + Style Style: The style with which to render this component. + Theme Theme: A Theme ContextItem, which is provided via mapToProps. + Stylizer Stylizer: A Stylizer ContextItem, which is provided via mapToProps. ]] - local TextService = game:GetService("TextService") local Framework = script.Parent.Parent @@ -25,6 +26,9 @@ local TextLabel = require(Framework.UI.TextLabel) local Util = require(Framework.Util) local Typecheck = Util.Typecheck local StyleModifier = Util.StyleModifier +local FlagsList = Util.Flags.new({ + FFlagRefactorDevFrameworkTheme = {"RefactorDevFrameworkTheme"}, +}) local RadioButton = Roact.PureComponent:extend("RadioButton") Typecheck.wrap(RadioButton, script) @@ -55,7 +59,13 @@ function RadioButton:render() local text = self.props.Text local theme = self.props.Theme - local style = theme:getStyle("Framework", self) + local style + if FlagsList:get("FFlagRefactorDevFrameworkTheme") then + style = self.props.Stylizer + else + style = theme:getStyle("Framework", self) + end + local font = style.Font local textSize = style.TextSize local imageSize = style.ImageSize @@ -116,7 +126,8 @@ function RadioButton:render() end ContextServices.mapToProps(RadioButton, { - Theme = ContextServices.Theme, + Stylizer = FlagsList:get("FFlagRefactorDevFrameworkTheme") and ContextServices.Stylizer or nil, + Theme = (not FlagsList:get("FFlagRefactorDevFrameworkTheme")) and ContextServices.Theme or nil, }) return RadioButton \ No newline at end of file diff --git a/BuiltInPlugins/PlayerEmulator/Packages/Framework/UI/RadioButton/example.lua b/BuiltInPlugins/PlayerEmulator/Packages/Framework/UI/RadioButton/example.lua index e9ad922971..d9476065bf 100644 --- a/BuiltInPlugins/PlayerEmulator/Packages/Framework/UI/RadioButton/example.lua +++ b/BuiltInPlugins/PlayerEmulator/Packages/Framework/UI/RadioButton/example.lua @@ -15,14 +15,26 @@ return function(plugin) local StudioUI = require(Framework.StudioUI) local StudioFrameworkStyles = StudioUI.StudioFrameworkStyles + local StudioTheme = require(Framework.Style.Themes.StudioTheme) + + local Util = require(Framework.Util) + local FlagsList = Util.Flags.new({ + FFlagRefactorDevFrameworkTheme = {"RefactorDevFrameworkTheme"}, + }) + local pluginItem = Plugin.new(plugin) local mouse = Mouse.new(plugin:GetMouse()) - local theme = Theme.new(function(theme, getColor) - return { - Framework = StudioFrameworkStyles.new(theme, getColor) - } - end) + local theme + if FlagsList:get("FFlagRefactorDevFrameworkTheme") then + theme = StudioTheme.new() + else + theme = Theme.new(function(theme, getColor) + return { + Framework = StudioFrameworkStyles.new(theme, getColor) + } + end) + end -- Mount and display a dialog local ExampleContainer = Roact.PureComponent:extend("ExampleContainer") diff --git a/BuiltInPlugins/PlayerEmulator/Packages/Framework/UI/RadioButton/style.lua b/BuiltInPlugins/PlayerEmulator/Packages/Framework/UI/RadioButton/style.lua index c4af3f83f7..e5b964d761 100644 --- a/BuiltInPlugins/PlayerEmulator/Packages/Framework/UI/RadioButton/style.lua +++ b/BuiltInPlugins/PlayerEmulator/Packages/Framework/UI/RadioButton/style.lua @@ -1,8 +1,13 @@ local Framework = script.Parent.Parent.Parent +local StyleKey = require(Framework.Style.StyleKey) + local Util = require(Framework.Util) local Style = Util.Style local StyleModifier = Util.StyleModifier +local FlagsList = Util.Flags.new({ + FFlagRefactorDevFrameworkTheme = {"RefactorDevFrameworkTheme"}, +}) local UI = require(Framework.UI) local Decoration = UI.Decoration @@ -10,10 +15,8 @@ local Decoration = UI.Decoration local StudioFrameworkStyles = Framework.StudioUI.StudioFrameworkStyles local Common = require(StudioFrameworkStyles.Common) -return function(theme, getColor) - local common = Common(theme, getColor) - - local Default = Style.extend(common.MainText, { +if FlagsList:get("FFlagRefactorDevFrameworkTheme") then + return { Padding = 6, ImageSize = UDim2.new(0, 20, 0, 20), Background = UI.Button, @@ -21,31 +24,67 @@ return function(theme, getColor) Background = Decoration.Image, BackgroundStyle = { Image = "rbxasset://textures/GameSettings/RadioButton.png", - Color = theme:GetColor("MainBackground"), + Color = StyleKey.MainBackground, }, [StyleModifier.Selected] = { BackgroundStyle = { Image = "rbxasset://textures/GameSettings/RadioButton.png", - Color = theme:GetColor("MainBackground"), + Color = StyleKey.MainBackground, }, Foreground = Decoration.Image, ForegroundStyle = { AnchorPoint = Vector2.new(0.5, 0.5), Image = "rbxasset://textures/ui/LuaApp/icons/ic-blue-dot.png", Position = UDim2.new(0.5, 0, 0.5, 0), - Size = UDim2.new(0.4, 0, 0.4, 0) + Size = UDim2.new(0.4, 0, 0.4, 0), }, }, [StyleModifier.Disabled] = { BackgroundStyle = { Image = "rbxasset://textures/GameSettings/RadioButton.png", - Color = theme:GetColor("MainBackground"), + Color = StyleKey.MainBackground, }, }, }, - }) - - return { - Default = Default, } -end +else + return function(theme, getColor) + local common = Common(theme, getColor) + + local Default = Style.extend(common.MainText, { + Padding = 6, + ImageSize = UDim2.new(0, 20, 0, 20), + Background = UI.Button, + BackgroundStyle = { + Background = Decoration.Image, + BackgroundStyle = { + Image = "rbxasset://textures/GameSettings/RadioButton.png", + Color = theme:GetColor("MainBackground"), + }, + [StyleModifier.Selected] = { + BackgroundStyle = { + Image = "rbxasset://textures/GameSettings/RadioButton.png", + Color = theme:GetColor("MainBackground"), + }, + Foreground = Decoration.Image, + ForegroundStyle = { + AnchorPoint = Vector2.new(0.5, 0.5), + Image = "rbxasset://textures/ui/LuaApp/icons/ic-blue-dot.png", + Position = UDim2.new(0.5, 0, 0.5, 0), + Size = UDim2.new(0.4, 0, 0.4, 0) + }, + }, + [StyleModifier.Disabled] = { + BackgroundStyle = { + Image = "rbxasset://textures/GameSettings/RadioButton.png", + Color = theme:GetColor("MainBackground"), + }, + }, + }, + }) + + return { + Default = Default, + } + end +end \ No newline at end of file diff --git a/BuiltInPlugins/PlayerEmulator/Packages/Framework/UI/RadioButton/test.spec.lua b/BuiltInPlugins/PlayerEmulator/Packages/Framework/UI/RadioButton/test.spec.lua index 9fc369ebcb..4e578229a9 100644 --- a/BuiltInPlugins/PlayerEmulator/Packages/Framework/UI/RadioButton/test.spec.lua +++ b/BuiltInPlugins/PlayerEmulator/Packages/Framework/UI/RadioButton/test.spec.lua @@ -8,17 +8,29 @@ return function() local RadioButton = require(script.Parent) local Immutable = require(Framework.Util.Immutable) + local StudioTheme = require(Framework.Style.Themes.StudioTheme) + + local Util = require(Framework.Util) + local FlagsList = Util.Flags.new({ + FFlagRefactorDevFrameworkTheme = {"RefactorDevFrameworkTheme"}, + }) + local DEFAULT_PROPS = { Key = "", Text = "", } local function createTestRadioButton(props, children) - local theme = Theme.new(function() - return { - Framework = FrameworkStyles.new(), - } - end) + local theme + if FlagsList:get("FFlagRefactorDevFrameworkTheme") then + theme = StudioTheme.mock() + else + theme = Theme.new(function() + return { + Framework = FrameworkStyles.new(), + } + end) + end local combinedProps if props then combinedProps = Immutable.JoinDictionaries(DEFAULT_PROPS, props) diff --git a/BuiltInPlugins/PlayerEmulator/Packages/Framework/UI/RadioButtonList.lua b/BuiltInPlugins/PlayerEmulator/Packages/Framework/UI/RadioButtonList.lua index ec577cea84..55a45e7104 100644 --- a/BuiltInPlugins/PlayerEmulator/Packages/Framework/UI/RadioButtonList.lua +++ b/BuiltInPlugins/PlayerEmulator/Packages/Framework/UI/RadioButtonList.lua @@ -4,21 +4,24 @@ Required Props: table Buttons: A list of buttons to display. Example: { Key = "", Text = "", Disabled = false }. - Theme Theme: A Theme ContextItem, which is provided via mapToProps. Optional Props: string SelectedKey: The initially selected key. number LayoutOrder: The layout order of the frame. Enum.FillDirection FillDirection: The direction in which buttons are filled. callback OnClick: paramters(string key). Fires when the button is activated and returns back the Key. + Theme Theme: A Theme ContextItem, which is provided via mapToProps. + Stylizer Stylizer: A Stylizer ContextItem, which is provided via mapToProps. ]] - local Framework = script.Parent.Parent local Roact = require(Framework.Parent.Roact) local ContextServices = require(Framework.ContextServices) local Util = require(Framework.Util) local Typecheck = Util.Typecheck +local FlagsList = Util.Flags.new({ + FFlagRefactorDevFrameworkTheme = {"RefactorDevFrameworkTheme"}, +}) local RadioButton = require(Framework.UI.RadioButton) @@ -57,7 +60,12 @@ function RadioButtonList:render() local layoutOrder = self.props.LayoutOrder local theme = self.props.Theme - local style = theme:getStyle("Framework", self) + local style + if FlagsList:get("FFlagRefactorDevFrameworkTheme") then + style = self.props.Stylizer + else + style = theme:getStyle("Framework", self) + end local children = {} @@ -82,7 +90,8 @@ function RadioButtonList:render() end ContextServices.mapToProps(RadioButtonList, { - Theme = ContextServices.Theme, + Stylizer = FlagsList:get("FFlagRefactorDevFrameworkTheme") and ContextServices.Stylizer or nil, + Theme = (not FlagsList:get("FFlagRefactorDevFrameworkTheme")) and ContextServices.Theme or nil, }) return RadioButtonList diff --git a/BuiltInPlugins/PlayerEmulator/Packages/Framework/UI/RadioButtonList/example.lua b/BuiltInPlugins/PlayerEmulator/Packages/Framework/UI/RadioButtonList/example.lua index 39b9d2b71e..45734e7da8 100644 --- a/BuiltInPlugins/PlayerEmulator/Packages/Framework/UI/RadioButtonList/example.lua +++ b/BuiltInPlugins/PlayerEmulator/Packages/Framework/UI/RadioButtonList/example.lua @@ -15,14 +15,26 @@ return function(plugin) local StudioUI = require(Framework.StudioUI) local StudioFrameworkStyles = StudioUI.StudioFrameworkStyles + local Util = require(Framework.Util) + local FlagsList = Util.Flags.new({ + FFlagRefactorDevFrameworkTheme = {"RefactorDevFrameworkTheme"}, + }) + + local StudioTheme = require(Framework.Style.Themes.StudioTheme) + local pluginItem = Plugin.new(plugin) local mouse = Mouse.new(plugin:GetMouse()) - local theme = Theme.new(function(theme, getColor) - return { - Framework = StudioFrameworkStyles.new(theme, getColor) - } - end) + local theme + if FlagsList:get("FFlagRefactorDevFrameworkTheme") then + theme = StudioTheme.new() + else + theme = Theme.new(function(theme, getColor) + return { + Framework = StudioFrameworkStyles.new(theme, getColor) + } + end) + end -- Mount and display a dialog local ExampleContainer = Roact.PureComponent:extend("ExampleContainer") diff --git a/BuiltInPlugins/PlayerEmulator/Packages/Framework/UI/RadioButtonList/style.lua b/BuiltInPlugins/PlayerEmulator/Packages/Framework/UI/RadioButtonList/style.lua index eb87ca16ec..88be1c8387 100644 --- a/BuiltInPlugins/PlayerEmulator/Packages/Framework/UI/RadioButtonList/style.lua +++ b/BuiltInPlugins/PlayerEmulator/Packages/Framework/UI/RadioButtonList/style.lua @@ -2,18 +2,27 @@ local Framework = script.Parent.Parent.Parent local Util = require(Framework.Util) local Style = Util.Style +local FlagsList = Util.Flags.new({ + FFlagRefactorDevFrameworkTheme = {"RefactorDevFrameworkTheme"}, +}) local StudioFrameworkStyles = Framework.StudioUI.StudioFrameworkStyles local Common = require(StudioFrameworkStyles.Common) -return function(theme, getColor) - local common = Common(theme, getColor) - - local Default = Style.extend(common.MainText, { - Padding = 6, - }) - +if FlagsList:get("FFlagRefactorDevFrameworkTheme") then return { - Default = Default, + Padding = 6, } +else + return function(theme, getColor) + local common = Common(theme, getColor) + + local Default = Style.extend(common.MainText, { + Padding = 6, + }) + + return { + Default = Default, + } + end end diff --git a/BuiltInPlugins/PlayerEmulator/Packages/Framework/UI/RangeSlider.lua b/BuiltInPlugins/PlayerEmulator/Packages/Framework/UI/RangeSlider.lua index ef2d1ba494..cd601a0e07 100644 --- a/BuiltInPlugins/PlayerEmulator/Packages/Framework/UI/RangeSlider.lua +++ b/BuiltInPlugins/PlayerEmulator/Packages/Framework/UI/RangeSlider.lua @@ -8,7 +8,6 @@ number UpperRangeValue: Current value for the upper range knob callback OnValuesChanged: The callback is called whenever the min or max value changes - OnValuesChanged(minValue: number, maxValue: number) Mouse Mouse: A Mouse ContextItem, which is provided via mapToProps. - Theme Theme: A Theme ContextItem, which is provided via mapToProps. Optional Props: Vector2 AnchorPoint: The anchorPoint of the component @@ -22,8 +21,9 @@ StyleModifier StyleModifier: The StyleModifier index into Style. number SnapIncrement: Incremental points that the slider's knob will snap to. A "0" snap increment means no snapping. number VerticalDragTolerance: A vertical pixel height for allowing a mouse button press to drag knobs on outside the component's size. + Theme Theme: A Theme ContextItem, which is provided via mapToProps. + Stylizer Stylizer: A Stylizer ContextItem, which is provided via mapToProps. ]] - local Framework = script.Parent.Parent local Roact = require(Framework.Parent.Roact) local ContextServices = require(Framework.ContextServices) @@ -31,6 +31,9 @@ local ContextServices = require(Framework.ContextServices) local Util = require(Framework.Util) local Typecheck = Util.Typecheck local StyleModifier = Util.StyleModifier +local FlagsList = Util.Flags.new({ + FFlagRefactorDevFrameworkTheme = {"RefactorDevFrameworkTheme"}, +}) local UI = Framework.UI local Container = require(UI.Container) @@ -159,7 +162,12 @@ end function RangeSlider:render() local theme = self.props.Theme - local style = theme:getStyle("Framework", self) + local style + if FlagsList:get("FFlagRefactorDevFrameworkTheme") then + style = self.props.Stylizer + else + style = theme:getStyle("Framework", self) + end local anchorPoint = self.props.AnchorPoint local isDisabled = self.props.Disabled @@ -252,7 +260,8 @@ end ContextServices.mapToProps(RangeSlider, { Mouse = ContextServices.Mouse, - Theme = ContextServices.Theme, + Stylizer = FlagsList:get("FFlagRefactorDevFrameworkTheme") and ContextServices.Stylizer or nil, + Theme = (not FlagsList:get("FFlagRefactorDevFrameworkTheme")) and ContextServices.Theme or nil, }) return RangeSlider \ No newline at end of file diff --git a/BuiltInPlugins/PlayerEmulator/Packages/Framework/UI/RangeSlider/example.lua b/BuiltInPlugins/PlayerEmulator/Packages/Framework/UI/RangeSlider/example.lua index 03f35a00e0..061dbb97a2 100644 --- a/BuiltInPlugins/PlayerEmulator/Packages/Framework/UI/RangeSlider/example.lua +++ b/BuiltInPlugins/PlayerEmulator/Packages/Framework/UI/RangeSlider/example.lua @@ -19,6 +19,13 @@ return function(plugin) local ExampleRangeSlider = Roact.PureComponent:extend("ExampleRangeSlider") + local Util = require(Framework.Util) + local FlagsList = Util.Flags.new({ + FFlagRefactorDevFrameworkTheme = {"RefactorDevFrameworkTheme"}, + }) + + local StudioTheme = require(Framework.Style.Themes.StudioTheme) + local pluginItem = Plugin.new(plugin) local mouse = Mouse.new(plugin:GetMouse()) @@ -38,11 +45,16 @@ return function(plugin) }) end - self.theme = Theme.new(function(theme, getColor) - return { - Framework = StudioFrameworkStyles.new(theme, getColor) - } - end) + self.theme = nil + if FlagsList:get("FFlagRefactorDevFrameworkTheme") then + self.theme = StudioTheme.new() + else + self.theme = Theme.new(function(theme, getColor) + return { + Framework = StudioFrameworkStyles.new(theme, getColor) + } + end) + end self.setValues = function(lowerValue, upperValue) self:setState({ @@ -95,13 +107,14 @@ return function(plugin) UpperRangeValue = 3, Min = MIN_VALUE, Max = MAX_VALUE, + OnValuesChanged = function() end, Position = UDim2.new(0.5, 0, 0.5, 0), Size = UDim2.new(0, 200, 0, 20), }), RangeSliderNoLower = Roact.createElement(RangeSlider, { AnchorPoint = Vector2.new(0.5, 0.5), Disabled = false, - HideLower = true, + HideLowerKnob = true, LowerRangeValue = self.state.currentMin, UpperRangeValue = self.state.currentMax, Min = MIN_VALUE, diff --git a/BuiltInPlugins/PlayerEmulator/Packages/Framework/UI/RangeSlider/style.lua b/BuiltInPlugins/PlayerEmulator/Packages/Framework/UI/RangeSlider/style.lua index b7167886d3..535d967e6b 100644 --- a/BuiltInPlugins/PlayerEmulator/Packages/Framework/UI/RangeSlider/style.lua +++ b/BuiltInPlugins/PlayerEmulator/Packages/Framework/UI/RangeSlider/style.lua @@ -1,9 +1,14 @@ local Framework = script.Parent.Parent.Parent +local StyleKey = require(Framework.Style.StyleKey) + local Util = require(Framework.Util) local Style = Util.Style local StyleModifier = Util.StyleModifier local StyleValue = Util.StyleValue +local FlagsList = Util.Flags.new({ + FFlagRefactorDevFrameworkTheme = {"RefactorDevFrameworkTheme"}, +}) local UI = require(Framework.UI) local Decoration = UI.Decoration @@ -15,65 +20,108 @@ local BAR_HEIGHT = 6 local BAR_SLICE_CENTER = Rect.new(3, 0, 4, 6) local SLIDER_HANDLE_SIZE = 18 -return function(theme, getColor) - local common = Common(theme, getColor) - - local BackgroundColor = StyleValue.new("BackgroundColor", { - Light = Color3.fromRGB(204, 204, 204), - Dark = Color3.fromRGB(37, 37, 37), - }) - - local KnobColor = StyleValue.new("KnobColor", { - Light = Color3.fromRGB(255, 255, 255), - Dark = Color3.fromRGB(85, 85, 85), - }) - - local KnobImage = StyleValue.new("KnobImage", { - Light = "rbxasset://textures/DeveloperFramework/slider_knob_light.png", - Dark = "rbxasset://textures/DeveloperFramework/slider_knob.png", - }) +if FlagsList:get("FFlagRefactorDevFrameworkTheme") then local knobStyle = { AnchorPoint = Vector2.new(0.5, 0.5), - Color = KnobColor:get(theme.Name), - Image = KnobImage:get(theme.Name), + Color = StyleKey.SliderKnobColor, + Image = StyleKey.SliderKnobImage, Size = UDim2.new(0, SLIDER_HANDLE_SIZE, 0, SLIDER_HANDLE_SIZE), [StyleModifier.Disabled] = { - Color = theme:GetColor("Button"), + Color = StyleKey.Button, }, } - local Default = Style.extend(common.MainText, { + return { KnobSize = Vector2.new(18, 18), Background = Decoration.Image, BackgroundStyle = { - AnchorPoint = Vector2.new(0, 0.5), - Color = BackgroundColor:get(theme.Name), + AnchorPoint = Vector2.new(0, 0.5), + Color = StyleKey.SliderBackground, Image = "rbxasset://textures/DeveloperFramework/slider_bg.png", - Position = UDim2.new(0, 0, 0.5, 0), - ScaleType = Enum.ScaleType.Slice, - Size = UDim2.new(UDim.new(1, 0), UDim.new(0, BAR_HEIGHT)), - SliceCenter = BAR_SLICE_CENTER, + Position = UDim2.new(0, 0, 0.5, 0), + ScaleType = Enum.ScaleType.Slice, + Size = UDim2.new(UDim.new(1, 0), UDim.new(0, BAR_HEIGHT)), + SliceCenter = BAR_SLICE_CENTER, }, Foreground = Decoration.Image, ForegroundStyle = { AnchorPoint = Vector2.new(0, 0.5), Image = "rbxasset://textures/DeveloperFramework/slider_bg.png", - Color = theme:GetColor("DialogMainButton"), + Color = StyleKey.DialogMainButton, ScaleType = Enum.ScaleType.Slice, Size = UDim2.new(UDim.new(1, 0), UDim.new(0, BAR_HEIGHT)), SliceCenter = BAR_SLICE_CENTER, [StyleModifier.Disabled] = { - Color = theme:GetColor("Button"), + Color = StyleKey.Button, }, }, LowerKnobBackground = Decoration.Image, LowerKnobBackgroundStyle = knobStyle, UpperKnobBackground = Decoration.Image, UpperKnobBackgroundStyle = knobStyle, - }) - - return { - Default = Default, } -end +else + return function(theme, getColor) + local common = Common(theme, getColor) + + local BackgroundColor = StyleValue.new("BackgroundColor", { + Light = Color3.fromRGB(204, 204, 204), + Dark = Color3.fromRGB(37, 37, 37), + }) + + local KnobColor = StyleValue.new("KnobColor", { + Light = Color3.fromRGB(255, 255, 255), + Dark = Color3.fromRGB(85, 85, 85), + }) + + local KnobImage = StyleValue.new("KnobImage", { + Light = "rbxasset://textures/DeveloperFramework/slider_knob_light.png", + Dark = "rbxasset://textures/DeveloperFramework/slider_knob.png", + }) + + local knobStyle = { + AnchorPoint = Vector2.new(0.5, 0.5), + Color = KnobColor:get(theme.Name), + Image = KnobImage:get(theme.Name), + Size = UDim2.new(0, SLIDER_HANDLE_SIZE, 0, SLIDER_HANDLE_SIZE), + [StyleModifier.Disabled] = { + Color = theme:GetColor("Button"), + }, + } + + local Default = Style.extend(common.MainText, { + KnobSize = Vector2.new(18, 18), + Background = Decoration.Image, + BackgroundStyle = { + AnchorPoint = Vector2.new(0, 0.5), + Color = BackgroundColor:get(theme.Name), + Image = "rbxasset://textures/DeveloperFramework/slider_bg.png", + Position = UDim2.new(0, 0, 0.5, 0), + ScaleType = Enum.ScaleType.Slice, + Size = UDim2.new(UDim.new(1, 0), UDim.new(0, BAR_HEIGHT)), + SliceCenter = BAR_SLICE_CENTER, + }, + Foreground = Decoration.Image, + ForegroundStyle = { + AnchorPoint = Vector2.new(0, 0.5), + Image = "rbxasset://textures/DeveloperFramework/slider_bg.png", + Color = theme:GetColor("DialogMainButton"), + ScaleType = Enum.ScaleType.Slice, + Size = UDim2.new(UDim.new(1, 0), UDim.new(0, BAR_HEIGHT)), + SliceCenter = BAR_SLICE_CENTER, + [StyleModifier.Disabled] = { + Color = theme:GetColor("Button"), + }, + }, + LowerKnobBackground = Decoration.Image, + LowerKnobBackgroundStyle = knobStyle, + UpperKnobBackground = Decoration.Image, + UpperKnobBackgroundStyle = knobStyle, + }) + + return { + Default = Default, + } + end +end \ No newline at end of file diff --git a/BuiltInPlugins/PlayerEmulator/Packages/Framework/UI/RoundBox.lua b/BuiltInPlugins/PlayerEmulator/Packages/Framework/UI/RoundBox.lua index dae4417961..ffff6a790a 100644 --- a/BuiltInPlugins/PlayerEmulator/Packages/Framework/UI/RoundBox.lua +++ b/BuiltInPlugins/PlayerEmulator/Packages/Framework/UI/RoundBox.lua @@ -1,12 +1,11 @@ --[[ A round Box decoration with a border. - Required Props: - Theme Theme: A Theme ContextItem, which is provided via mapToProps. - Optional Props: Style Style: The style with which to render this component. StyleModifier StyleModifier: The StyleModifier index into Style. + Theme Theme: A Theme ContextItem, which is provided via mapToProps. + Stylizer Stylizer: A Stylizer ContextItem, which is provided via mapToProps. Style Values: Color3 Color: The color tint of the image. @@ -22,9 +21,12 @@ local Framework = script.Parent.Parent local Roact = require(Framework.Parent.Roact) local ContextServices = require(Framework.ContextServices) -local Typecheck = require(Framework.Util).Typecheck -local FFlagRoundBoxZIndexProp = game:DefineFastFlag("RoundBoxZIndexProp", false) +local Util = require(Framework.Util) +local Typecheck = Util.Typecheck +local FlagsList = Util.Flags.new({ + FFlagRefactorDevFrameworkTheme = {"RefactorDevFrameworkTheme"}, +}) local RoundBox = Roact.PureComponent:extend("RoundBox") Typecheck.wrap(RoundBox, script) @@ -32,7 +34,12 @@ Typecheck.wrap(RoundBox, script) function RoundBox:render() local props = self.props local theme = props.Theme - local style = theme:getStyle("Framework", self) + local style + if FlagsList:get("FFlagRefactorDevFrameworkTheme") then + style = props.Stylizer + else + style = theme:getStyle("Framework", self) + end local color = style.Color local borderColor = style.BorderColor @@ -52,7 +59,7 @@ function RoundBox:render() Image = backgroundImage, ScaleType = Enum.ScaleType.Slice, SliceCenter = sliceCenter, - ZIndex = FFlagRoundBoxZIndexProp and zIndex or nil + ZIndex = zIndex }, { Border = Roact.createElement("ImageLabel", { Size = UDim2.new(1, 0, 1, 0), @@ -68,7 +75,8 @@ function RoundBox:render() end ContextServices.mapToProps(RoundBox, { - Theme = ContextServices.Theme, + Stylizer = FlagsList:get("FFlagRefactorDevFrameworkTheme") and ContextServices.Stylizer or nil, + Theme = (not FlagsList:get("FFlagRefactorDevFrameworkTheme")) and ContextServices.Theme or nil, }) return RoundBox diff --git a/BuiltInPlugins/PlayerEmulator/Packages/Framework/UI/RoundBox/style.lua b/BuiltInPlugins/PlayerEmulator/Packages/Framework/UI/RoundBox/style.lua index 0355f256cc..8faeba599a 100644 --- a/BuiltInPlugins/PlayerEmulator/Packages/Framework/UI/RoundBox/style.lua +++ b/BuiltInPlugins/PlayerEmulator/Packages/Framework/UI/RoundBox/style.lua @@ -1,23 +1,40 @@ local Framework = script.Parent.Parent.Parent +local StyleKey = require(Framework.Style.StyleKey) + local Util = require(Framework.Util) local Style = Util.Style +local FlagsList = Util.Flags.new({ + FFlagRefactorDevFrameworkTheme = {"RefactorDevFrameworkTheme"}, +}) local StudioFrameworkStyles = Framework.StudioUI.StudioFrameworkStyles local Common = require(StudioFrameworkStyles.Common) -return function(theme, getColor) - local common = Common(theme, getColor) - - local Default = Style.extend(common.Background, common.Border, { +if FlagsList:get("FFlagRefactorDevFrameworkTheme") then + return { + Color = StyleKey.MainBackground, + BorderColor = StyleKey.Border, Transparency = 0, BorderTransparency = 0, BackgroundImage = "rbxasset://textures/StudioToolbox/RoundedBackground.png", BorderImage = "rbxasset://textures/StudioToolbox/RoundedBorder.png", SliceCenter = Rect.new(3, 3, 13, 13), - }) - - return { - Default = Default, } -end +else + return function(theme, getColor) + local common = Common(theme, getColor) + + local Default = Style.extend(common.Background, common.Border, { + Transparency = 0, + BorderTransparency = 0, + BackgroundImage = "rbxasset://textures/StudioToolbox/RoundedBackground.png", + BorderImage = "rbxasset://textures/StudioToolbox/RoundedBorder.png", + SliceCenter = Rect.new(3, 3, 13, 13), + }) + + return { + Default = Default, + } + end +end \ No newline at end of file diff --git a/BuiltInPlugins/PlayerEmulator/Packages/Framework/UI/RoundBox/test.spec.lua b/BuiltInPlugins/PlayerEmulator/Packages/Framework/UI/RoundBox/test.spec.lua index 3110398a8d..d9715cf163 100644 --- a/BuiltInPlugins/PlayerEmulator/Packages/Framework/UI/RoundBox/test.spec.lua +++ b/BuiltInPlugins/PlayerEmulator/Packages/Framework/UI/RoundBox/test.spec.lua @@ -7,12 +7,24 @@ return function() local FrameworkStyles = require(Framework.UI.FrameworkStyles) local RoundBox = require(script.Parent) + local StudioTheme = require(Framework.Style.Themes.StudioTheme) + + local Util = require(Framework.Util) + local FlagsList = Util.Flags.new({ + FFlagRefactorDevFrameworkTheme = {"RefactorDevFrameworkTheme"}, + }) + local function createTestRoundBoxDecoration() - local theme = Theme.new(function() - return { - Framework = FrameworkStyles.new(), - } - end) + local theme + if FlagsList:get("FFlagRefactorDevFrameworkTheme") then + theme = StudioTheme.mock() + else + theme = Theme.new(function() + return { + Framework = FrameworkStyles.new(), + } + end) + end return provide({theme}, { RoundBox = Roact.createElement(RoundBox), }) diff --git a/BuiltInPlugins/PlayerEmulator/Packages/Framework/UI/ScrollingFrame.lua b/BuiltInPlugins/PlayerEmulator/Packages/Framework/UI/ScrollingFrame.lua index 58a2082545..6c62cf563e 100644 --- a/BuiltInPlugins/PlayerEmulator/Packages/Framework/UI/ScrollingFrame.lua +++ b/BuiltInPlugins/PlayerEmulator/Packages/Framework/UI/ScrollingFrame.lua @@ -2,11 +2,7 @@ A scrolling frame with a colored background, providing a consistent look with the native Studio Start Page. - Required Props: - Theme Theme: the theme supplied from mapToProps() - Optional Props: - Style Style: a style table supplied from props and theme:getStyle() callback OnScrollUpdate: A callback function that will update the index change. UDim2 Position: The position of the scrolling frame. UDim2 Size: The size of the scrolling frame. @@ -15,6 +11,9 @@ table AutoSizeLayoutOptions: The options of the UILayout instance if auto-sizing. UDim2 CanvasSize: The size of the scrolling frame's canvas. integer ElementPadding: The padding between children when AutoSizeCanvas is true. + Style Style: a style table supplied from props and theme:getStyle() + Stylizer Stylizer: A Stylizer ContextItem, which is provided via mapToProps + Theme Theme: the theme supplied from mapToProps() Style Values: string BottomImage: The image that appears in the bottom 3rd of the scrollbar @@ -26,12 +25,13 @@ boolean ScrollingEnabled: Whether scrolling in this frame will change the CanvasPosition. integer ZIndex: The draw index of the frame. ]] - local Framework = script.Parent.Parent local Roact = require(Framework.Parent.Roact) local Util = require(Framework.Util) local FlagsList = Util.Flags.new({ + FFlagRefactorDevFrameworkTheme = {"RefactorDevFrameworkTheme"}, FFlagStudioDevFrameworkPackage = {"StudioDevFrameworkPackage"}, + FFlagFixContentNotFullyShownAfterResize = {"FixContentNotFullyShownAfterResize"}, }) local Cryo local isUsedAsPackage = require(Framework.Util.isUsedAsPackage) @@ -51,6 +51,18 @@ local Typecheck = Util.Typecheck local ScrollingFrame = Roact.PureComponent:extend("ScrollingFrame") Typecheck.wrap(ScrollingFrame, script) +local function getStyle(self) + local props = self.props + local theme = props.Theme + local style + if FlagsList:get("FFlagRefactorDevFrameworkTheme") then + style = props.Stylizer + else + style = theme:getStyle("Framework", self) + end + return style +end + function ScrollingFrame:init() self.scrollingRef = Roact.createRef() self.layoutRef = Roact.createRef() @@ -64,7 +76,23 @@ function ScrollingFrame:init() self.updateCanvasSize = function(rbx) if self.scrollingRef.current and self.layoutRef.current then local contentSize = self.layoutRef.current.AbsoluteContentSize - self.scrollingRef.current.CanvasSize = UDim2.new(0, contentSize.X, 0, contentSize.Y) + local contentSizeX = contentSize.X + local contentSizeY = contentSize.Y + if FlagsList:get("FFlagFixContentNotFullyShownAfterResize") then + local props = self.props + local style = getStyle(self) + local scrollingFrameProps = self.getScrollingFrameProps(props, style) + -- for vertical scroll, canvas size on x axis should not update when content size changes + -- for horizon one, y axis should not change + -- for both scrolling, canvas size can be fully controlled by content + if scrollingFrameProps.ScrollingDirection == Enum.ScrollingDirection.Y then + contentSizeX = 0 + elseif scrollingFrameProps.ScrollingDirection == Enum.ScrollingDirection.X then + contentSizeY = 0 + end + end + + self.scrollingRef.current.CanvasSize = UDim2.new(0, contentSizeX, 0, contentSizeY) end end @@ -84,14 +112,23 @@ function ScrollingFrame:init() self.getScrollingFrameProps = function(props, style) -- after filtering out parent's props and other component specific props, -- what is left should be ScrollingFrame specific props + local updatedProps + if FlagsList:get("FFlagRefactorDevFrameworkTheme") then + updatedProps = Cryo.Dictionary.join(props, { + Stylizer = Cryo.None + }) + else + updatedProps = props + end return Cryo.Dictionary.join( style, - props, + updatedProps, self.propFilters.parentContainerProps, { Size = UDim2.new(1, 0, 1, 0), [Roact.Children] = Cryo.None, [Roact.Change.CanvasPosition] = self.onScroll, + [Roact.Change.AbsoluteSize] = FlagsList:get("FFlagFixContentNotFullyShownAfterResize") and self.updateCanvasSize or nil, [Roact.Ref] = self.scrollingRef, }) end @@ -103,8 +140,7 @@ end function ScrollingFrame:render() local props = self.props - local theme = props.Theme - local style = theme:getStyle("Framework", self) + local style = getStyle(self) local position = props.Position local size = props.Size @@ -139,8 +175,8 @@ function ScrollingFrame:render() end ContextServices.mapToProps(ScrollingFrame, { - Theme = ContextServices.Theme, + Stylizer = FlagsList:get("FFlagRefactorDevFrameworkTheme") and ContextServices.Stylizer or nil, + Theme = (not FlagsList:get("FFlagRefactorDevFrameworkTheme")) and ContextServices.Theme or nil, }) - return ScrollingFrame \ No newline at end of file diff --git a/BuiltInPlugins/PlayerEmulator/Packages/Framework/UI/ScrollingFrame/example.lua b/BuiltInPlugins/PlayerEmulator/Packages/Framework/UI/ScrollingFrame/example.lua index 87b1ed2ce1..3fc04c911d 100644 --- a/BuiltInPlugins/PlayerEmulator/Packages/Framework/UI/ScrollingFrame/example.lua +++ b/BuiltInPlugins/PlayerEmulator/Packages/Framework/UI/ScrollingFrame/example.lua @@ -6,7 +6,6 @@ return function(plugin) local Theme = ContextServices.Theme local UI = require(Framework.UI) - local Container = UI.Container local ScrollingFrame = UI.ScrollingFrame local StudioUI = require(Framework.StudioUI) @@ -16,15 +15,21 @@ return function(plugin) local pluginItem = Plugin.new(plugin) local Util = require(Framework.Util) - local StyleTable = Util.StyleTable - local Style = Util.Style - local StyleModifier = Util.StyleModifier - - local theme = Theme.new(function(theme, getColor) - return { - Framework = StudioFrameworkStyles.new(theme, getColor) - } - end) + local FlagsList = Util.Flags.new({ + FFlagRefactorDevFrameworkTheme = {"RefactorDevFrameworkTheme"}, + }) + local StudioTheme = require(Framework.Style.Themes.StudioTheme) + + local theme + if FlagsList:get("FFlagRefactorDevFrameworkTheme") then + theme = StudioTheme.new() + else + theme = Theme.new(function(theme, getColor) + return { + Framework = StudioFrameworkStyles.new(theme, getColor) + } + end) + end local ExampleButton = Roact.PureComponent:extend("ExampleButton") @@ -56,10 +61,9 @@ return function(plugin) }) end - return ContextServices.provide({pluginItem, theme}, { Main = Roact.createElement(Dialog, { - Enabled = enabled, + Enabled = self.state.enabled, Title = "ToggleButton Example", Size = Vector2.new(200, 200), Resizable = false, diff --git a/BuiltInPlugins/PlayerEmulator/Packages/Framework/UI/ScrollingFrame/style.lua b/BuiltInPlugins/PlayerEmulator/Packages/Framework/UI/ScrollingFrame/style.lua index 2e46b067f9..2b68854f59 100644 --- a/BuiltInPlugins/PlayerEmulator/Packages/Framework/UI/ScrollingFrame/style.lua +++ b/BuiltInPlugins/PlayerEmulator/Packages/Framework/UI/ScrollingFrame/style.lua @@ -1,23 +1,41 @@ local Framework = script.Parent.Parent.Parent local Util = require(Framework.Util) +local Cryo = Util.Cryo +local deepCopy = Util.deepCopy local Style = Util.Style +local FlagsList = Util.Flags.new({ + FFlagRefactorDevFrameworkTheme = {"RefactorDevFrameworkTheme"}, +}) local StudioFrameworkStyles = Framework.StudioUI.StudioFrameworkStyles local Common = require(StudioFrameworkStyles.Common) +local UIFolderData = require(Framework.UI.UIFolderData) +local InfiniteScrollingFrame = require(UIFolderData.InfiniteScrollingFrame.style) -return function(theme, getColor) - local common = Common(theme, getColor) - - local Default = Style.extend(common.Scroller, { +if FlagsList:get("FFlagRefactorDevFrameworkTheme") then + local infiniteScrollingFrame = deepCopy(InfiniteScrollingFrame) + return Cryo.Dictionary.join(infiniteScrollingFrame, { AutoSizeCanvas = true, AutoSizeLayoutElement = "UIListLayout", AutoSizeLayoutOptions = { Padding = UDim.new(0, 4), }, }) +else + return function(theme, getColor) + local common = Common(theme, getColor) + + local Default = Style.extend(common.Scroller, { + AutoSizeCanvas = true, + AutoSizeLayoutElement = "UIListLayout", + AutoSizeLayoutOptions = { + Padding = UDim.new(0, 4), + }, + }) - return { - Default = Default, - } + return { + Default = Default, + } + end end diff --git a/BuiltInPlugins/PlayerEmulator/Packages/Framework/UI/SelectInput.lua b/BuiltInPlugins/PlayerEmulator/Packages/Framework/UI/SelectInput.lua index dfb8b097fb..a86a9ee2d4 100644 --- a/BuiltInPlugins/PlayerEmulator/Packages/Framework/UI/SelectInput.lua +++ b/BuiltInPlugins/PlayerEmulator/Packages/Framework/UI/SelectInput.lua @@ -4,13 +4,15 @@ Required Props: table Items: An array of each item that should appear in the dropdown. callback OnItemActivated: A callback for when the user selects a dropdown entry. - Theme Theme: a Theme object supplied by mapToProps() Focus Focus: a Focus object supplied by mapToProps() Optional Props: string PlaceholderText: A placeholder to display if there is no item selected. callback OnRenderItem: A function used to render a dropdown menu item. number SelectedIndex: The currently selected item index. + Style Style: The style with which to render this component. + Stylizer Stylizer: A Stylizer ContextItem, which is provided via mapToProps. + Theme Theme: a Theme object supplied by mapToProps() Style Values: Style BackgroundStyle: The style with which to render the background. @@ -36,6 +38,10 @@ local StyleModifier = Util.StyleModifier local SelectInput = Roact.PureComponent:extend("SelectInput") Typecheck.wrap(SelectInput, script) +local FlagsList = Util.Flags.new({ + FFlagRefactorDevFrameworkTheme = {"RefactorDevFrameworkTheme"}, +}) + local BORDER_SIZE = 1 function SelectInput:init() @@ -73,7 +79,13 @@ end function SelectInput:render() local props = self.props local state = self.state - local style = props.Theme:getStyle("Framework", self) + + local style + if FlagsList:get("FFlagRefactorDevFrameworkTheme") then + style = props.Stylizer + else + style = props.Theme:getStyle("Framework", self) + end local items = props.Items local isOpen = state.isOpen @@ -126,7 +138,8 @@ end ContextServices.mapToProps(SelectInput, { Focus = ContextServices.Focus, - Theme = ContextServices.Theme, + Stylizer = FlagsList:get("FFlagRefactorDevFrameworkTheme") and ContextServices.Stylizer or nil, + Theme = (not FlagsList:get("FFlagRefactorDevFrameworkTheme")) and ContextServices.Theme or nil, }) return SelectInput diff --git a/BuiltInPlugins/PlayerEmulator/Packages/Framework/UI/SelectInput/style.lua b/BuiltInPlugins/PlayerEmulator/Packages/Framework/UI/SelectInput/style.lua index 9ce9585bda..56453ad7f6 100644 --- a/BuiltInPlugins/PlayerEmulator/Packages/Framework/UI/SelectInput/style.lua +++ b/BuiltInPlugins/PlayerEmulator/Packages/Framework/UI/SelectInput/style.lua @@ -3,38 +3,73 @@ local Framework = script.Parent.Parent.Parent local StudioFrameworkStyles = Framework.StudioUI.StudioFrameworkStyles local Common = require(StudioFrameworkStyles.Common) local UIFolderData = require(Framework.UI.UIFolderData) -local Util = require(Framework.Util) local RoundBox = require(UIFolderData.RoundBox.style) + +local StyleKey = require(Framework.Style.StyleKey) + +local Util = require(Framework.Util) +local Cryo = Util.Cryo +local deepCopy = Util.deepCopy local Style = Util.Style local StyleModifier = Util.StyleModifier +local FlagsList = Util.Flags.new({ + FFlagRefactorDevFrameworkTheme = {"RefactorDevFrameworkTheme"}, +}) -return function(theme, getColor) - local common = Common(theme, getColor) - local roundBox = RoundBox(theme, getColor) - +if FlagsList:get("FFlagRefactorDevFrameworkTheme") then + local roundBox = deepCopy(RoundBox) return { - Default = Style.extend(common.Border, { - Padding = 10, - BackgroundStyle = roundBox.Default, - [StyleModifier.Hover] = { - BackgroundStyle = Style.extend(roundBox.Default, common.BorderHover), - }, - DropdownMenu = { - BackgroundStyle = roundBox.Default, - Width = 240, - MaxHeight = 240, - Offset = Vector2.new(0, 0), - }, - Size = UDim2.new(0, 240, 0, 32), - ArrowOffset = 10, - ArrowSize = UDim2.new(0, 12, 0, 12), - ArrowImage = "rbxasset://textures/StudioToolbox/ArrowDownIconWhite.png", - ArrowColor = theme:GetColor("MainText"), - PlaceholderTextColor = theme:GetColor("DimmedText"), - Text = Style.extend(common.MainText, { - TextXAlignment = Enum.TextXAlignment.Left, - }) - }) + Padding = 10, + BackgroundStyle = roundBox, + [StyleModifier.Hover] = { + BackgroundStyle = Cryo.Dictionary.join(roundBox, { + BorderColor = StyleKey.DialogMainButton, + }), + }, + DropdownMenu = { + BackgroundStyle = roundBox, + Width = 240, + MaxHeight = 240, + Offset = Vector2.new(0, 0), + }, + Size = UDim2.new(0, 240, 0, 32), + ArrowOffset = 10, + ArrowSize = UDim2.new(0, 12, 0, 12), + ArrowImage = "rbxasset://textures/StudioToolbox/ArrowDownIconWhite.png", + ArrowColor = StyleKey.MainText, + PlaceholderTextColor = StyleKey.DimmedText, + Text = Cryo.Dictionary.join(Common.MainText, { + TextXAlignment = Enum.TextXAlignment.Left, + }), } +else + return function(theme, getColor) + local common = Common(theme, getColor) + local roundBox = RoundBox(theme, getColor) + return { + Default = Style.extend(common.Border, { + Padding = 10, + BackgroundStyle = roundBox.Default, + [StyleModifier.Hover] = { + BackgroundStyle = Style.extend(roundBox.Default, common.BorderHover), + }, + DropdownMenu = { + BackgroundStyle = roundBox.Default, + Width = 240, + MaxHeight = 240, + Offset = Vector2.new(0, 0), + }, + Size = UDim2.new(0, 240, 0, 32), + ArrowOffset = 10, + ArrowSize = UDim2.new(0, 12, 0, 12), + ArrowImage = "rbxasset://textures/StudioToolbox/ArrowDownIconWhite.png", + ArrowColor = theme:GetColor("MainText"), + PlaceholderTextColor = theme:GetColor("DimmedText"), + Text = Style.extend(common.MainText, { + TextXAlignment = Enum.TextXAlignment.Left, + }) + }) + } + end end \ No newline at end of file diff --git a/BuiltInPlugins/PlayerEmulator/Packages/Framework/UI/Separator.lua b/BuiltInPlugins/PlayerEmulator/Packages/Framework/UI/Separator.lua index b32f8ad0ba..910b2b6960 100644 --- a/BuiltInPlugins/PlayerEmulator/Packages/Framework/UI/Separator.lua +++ b/BuiltInPlugins/PlayerEmulator/Packages/Framework/UI/Separator.lua @@ -1,9 +1,6 @@ --[[ A simple border to separate elements. - Required Props: - Theme Theme: A Theme ContextItem, which is provided via mapToProps. - Optional Props: Enum.DominantAxis DominantAxis: Specifies whether the separator fills the space horizontally or vertically. Width will make the separator @@ -12,7 +9,9 @@ number LayoutOrder: The layout order of this component in a UILayout. UDim2 Position: The position of the center of the separator. Style Style: The style with which to render this component. + Stylizer Stylizer: A Stylizer ContextItem, which is provided via mapToProps. StyleModifier StyleModifier: The StyleModifier index into Style. + Theme Theme: A Theme ContextItem, which is provided via mapToProps. number ZIndex: The render index of this component. Style Values: @@ -20,12 +19,16 @@ number StretchMargin: The padding in pixels to subtract from either side of the separator's dominant axis. number Weight: The thickness of the separator line. ]] - local Framework = script.Parent.Parent local Roact = require(Framework.Parent.Roact) local ContextServices = require(Framework.ContextServices) local t = require(Framework.Util.Typecheck.t) -local Typecheck = require(Framework.Util).Typecheck + +local Util = require(Framework.Util) +local Typecheck = Util.Typecheck +local FlagsList = Util.Flags.new({ + FFlagRefactorDevFrameworkTheme = {"RefactorDevFrameworkTheme"}, +}) local Separator = Roact.PureComponent:extend("Separator") Typecheck.wrap(Separator, script) @@ -39,7 +42,13 @@ function Separator:render() local dominantAxis = props.DominantAxis or Enum.DominantAxis.Width local theme = props.Theme - local style = theme:getStyle("Framework", self) + + local style + if FlagsList:get("FFlagRefactorDevFrameworkTheme") then + style = props.Stylizer + else + style = theme:getStyle("Framework", self) + end local color = style.Color local stretchMargin = style.StretchMargin @@ -67,7 +76,8 @@ function Separator:render() end ContextServices.mapToProps(Separator, { - Theme = ContextServices.Theme, + Stylizer = FlagsList:get("FFlagRefactorDevFrameworkTheme") and ContextServices.Stylizer or nil, + Theme = (not FlagsList:get("FFlagRefactorDevFrameworkTheme")) and ContextServices.Theme or nil, }) return Separator diff --git a/BuiltInPlugins/PlayerEmulator/Packages/Framework/UI/Separator/style.lua b/BuiltInPlugins/PlayerEmulator/Packages/Framework/UI/Separator/style.lua index f649ad9fd6..6be52cc262 100644 --- a/BuiltInPlugins/PlayerEmulator/Packages/Framework/UI/Separator/style.lua +++ b/BuiltInPlugins/PlayerEmulator/Packages/Framework/UI/Separator/style.lua @@ -1,17 +1,30 @@ local Framework = script.Parent.Parent.Parent +local StyleKey = require(Framework.Style.StyleKey) + local Util = require(Framework.Util) local Style = Util.Style +local FlagsList = Util.Flags.new({ + FFlagRefactorDevFrameworkTheme = {"RefactorDevFrameworkTheme"}, +}) -return function(theme, getColor) - - local Default = Style.new({ - Color = theme:GetColor("Border"), +if FlagsList:get("FFlagRefactorDevFrameworkTheme") then + return { + Color = StyleKey.Border, StretchMargin = 0, Weight = 1 - }) - - return { - Default = Default, } -end +else + return function(theme, getColor) + + local Default = Style.new({ + Color = theme:GetColor("Border"), + StretchMargin = 0, + Weight = 1 + }) + + return { + Default = Default, + } + end +end \ No newline at end of file diff --git a/BuiltInPlugins/PlayerEmulator/Packages/Framework/UI/Slider/test.spec.lua b/BuiltInPlugins/PlayerEmulator/Packages/Framework/UI/Slider/test.spec.lua index 36b9c3be38..5f1d7d8c29 100644 --- a/BuiltInPlugins/PlayerEmulator/Packages/Framework/UI/Slider/test.spec.lua +++ b/BuiltInPlugins/PlayerEmulator/Packages/Framework/UI/Slider/test.spec.lua @@ -43,8 +43,6 @@ return function() local element = createTestSlider(props) local instance = Roact.mount(element, container) - container.Parent = workspace - local frame = container:FindFirstChildOfClass("Frame") expect(frame).to.be.ok() @@ -56,4 +54,4 @@ return function() Roact.unmount(instance) end) -end \ No newline at end of file +end diff --git a/BuiltInPlugins/PlayerEmulator/Packages/Framework/UI/TextInput.lua b/BuiltInPlugins/PlayerEmulator/Packages/Framework/UI/TextInput.lua index 4e84e6536e..98aef86460 100644 --- a/BuiltInPlugins/PlayerEmulator/Packages/Framework/UI/TextInput.lua +++ b/BuiltInPlugins/PlayerEmulator/Packages/Framework/UI/TextInput.lua @@ -5,9 +5,6 @@ It does not handle labels, error messages or tooltips. They should be implemented by higher order wrappers. Descended from TextEntry in UILibrary and LabeledTextInput in TerrainTools. - Required Props: - Theme Theme: A Theme ContextItem, which is provided via mapToProps. - Optional Props: boolean Enabled: Whether the input is editable. Defaults to true. number LayoutOrder: The layout order of this component in a list. @@ -17,6 +14,8 @@ string PlaceholderText: Placeholder text to show when the input is empty. string Text: Text to populate the input with. Style Style: The style with which to render this component. + Stylizer Stylizer: A Stylizer ContextItem, which is provided via mapToProps. + Theme Theme: A Theme ContextItem, which is provided via mapToProps. StyleModifier StyleModifier: The StyleModifier index into Style. boolean ShouldFocus: Set focus onto the box so that the user can start typing. UDim2 Position: The position of this component. @@ -31,12 +30,17 @@ number TextSize: The font size of the text. Color3 TextColor: The color of the search term text. ]] - local Framework = script.Parent.Parent local Roact = require(Framework.Parent.Roact) local ContextServices = require(Framework.ContextServices) -local Typecheck = require(Framework.Util).Typecheck + +local Util = require(Framework.Util) +local Typecheck = Util.Typecheck +local FlagsList = Util.Flags.new({ + FFlagRefactorDevFrameworkTheme = {"RefactorDevFrameworkTheme"}, +}) + local Container = require(Framework.UI.Container) local RoundBox = require(Framework.UI.RoundBox) local StyleModifier = require(Framework.Util.StyleModifier) @@ -98,7 +102,13 @@ function TextInput:render() local position = props.Position local theme = props.Theme - local style = theme:getStyle("Framework", self) + local style + if FlagsList:get("FFlagRefactorDevFrameworkTheme") then + style = props.Stylizer + else + style = theme:getStyle("Framework", self) + end + local font = style.Font local textSize = style.TextSize local textColor = style.TextColor @@ -179,7 +189,8 @@ function TextInput:render() end ContextServices.mapToProps(TextInput, { - Theme = ContextServices.Theme, + Stylizer = FlagsList:get("FFlagRefactorDevFrameworkTheme") and ContextServices.Stylizer or nil, + Theme = (not FlagsList:get("FFlagRefactorDevFrameworkTheme")) and ContextServices.Theme or nil, }) return TextInput \ No newline at end of file diff --git a/BuiltInPlugins/PlayerEmulator/Packages/Framework/UI/TextInput/style.lua b/BuiltInPlugins/PlayerEmulator/Packages/Framework/UI/TextInput/style.lua index 921e462009..ee8b7da296 100644 --- a/BuiltInPlugins/PlayerEmulator/Packages/Framework/UI/TextInput/style.lua +++ b/BuiltInPlugins/PlayerEmulator/Packages/Framework/UI/TextInput/style.lua @@ -1,39 +1,66 @@ local Framework = script.Parent.Parent.Parent +local StyleKey = require(Framework.Style.StyleKey) + local Util = require(Framework.Util) +local Cryo = Util.Cryo +local deepCopy = Util.deepCopy local Style = Util.Style local StyleModifier = Util.StyleModifier +local FlagsList = Util.Flags.new({ + FFlagRefactorDevFrameworkTheme = {"RefactorDevFrameworkTheme"}, +}) local Common = require(Framework.StudioUI.StudioFrameworkStyles.Common) local UIFolderData = require(Framework.UI.UIFolderData) -local UI = require(Framework.UI) -local Container = UI.Container local RoundBox = require(UIFolderData.RoundBox.style) -return function(theme, getColor) - local common = Common(theme, getColor) +if FlagsList:get("FFlagRefactorDevFrameworkTheme") then + local roundBox = deepCopy(RoundBox) + return { + PlaceholderTextColor = StyleKey.DimmedText, + + ["&RoundedBorder"] = { + Padding = { + Left = 10, + Top = 5, + Right = 10, + Bottom = 5 + }, + BackgroundStyle = RoundBox, + [StyleModifier.Hover] = { + BackgroundStyle = Cryo.Dictionary.join(roundBox, { + BorderColor = StyleKey.DialogMainButton, + }) + }, + } + } +else + return function(theme, getColor) + local common = Common(theme, getColor) - local Default = Style.extend(common.MainText, common.Border, { - PlaceholderTextColor = theme:GetColor("DimmedText"), - }) + local Default = Style.extend(common.MainText, common.Border, { + PlaceholderTextColor = theme:GetColor("DimmedText"), + }) - local roundBox = RoundBox(theme, getColor) + local roundBox = RoundBox(theme, getColor) - local RoundedBorder = Style.extend(Default, { - Padding = { - Left = 10, - Top = 5, - Right = 10, - Bottom = 5 - }, - BackgroundStyle = roundBox.Default, - [StyleModifier.Hover] = { - BackgroundStyle = Style.extend(roundBox.Default, common.BorderHover) - }, - }) + local RoundedBorder = Style.extend(Default, { + Padding = { + Left = 10, + Top = 5, + Right = 10, + Bottom = 5 + }, + BackgroundStyle = roundBox.Default, + [StyleModifier.Hover] = { + BackgroundStyle = Style.extend(roundBox.Default, common.BorderHover) + }, + }) - return { - Default = Default, - RoundedBorder = RoundedBorder - } + return { + Default = Default, + RoundedBorder = RoundedBorder + } + end end diff --git a/BuiltInPlugins/PlayerEmulator/Packages/Framework/UI/TextLabel.lua b/BuiltInPlugins/PlayerEmulator/Packages/Framework/UI/TextLabel.lua index 62fd8f0067..a22d1ccbda 100644 --- a/BuiltInPlugins/PlayerEmulator/Packages/Framework/UI/TextLabel.lua +++ b/BuiltInPlugins/PlayerEmulator/Packages/Framework/UI/TextLabel.lua @@ -3,9 +3,10 @@ Required Props: string Text: The text to display in this button. - Theme Theme: A Theme ContextItem, which is provided via mapToProps. Optional Props: + Theme Theme: A Theme ContextItem, which is provided via mapToProps. + Stylizer Stylizer: A Stylizer ContextItem, which is provided via mapToProps. number LayoutOrder: The layout order of this component in a list. UDim2 Size: The size of this component. UDim2 Position: The position of this component. @@ -28,15 +29,17 @@ local Framework = script.Parent.Parent local Roact = require(Framework.Parent.Roact) local ContextServices = require(Framework.ContextServices) + local Util = require(Framework.Util) local Typecheck = Util.Typecheck local prioritize = Util.prioritize +local FlagsList = Util.Flags.new({ + FFlagRefactorDevFrameworkTheme = {"RefactorDevFrameworkTheme"}, +}) local TextLabel = Roact.PureComponent:extend("TextLabel") Typecheck.wrap(TextLabel, script) -local FFlagTextLabelProps = game:DefineFastFlag("TextLabelProps", false) - function TextLabel:render() local layoutOrder = self.props.LayoutOrder local size = self.props.Size @@ -45,7 +48,13 @@ function TextLabel:render() local theme = self.props.Theme local textWrapped = self.props.TextWrapped local zIndex = self.props.ZIndex - local style = theme:getStyle("Framework", self) + + local style + if FlagsList:get("FFlagRefactorDevFrameworkTheme") then + style = self.props.Stylizer + else + style = theme:getStyle("Framework", self) + end local backgroundTransparency = prioritize(self.props.BackgroundTransparency, style.BackgroundTransparency, 1) local font = prioritize(self.props.Font, style.Font) @@ -54,8 +63,6 @@ function TextLabel:render() local transparency = prioritize(self.props.TextTransparency, style.TextTransparency) local textXAlignment = prioritize(self.props.TextXAlignment, style.TextXAlignment) local textYAlignment = prioritize(self.props.TextYAlignment, style.TextYAlignment) - local position = self.props.Position - return Roact.createElement("TextLabel", { BackgroundTransparency = backgroundTransparency, @@ -67,15 +74,16 @@ function TextLabel:render() TextColor3 = textColor, TextSize = textSize, TextTransparency = transparency, - TextWrapped = FFlagTextLabelProps and textWrapped or nil, + TextWrapped = textWrapped, TextXAlignment = textXAlignment, TextYAlignment = textYAlignment, - ZIndex = FFlagTextLabelProps and zIndex or nil, + ZIndex = zIndex, }, self.props[Roact.Children]) end ContextServices.mapToProps(TextLabel, { - Theme = ContextServices.Theme, + Stylizer = FlagsList:get("FFlagRefactorDevFrameworkTheme") and ContextServices.Stylizer or nil, + Theme = (not FlagsList:get("FFlagRefactorDevFrameworkTheme")) and ContextServices.Theme or nil, }) return TextLabel diff --git a/BuiltInPlugins/PlayerEmulator/Packages/Framework/UI/TextLabel/style.lua b/BuiltInPlugins/PlayerEmulator/Packages/Framework/UI/TextLabel/style.lua index d48aec8fa4..ac53342673 100644 --- a/BuiltInPlugins/PlayerEmulator/Packages/Framework/UI/TextLabel/style.lua +++ b/BuiltInPlugins/PlayerEmulator/Packages/Framework/UI/TextLabel/style.lua @@ -2,21 +2,32 @@ local Framework = script.Parent.Parent.Parent local Util = require(Framework.Util) local Style = Util.Style +local FlagsList = Util.Flags.new({ + FFlagRefactorDevFrameworkTheme = {"RefactorDevFrameworkTheme"}, +}) local StudioFrameworkStyles = Framework.StudioUI.StudioFrameworkStyles local Common = require(StudioFrameworkStyles.Common) local StyleModifier = Util.StyleModifier -return function(theme, getColor) - local common = Common(theme, getColor) - - local Default = Style.extend(common.MainText, { +if FlagsList:get("FFlagRefactorDevFrameworkTheme") then + return { [StyleModifier.Disabled] = { TextTransparency = 0.5, }, - }) - - return { - Default = Default, } -end +else + return function(theme, getColor) + local common = Common(theme, getColor) + + local Default = Style.extend(common.MainText, { + [StyleModifier.Disabled] = { + TextTransparency = 0.5, + }, + }) + + return { + Default = Default, + } + end +end \ No newline at end of file diff --git a/BuiltInPlugins/PlayerEmulator/Packages/Framework/UI/TextLabel/test.spec.lua b/BuiltInPlugins/PlayerEmulator/Packages/Framework/UI/TextLabel/test.spec.lua index d8788444dc..5f739cb55f 100644 --- a/BuiltInPlugins/PlayerEmulator/Packages/Framework/UI/TextLabel/test.spec.lua +++ b/BuiltInPlugins/PlayerEmulator/Packages/Framework/UI/TextLabel/test.spec.lua @@ -7,12 +7,24 @@ return function() local FrameworkStyles = require(Framework.UI.FrameworkStyles) local TextLabel = require(script.Parent) + local StudioTheme = require(Framework.Style.Themes.StudioTheme) + + local Util = require(Framework.Util) + local FlagsList = Util.Flags.new({ + FFlagRefactorDevFrameworkTheme = {"RefactorDevFrameworkTheme"}, + }) + local function createTestTextLabelDecoration() - local theme = Theme.new(function() - return { - Framework = FrameworkStyles.new(), - } - end) + local theme + if FlagsList:get("FFlagRefactorDevFrameworkTheme") then + theme = StudioTheme.mock() + else + theme = Theme.new(function() + return { + Framework = FrameworkStyles.new(), + } + end) + end return provide({theme}, { TextLabel = Roact.createElement(TextLabel,{ Text = "hello world" diff --git a/BuiltInPlugins/PlayerEmulator/Packages/Framework/UI/ToggleButton.lua b/BuiltInPlugins/PlayerEmulator/Packages/Framework/UI/ToggleButton.lua index 14676ca8a7..84fadc753c 100644 --- a/BuiltInPlugins/PlayerEmulator/Packages/Framework/UI/ToggleButton.lua +++ b/BuiltInPlugins/PlayerEmulator/Packages/Framework/UI/ToggleButton.lua @@ -4,9 +4,11 @@ Required Props: callback OnClick: The function that will be called when this button is clicked to turn on and off. - Theme Theme: A Theme ContextItem, which is provided via mapToProps. Optional Props: + Style Style: The style with which to render this component. + Theme Theme: A Theme ContextItem, which is provided via mapToProps. + Stylizer Stylizer: A Stylizer ContextItem, which is provided via mapToProps. Vector2 AnchorPoint: The pivot point of this component's Position prop. boolean Disabled: Whether or not this button can be clicked. number LayoutOrder: The layout order of this component. @@ -17,13 +19,15 @@ string Text: A text to be displayed over the image if any. number ZIndex: The render index of this component. ]] - local Framework = script.Parent.Parent local Roact = require(Framework.Parent.Roact) local ContextServices = require(Framework.ContextServices) local Util = require(Framework.Util) local Typecheck = Util.Typecheck +local FlagsList = Util.Flags.new({ + FFlagRefactorDevFrameworkTheme = {"RefactorDevFrameworkTheme"}, +}) local Button = require(Framework.UI.Button) local HoverArea = require(Framework.UI.HoverArea) @@ -59,7 +63,12 @@ function ToggleButton:render() local theme = self.props.Theme local zIndex = self.props.ZIndex - local style = theme:getStyle("Framework", self) + local style + if FlagsList:get("FFlagRefactorDevFrameworkTheme") then + style = self.props.Stylizer + else + style = theme:getStyle("Framework", self) + end local styleModifier if isDisabled then @@ -84,7 +93,8 @@ function ToggleButton:render() end ContextServices.mapToProps(ToggleButton, { - Theme = ContextServices.Theme, + Stylizer = FlagsList:get("FFlagRefactorDevFrameworkTheme") and ContextServices.Stylizer or nil, + Theme = (not FlagsList:get("FFlagRefactorDevFrameworkTheme")) and ContextServices.Theme or nil, }) return ToggleButton diff --git a/BuiltInPlugins/PlayerEmulator/Packages/Framework/UI/ToggleButton/example.lua b/BuiltInPlugins/PlayerEmulator/Packages/Framework/UI/ToggleButton/example.lua index 2497509430..f7a98aba24 100644 --- a/BuiltInPlugins/PlayerEmulator/Packages/Framework/UI/ToggleButton/example.lua +++ b/BuiltInPlugins/PlayerEmulator/Packages/Framework/UI/ToggleButton/example.lua @@ -19,15 +19,21 @@ return function(plugin) local pluginItem = Plugin.new(plugin) local Util = require(Framework.Util) - local StyleTable = Util.StyleTable - local Style = Util.Style - local StyleModifier = Util.StyleModifier + local FlagsList = Util.Flags.new({ + FFlagRefactorDevFrameworkTheme = {"RefactorDevFrameworkTheme"}, + }) + local StudioTheme = require(Framework.Style.Themes.StudioTheme) - local theme = Theme.new(function(theme, getColor) - return { - Framework = StudioFrameworkStyles.new(theme, getColor) - } - end) + local theme + if FlagsList:get("FFlagRefactorDevFrameworkTheme") then + theme = StudioTheme.new() + else + theme = Theme.new(function(theme, getColor) + return { + Framework = StudioFrameworkStyles.new(theme, getColor) + } + end) + end local ExampleButton = Roact.PureComponent:extend("ExampleButton") @@ -84,7 +90,7 @@ return function(plugin) Disabled = true, Selected = false, LayoutOrder = 0, - OnClick = self.onToggle, + OnClick = self.onToggle1, Size = UDim2.fromOffset(40, 24), }), ToggleButton = Roact.createElement(ToggleButton, { diff --git a/BuiltInPlugins/PlayerEmulator/Packages/Framework/UI/ToggleButton/style.lua b/BuiltInPlugins/PlayerEmulator/Packages/Framework/UI/ToggleButton/style.lua index a85c8628a2..6b8d8f09bf 100644 --- a/BuiltInPlugins/PlayerEmulator/Packages/Framework/UI/ToggleButton/style.lua +++ b/BuiltInPlugins/PlayerEmulator/Packages/Framework/UI/ToggleButton/style.lua @@ -1,9 +1,14 @@ local Framework = script.Parent.Parent.Parent +local StyleKey = require(Framework.Style.StyleKey) + local Util = require(Framework.Util) local Style = Util.Style local StyleModifier = Util.StyleModifier local StyleValue = Util.StyleValue +local FlagsList = Util.Flags.new({ + FFlagRefactorDevFrameworkTheme = {"RefactorDevFrameworkTheme"}, +}) local UI = require(Framework.UI) local Decoration = UI.Decoration @@ -11,61 +16,97 @@ local Decoration = UI.Decoration local StudioFrameworkStyles = Framework.StudioUI.StudioFrameworkStyles local Common = require(StudioFrameworkStyles.Common) -return function(theme, getColor) - local common = Common(theme, getColor) - local themeName = theme.Name - - local onImage = StyleValue.new("onImage", { - Light = "rbxasset://textures/RoactStudioWidgets/toggle_on_light.png", - Dark = "rbxasset://textures/RoactStudioWidgets/toggle_on_dark.png", - }) - - local offImage = StyleValue.new("offImage", { - Light = "rbxasset://textures/RoactStudioWidgets/toggle_off_light.png", - Dark = "rbxasset://textures/RoactStudioWidgets/toggle_off_dark.png", - }) - - local disabledImage = StyleValue.new("disabledImage", { - Light = "rbxasset://textures/RoactStudioWidgets/toggle_disable_light.png", - Dark = "rbxasset://textures/RoactStudioWidgets/toggle_disable_dark.png", - }) - - local Default = Style.extend(common.MainText, { +if FlagsList:get("FFlagRefactorDevFrameworkTheme") then + return { Background = Decoration.Image, BackgroundStyle = { - Image = offImage:get(themeName), + Image = StyleKey.ToggleOffImage, }, [StyleModifier.Selected] = { BackgroundStyle = { - Image = onImage:get(themeName), + Image = StyleKey.ToggleOnImage, }, }, [StyleModifier.Disabled] = { BackgroundStyle = { - Image = disabledImage:get(themeName), + Image = StyleKey.ToggleDisabledImage, }, }, - }) - local Checkbox = Style.new({ - Background = Decoration.Image, - BackgroundStyle = { - Image = "rbxasset://textures/GameSettings/UncheckedBox.png", - }, - [StyleModifier.Selected] = { + ["&Checkbox"] = { + Background = Decoration.Image, BackgroundStyle = { - Image = "rbxasset://textures/GameSettings/CheckedBoxLight.png", + Image = "rbxasset://textures/GameSettings/UncheckedBox.png", + }, + [StyleModifier.Selected] = { + BackgroundStyle = { + Image = "rbxasset://textures/GameSettings/CheckedBoxLight.png", + }, + }, + [StyleModifier.Disabled] = { + BackgroundStyle = { + Image = "rbxasset://textures/GameSettings/UncheckedBox.png", + }, }, }, - [StyleModifier.Disabled] = { + } +else + return function(theme, getColor) + local common = Common(theme, getColor) + local themeName = theme.Name + + local onImage = StyleValue.new("onImage", { + Light = "rbxasset://textures/RoactStudioWidgets/toggle_on_light.png", + Dark = "rbxasset://textures/RoactStudioWidgets/toggle_on_dark.png", + }) + + local offImage = StyleValue.new("offImage", { + Light = "rbxasset://textures/RoactStudioWidgets/toggle_off_light.png", + Dark = "rbxasset://textures/RoactStudioWidgets/toggle_off_dark.png", + }) + + local disabledImage = StyleValue.new("disabledImage", { + Light = "rbxasset://textures/RoactStudioWidgets/toggle_disable_light.png", + Dark = "rbxasset://textures/RoactStudioWidgets/toggle_disable_dark.png", + }) + + local Default = Style.extend(common.MainText, { + Background = Decoration.Image, + BackgroundStyle = { + Image = offImage:get(themeName), + }, + [StyleModifier.Selected] = { + BackgroundStyle = { + Image = onImage:get(themeName), + }, + }, + [StyleModifier.Disabled] = { + BackgroundStyle = { + Image = disabledImage:get(themeName), + }, + }, + }) + + local Checkbox = Style.new({ + Background = Decoration.Image, BackgroundStyle = { Image = "rbxasset://textures/GameSettings/UncheckedBox.png", }, - }, - }) + [StyleModifier.Selected] = { + BackgroundStyle = { + Image = "rbxasset://textures/GameSettings/CheckedBoxLight.png", + }, + }, + [StyleModifier.Disabled] = { + BackgroundStyle = { + Image = "rbxasset://textures/GameSettings/UncheckedBox.png", + }, + }, + }) - return { - Default = Default, - Checkbox = Checkbox, - } -end + return { + Default = Default, + Checkbox = Checkbox, + } + end +end \ No newline at end of file diff --git a/BuiltInPlugins/PlayerEmulator/Packages/Framework/UI/ToggleButton/test.spec.lua b/BuiltInPlugins/PlayerEmulator/Packages/Framework/UI/ToggleButton/test.spec.lua index 32a617b695..c4db80d801 100644 --- a/BuiltInPlugins/PlayerEmulator/Packages/Framework/UI/ToggleButton/test.spec.lua +++ b/BuiltInPlugins/PlayerEmulator/Packages/Framework/UI/ToggleButton/test.spec.lua @@ -12,17 +12,29 @@ return function() local ToggleButton = require(script.Parent) local Box = require(Framework.UI.Box) + local StudioTheme = require(Framework.Style.Themes.StudioTheme) + + local Util = require(Framework.Util) + local FlagsList = Util.Flags.new({ + FFlagRefactorDevFrameworkTheme = {"RefactorDevFrameworkTheme"}, + }) + local DEFAULT_PROPS = { Selected = true, OnClick = function() end, } local function createTestToggle(props) local mouse = Mouse.new({}) - local theme = Theme.new(function() - return { - Framework = FrameworkStyles.new(), - } - end) + local theme + if FlagsList:get("FFlagRefactorDevFrameworkTheme") then + theme = StudioTheme.mock() + else + theme = Theme.new(function() + return { + Framework = FrameworkStyles.new(), + } + end) + end return provide({theme, mouse}, { ToggleButton = Roact.createElement(ToggleButton, props or DEFAULT_PROPS), }) diff --git a/BuiltInPlugins/PlayerEmulator/Packages/Framework/UI/Tooltip.lua b/BuiltInPlugins/PlayerEmulator/Packages/Framework/UI/Tooltip.lua index 36f7166df9..d25cfd053f 100644 --- a/BuiltInPlugins/PlayerEmulator/Packages/Framework/UI/Tooltip.lua +++ b/BuiltInPlugins/PlayerEmulator/Packages/Framework/UI/Tooltip.lua @@ -4,11 +4,12 @@ after a short delay. Required Props: - Theme Theme: A Theme ContextItem, which is provided via mapToProps. Focus Focus: A Focus ContextItem, which is provided via mapToProps. string Text: The text to display in the tooltip. Optional Props: + Theme Theme: A Theme ContextItem, which is provided via mapToProps. + Stylizer Stylizer: A Stylizer ContextItem, which is provided via mapToProps. boolean Enabled: Whether the tooltip will display on hover. integer Priority: The display order of this element, compared to other focused elements or elements that show on top. @@ -20,7 +21,6 @@ number ShowDelay: The time in seconds before the tooltip appears after the user stops moving the mouse over the element. ]] - local RunService = game:GetService("RunService") local TextService = game:GetService("TextService") @@ -32,7 +32,12 @@ local ShowOnTop = require(Framework.UI.ShowOnTop) local DropShadow = require(Framework.UI.DropShadow) local Box = require(Framework.UI.Box) local TextLabel = require(Framework.UI.TextLabel) -local Typecheck = require(Framework.Util).Typecheck + +local Util = require(Framework.Util) +local Typecheck = Util.Typecheck +local FlagsList = Util.Flags.new({ + FFlagRefactorDevFrameworkTheme = {"RefactorDevFrameworkTheme"}, +}) local Tooltip = Roact.PureComponent:extend("Tooltip") Typecheck.wrap(Tooltip, script) @@ -42,7 +47,7 @@ Tooltip.defaultProps = { Priority = 0 } -function Tooltip:init(props) +function Tooltip:init(props) self.state = { showTooltip = false, } @@ -51,9 +56,14 @@ function Tooltip:init(props) end function Tooltip:didMount() - local theme = self.props.Theme - local style = theme:getStyle("Framework", self) + + local style + if FlagsList:get("FFlagRefactorDevFrameworkTheme") then + style = self.props.Stylizer + else + style = theme:getStyle("Framework", self) + end local showDelay = style.ShowDelay self.connectHover = function() @@ -106,11 +116,17 @@ function Tooltip:render() local state = self.state local theme = props.Theme - local style = theme:getStyle("Framework", self) + + local style + if FlagsList:get("FFlagRefactorDevFrameworkTheme") then + style = props.Stylizer + else + style = theme:getStyle("Framework", self) + end + local padding = style.Padding local dropShadowPadding = style.DropShadow and style.DropShadow.Radius or 0 local offset = style.Offset - local showDelay = style.ShowDelay local maxWidth = style.MaxWidth local text = props.Text @@ -138,8 +154,6 @@ function Tooltip:render() local textBound = TextService:GetTextSize(text, style.TextSize, style.Font, Vector2.new(maxAvailableWidth, math.huge)) - local shadowPadding = style.DropShadow and style.DropShadow.Padding or 0 - -- GetTextSize calculates a float value and then rounds it down before returning local tooltipTargetWidth = textBound.X + paddingSize + 1 local tooltipTargetHeight = textBound.Y + paddingSize + 1 @@ -173,9 +187,9 @@ function Tooltip:render() Label = Roact.createElement(TextLabel, { Size = UDim2.new(1, 0, 1, 0), Text = text, - TextWrapped = true + TextWrapped = true, }) - }), + }), }) }) }) @@ -191,8 +205,9 @@ function Tooltip:render() end ContextServices.mapToProps(Tooltip, { - Theme = ContextServices.Theme, Focus = ContextServices.Focus, + Stylizer = FlagsList:get("FFlagRefactorDevFrameworkTheme") and ContextServices.Stylizer or nil, + Theme = (not FlagsList:get("FFlagRefactorDevFrameworkTheme")) and ContextServices.Theme or nil, }) return Tooltip \ No newline at end of file diff --git a/BuiltInPlugins/PlayerEmulator/Packages/Framework/UI/Tooltip/style.lua b/BuiltInPlugins/PlayerEmulator/Packages/Framework/UI/Tooltip/style.lua index ed04e66ad8..074930b4eb 100644 --- a/BuiltInPlugins/PlayerEmulator/Packages/Framework/UI/Tooltip/style.lua +++ b/BuiltInPlugins/PlayerEmulator/Packages/Framework/UI/Tooltip/style.lua @@ -1,25 +1,44 @@ local Framework = script.Parent.Parent.Parent local Util = require(Framework.Util) +local Cryo = Util.Cryo local Style = Util.Style +local FlagsList = Util.Flags.new({ + FFlagRefactorDevFrameworkTheme = {"RefactorDevFrameworkTheme"}, +}) local StudioFrameworkStyles = Framework.StudioUI.StudioFrameworkStyles local Common = require(StudioFrameworkStyles.Common) local UIFolderData = require(Framework.UI.UIFolderData) local DropShadow = require(UIFolderData.DropShadow.style) -return function(theme, getColor) - local common = Common(theme, getColor) - local dropShadow = DropShadow(theme, getColor) - - local Default = Style.extend(common.MainText, { +if FlagsList:get("FFlagRefactorDevFrameworkTheme") then + local dropShadow = DropShadow + return { Padding = 5, MaxWidth = 200, ShowDelay = 0.3, Offset = Vector2.new(10, 5), - DropShadow = dropShadow.Default - }) - - return { - Default = Default, + DropShadow = Cryo.Dictionary.join(dropShadow, { + Radius = 3, + }), } -end +else + return function(theme, getColor) + local common = Common(theme, getColor) + local dropShadow = DropShadow(theme, getColor) + + local Default = Style.extend(common.MainText, { + Padding = 5, + MaxWidth = 200, + ShowDelay = 0.3, + Offset = Vector2.new(10, 5), + DropShadow = Style.extend(dropShadow.Default, { + Radius = 3, + }), + }) + + return { + Default = Default, + } + end +end \ No newline at end of file diff --git a/BuiltInPlugins/PlayerEmulator/Packages/Framework/UI/Tooltip/test.spec.lua b/BuiltInPlugins/PlayerEmulator/Packages/Framework/UI/Tooltip/test.spec.lua index 965a4ac846..8f7a0a6fc9 100644 --- a/BuiltInPlugins/PlayerEmulator/Packages/Framework/UI/Tooltip/test.spec.lua +++ b/BuiltInPlugins/PlayerEmulator/Packages/Framework/UI/Tooltip/test.spec.lua @@ -11,18 +11,28 @@ return function() local provide = ContextServices.provide local Tooltip = require(script.Parent) - local Box = require(Framework.UI.Box) + local StudioTheme = require(Framework.Style.Themes.StudioTheme) + + local Util = require(Framework.Util) + local FlagsList = Util.Flags.new({ + FFlagRefactorDevFrameworkTheme = {"RefactorDevFrameworkTheme"}, + }) local DEFAULT_PROPS = {} local function createTooltip(props) local mouse = Mouse.new({}) - local target = container or Instance.new("ScreenGui") + local target = Instance.new("ScreenGui") local focus = Focus.new(target) - local theme = Theme.new(function() - return { - Framework = FrameworkStyles.new(), - } - end) + local theme + if FlagsList:get("FFlagRefactorDevFrameworkTheme") then + theme = StudioTheme.mock() + else + theme = Theme.new(function() + return { + Framework = FrameworkStyles.new(), + } + end) + end return provide({theme, mouse, focus}, { Tooltip = Roact.createElement(Tooltip, props or DEFAULT_PROPS), }) @@ -48,4 +58,4 @@ return function() Roact.unmount(instance) end) -end \ No newline at end of file +end diff --git a/BuiltInPlugins/PlayerEmulator/Packages/Framework/UI/TreeView.lua b/BuiltInPlugins/PlayerEmulator/Packages/Framework/UI/TreeView.lua index af58fcf1af..a0f53a305c 100644 --- a/BuiltInPlugins/PlayerEmulator/Packages/Framework/UI/TreeView.lua +++ b/BuiltInPlugins/PlayerEmulator/Packages/Framework/UI/TreeView.lua @@ -2,7 +2,6 @@ TreeView - Displays a hierarchical data set. Required Props: - Theme Theme: The theme supplied from mapToProps() UDim2 Size: The size of the component table RootItems: The root items displayed in the tree view. callback GetChildren: This should return a list of children for a given item - GetChildren(item: Item) => Item[] @@ -10,7 +9,9 @@ table Expansion: Which items should be expanded - Set Optional Props: + Theme Theme: The theme supplied from mapToProps() Style Style: a style table supplied from props and theme:getStyle() + Stylizer Stylizer: A Stylizer ContextItem, which is provided via mapToProps. callback SortChildren: A comparator function to sort two items in the tree - SortChildren(left: Item, right: Item) => boolean callback GetItemKey: Return a unique key for an item - GetItemKey(item: Item) => string @@ -36,6 +37,11 @@ local Typecheck = require(Framework.Util).Typecheck local UI = Framework.UI local Container = require(UI.Container) +local Util = require(Framework.Util) + +local FlagsList = Util.Flags.new({ + FFlagRefactorDevFrameworkTheme = {"RefactorDevFrameworkTheme"}, +}) local TreeView = Roact.PureComponent:extend("TreeView") local ScrollingFrame = require(Framework.UI.ScrollingFrame) @@ -100,7 +106,12 @@ function TreeView:render() local state = self.state local rows = state.rows local theme = props.Theme - local style = theme:getStyle("Framework", self) + local style + if FlagsList:get("FFlagRefactorDevFrameworkTheme") then + style = props.Stylizer + else + style = theme:getStyle("Framework", self) + end local children = {} for index, row in ipairs(rows) do @@ -126,7 +137,8 @@ function TreeView:render() end ContextServices.mapToProps(TreeView, { - Theme = ContextServices.Theme, + Stylizer = FlagsList:get("FFlagRefactorDevFrameworkTheme") and ContextServices.Stylizer or nil, + Theme = (not FlagsList:get("FFlagRefactorDevFrameworkTheme")) and ContextServices.Theme or nil, }) return TreeView \ No newline at end of file diff --git a/BuiltInPlugins/PlayerEmulator/Packages/Framework/UI/TreeView/style.lua b/BuiltInPlugins/PlayerEmulator/Packages/Framework/UI/TreeView/style.lua index 5bdcc55094..0b00cf1a29 100644 --- a/BuiltInPlugins/PlayerEmulator/Packages/Framework/UI/TreeView/style.lua +++ b/BuiltInPlugins/PlayerEmulator/Packages/Framework/UI/TreeView/style.lua @@ -8,18 +8,31 @@ local UIFolderData = require(Framework.UI.UIFolderData) local ScrollingFrame = require(UIFolderData.ScrollingFrame.style) local RoundBox = require(UIFolderData.RoundBox.style) -return function(theme, getColor) - local roundBox = RoundBox(theme, getColor) - local scrollingFrame = ScrollingFrame(theme, getColor) +local FlagsList = Util.Flags.new({ + FFlagRefactorDevFrameworkTheme = {"RefactorDevFrameworkTheme"}, +}) - local Default = Style.new({ +if FlagsList:get("FFlagRefactorDevFrameworkTheme") then + return { Background = Decoration.RoundBox, - BackgroundStyle = roundBox.Default, - ScrollingFrame = Style.extend(scrollingFrame.Default, {}), + BackgroundStyle = RoundBox, + ScrollingFrame = ScrollingFrame, Padding = 1 - }) - - return { - Default = Default, } -end +else + return function(theme, getColor) + local roundBox = RoundBox(theme, getColor) + local scrollingFrame = ScrollingFrame(theme, getColor) + + local Default = Style.new({ + Background = Decoration.RoundBox, + BackgroundStyle = roundBox.Default, + ScrollingFrame = Style.extend(scrollingFrame.Default, {}), + Padding = 1 + }) + + return { + Default = Default, + } + end +end \ No newline at end of file diff --git a/BuiltInPlugins/PlayerEmulator/Packages/Framework/UI/TreeView/test.spec.lua b/BuiltInPlugins/PlayerEmulator/Packages/Framework/UI/TreeView/test.spec.lua index 46bd84fa70..8d06ee8a86 100644 --- a/BuiltInPlugins/PlayerEmulator/Packages/Framework/UI/TreeView/test.spec.lua +++ b/BuiltInPlugins/PlayerEmulator/Packages/Framework/UI/TreeView/test.spec.lua @@ -8,6 +8,13 @@ return function() local TreeView = require(script.Parent) local TextLabel = require(Framework.UI.TextLabel) + local StudioTheme = require(Framework.Style.Themes.StudioTheme) + + local Util = require(Framework.Util) + local FlagsList = Util.Flags.new({ + FFlagRefactorDevFrameworkTheme = {"RefactorDevFrameworkTheme"}, + }) + local items = { { name = "Workspace", @@ -54,11 +61,16 @@ return function() } local function createTreeView() - local theme = Theme.new(function() - return { - Framework = FrameworkStyles.new(), - } - end) + local theme + if FlagsList:get("FFlagRefactorDevFrameworkTheme") then + theme = StudioTheme.mock() + else + theme = Theme.new(function() + return { + Framework = FrameworkStyles.new(), + } + end) + end return provide({theme}, { TreeView = Roact.createElement(TreeView, { RootItems = items, diff --git a/BuiltInPlugins/PlayerEmulator/Packages/Framework/Util.lua b/BuiltInPlugins/PlayerEmulator/Packages/Framework/Util.lua index 661416fbb9..6a3e12c089 100644 --- a/BuiltInPlugins/PlayerEmulator/Packages/Framework/Util.lua +++ b/BuiltInPlugins/PlayerEmulator/Packages/Framework/Util.lua @@ -12,11 +12,14 @@ return strict({ Cryo = require(script.Cryo), CrossPluginCommunication = require(script.CrossPluginCommunication), deepEqual = require(script.deepEqual), + deepJoin = require(script.deepJoin), + deepCopy = require(script.deepCopy), -- TODO DEVTOOLS-4459: Remove this export FitFrame = require(script.FitFrame), Flags = require(script.Flags), + getTestVariation = require(script.getTestVariation), Immutable = require(script.Immutable), LayoutOrderIterator = require(script.LayoutOrderIterator), Math = require(script.Math), @@ -25,6 +28,7 @@ return strict({ Symbol = require(script.Symbol), ThunkWithArgsMiddleware = require(script.ThunkWithArgsMiddleware), strict = strict, + tableCache = require(script.tableCache), -- Style and Theming Utilities Palette = require(script.Palette), @@ -36,4 +40,4 @@ return strict({ -- Document Generation and Type Enforcement Utilities Typecheck = require(script.Typecheck), -}) \ No newline at end of file +}) diff --git a/BuiltInPlugins/PlayerEmulator/Packages/Framework/Util/CrossPluginCommunication.lua b/BuiltInPlugins/PlayerEmulator/Packages/Framework/Util/CrossPluginCommunication.lua index 37caa1bece..d3b82f5783 100644 --- a/BuiltInPlugins/PlayerEmulator/Packages/Framework/Util/CrossPluginCommunication.lua +++ b/BuiltInPlugins/PlayerEmulator/Packages/Framework/Util/CrossPluginCommunication.lua @@ -22,10 +22,12 @@ CrossPluginCommunication.__index = CrossPluginCommunication CrossPluginCommunication.BASE_FOLDER_NAME = "CrossPluginCommunication" -function CrossPluginCommunication.new(pluginNamespace) +function CrossPluginCommunication.new(pluginNamespace, hostService) assert(t.string(pluginNamespace), "pluginNamespace must be a string") + assert(t.optional(t.instance(pluginNamespace)), "hostService must be an instance if defined") local self = { + hostService = hostService or RobloxPluginGuiService, pluginNamespace = pluginNamespace } @@ -49,7 +51,7 @@ function CrossPluginCommunication:ensureFolderExists(parent, name) end function CrossPluginCommunication:getNamespaceFolder() - local base = self:ensureFolderExists(RobloxPluginGuiService, self.BASE_FOLDER_NAME) + local base = self:ensureFolderExists(self.hostService, self.BASE_FOLDER_NAME) return self:ensureFolderExists(base, self.pluginNamespace) end diff --git a/BuiltInPlugins/PlayerEmulator/Packages/Framework/Util/CrossPluginCommunication.spec.lua b/BuiltInPlugins/PlayerEmulator/Packages/Framework/Util/CrossPluginCommunication.spec.lua index 60338f2b0c..f3c1035f50 100644 --- a/BuiltInPlugins/PlayerEmulator/Packages/Framework/Util/CrossPluginCommunication.spec.lua +++ b/BuiltInPlugins/PlayerEmulator/Packages/Framework/Util/CrossPluginCommunication.spec.lua @@ -1,5 +1,3 @@ -local RobloxPluginGuiService = game:GetService("RobloxPluginGuiService") - local Framework = script.Parent.Parent local CrossPluginCommunication = require(Framework.Util.CrossPluginCommunication) @@ -8,8 +6,10 @@ return function() -- TODO DEVTOOLS-4397: Move cleanup to afterEach hook after the dependency structure is fixed + local hostService = workspace + local function createTestComms() - return CrossPluginCommunication.new(TEST_NAMESPACE) + return CrossPluginCommunication.new(TEST_NAMESPACE, hostService) end describe("Invoke", function() @@ -32,7 +32,7 @@ return function() comms:OnInvoke("blah", function() end) - local namespace = RobloxPluginGuiService:FindFirstChild(CrossPluginCommunication.BASE_FOLDER_NAME):FindFirstChild(TEST_NAMESPACE) + local namespace = hostService:FindFirstChild(CrossPluginCommunication.BASE_FOLDER_NAME):FindFirstChild(TEST_NAMESPACE) assert(namespace) diff --git a/BuiltInPlugins/PlayerEmulator/Packages/Framework/Util/Flags.lua b/BuiltInPlugins/PlayerEmulator/Packages/Framework/Util/Flags.lua index beb3097029..508d25912e 100644 --- a/BuiltInPlugins/PlayerEmulator/Packages/Framework/Util/Flags.lua +++ b/BuiltInPlugins/PlayerEmulator/Packages/Framework/Util/Flags.lua @@ -94,7 +94,7 @@ function Flags.new(featuresMap, config) assert(type(config.shouldFetchLiveValues) == "boolean", "shouldFetchLiveValues expected to be a boolean") assert(type(config.defaultValueIfMissing) == "boolean", "Default values for flags must be a boolean") assert(type(featuresMap) == "table", "Flags.new expects a table mapping keys to flag names.") - local isMap = type(next(featuresMap)) == "nil" or type(next(featuresMap)) == "string" + local isMap = type((next(featuresMap))) == "nil" or type((next(featuresMap))) == "string" assert(isMap, "Flags.new expects a map of string keys.") local self = { @@ -186,4 +186,4 @@ function Flags:clearAllLocalOverrides() self.localOverrides = {} end -return Flags \ No newline at end of file +return Flags diff --git a/BuiltInPlugins/PlayerEmulator/Packages/Framework/Util/Palette.spec.lua b/BuiltInPlugins/PlayerEmulator/Packages/Framework/Util/Palette.spec.lua index 8794c80717..02911b17c7 100644 --- a/BuiltInPlugins/PlayerEmulator/Packages/Framework/Util/Palette.spec.lua +++ b/BuiltInPlugins/PlayerEmulator/Packages/Framework/Util/Palette.spec.lua @@ -1,5 +1,16 @@ return function() - local Palette = require(script.Parent.Palette) + + local Util = script.Parent + local Flags = require(Util.Flags) + local FlagsList = Flags.new({ + FFlagRefactorDevFrameworkTheme = {"RefactorDevFrameworkTheme"}, + }) + + if FlagsList:get("FFlagRefactorDevFrameworkTheme") then + return + end + + local Palette = require(Util.Palette) it("should be a table of color StyleValues", function() expect(Palette).to.be.ok() diff --git a/BuiltInPlugins/PlayerEmulator/Packages/Framework/Util/Promise.lua b/BuiltInPlugins/PlayerEmulator/Packages/Framework/Util/Promise.lua index ea1b998dc7..77dfacfe53 100644 --- a/BuiltInPlugins/PlayerEmulator/Packages/Framework/Util/Promise.lua +++ b/BuiltInPlugins/PlayerEmulator/Packages/Framework/Util/Promise.lua @@ -41,8 +41,6 @@ ]] -local FFlagDevFrameworkUnhandledPromiseRejections = game:DefineFastFlag("DevFrameworkUnhandledPromiseRejections", false) - local PROMISE_DEBUG = false -- If promise debugging is on, use a version of pcall that warns on failure. @@ -92,15 +90,13 @@ Promise.Status = { Rejected = "Rejected", } -if FFlagDevFrameworkUnhandledPromiseRejections then - --[[ - This can be overridden to change the global callback for unhandled rejections. +--[[ + This can be overridden to change the global callback for unhandled rejections. - By default it is disabled (set to nil) so that consumers can choose how to log - unhandled rejections, and not pollute the output (e.g. with "warn"). - ]] - Promise.onUnhandledRejection = nil -end + By default it is disabled (set to nil) so that consumers can choose how to log + unhandled rejections, and not pollute the output (e.g. with "warn"). +]] +Promise.onUnhandledRejection = nil --[[ Constructs a new Promise with the given initializing callback. @@ -143,12 +139,10 @@ function Promise.new(callback) -- Queues representing functions we should invoke when we update! _queuedResolve = {}, _queuedReject = {}, - } - if FFlagDevFrameworkUnhandledPromiseRejections then -- If an error occurs with no handlers, this will be set to true. - promise._unhandledRejection = false - end + _unhandledRejection = false, + } setmetatable(promise, Promise) @@ -259,9 +253,7 @@ end The given callbacks are invoked depending on that result. ]] function Promise:andThen(successHandler, failureHandler) - if FFlagDevFrameworkUnhandledPromiseRejections then - self._unhandledRejection = false - end + self._unhandledRejection = false -- Create a new promise to follow this part of the chain return Promise.new(function(resolve, reject) @@ -305,9 +297,7 @@ end This matches the execution model of normal Roblox functions. ]] function Promise:await() - if FFlagDevFrameworkUnhandledPromiseRejections then - self._unhandledRejection = false - end + self._unhandledRejection = false if self._status == Promise.Status.Started then local result @@ -334,6 +324,8 @@ function Promise:await() elseif self._status == Promise.Status.Rejected then error(tostring(self._value[1]), 2) end + + return end function Promise:_resolve(...) @@ -386,33 +378,31 @@ function Promise:_reject(...) callback(...) end else - if FFlagDevFrameworkUnhandledPromiseRejections then - self._unhandledRejection = true - local err = tostring((...)) - - -- At this point, no error handler is available. - -- An error handler might still be attached if the error occurred - -- synchronously. We'll wait one tick, and if there are still no - -- handlers, call the global onUnhandledRejection handler. - spawn(function() - -- The error was handled while we were waiting - if not self._unhandledRejection then - return - end + self._unhandledRejection = true + local err = tostring((...)) + + -- At this point, no error handler is available. + -- An error handler might still be attached if the error occurred + -- synchronously. We'll wait one tick, and if there are still no + -- handlers, call the global onUnhandledRejection handler. + spawn(function() + -- The error was handled while we were waiting + if not self._unhandledRejection then + return + end - local message = ("Unhandled promise rejection:\n\n%s\n\n%s"):format( - err, - self._source - ) + local message = ("Unhandled promise rejection:\n\n%s\n\n%s"):format( + err, + self._source + ) - -- Ignore failures in logging the rejection - pcall(function() - if Promise.onUnhandledRejection then - Promise.onUnhandledRejection(message) - end - end) + -- Ignore failures in logging the rejection + pcall(function() + if Promise.onUnhandledRejection then + Promise.onUnhandledRejection(message) + end end) - end + end) end end diff --git a/BuiltInPlugins/PlayerEmulator/Packages/Framework/Util/Promise.spec.lua b/BuiltInPlugins/PlayerEmulator/Packages/Framework/Util/Promise.spec.lua index 68497501d2..3c29f9c9a9 100644 --- a/BuiltInPlugins/PlayerEmulator/Packages/Framework/Util/Promise.spec.lua +++ b/BuiltInPlugins/PlayerEmulator/Packages/Framework/Util/Promise.spec.lua @@ -1,10 +1,6 @@ return function() - local Framework = script.Parent.Parent - local Promise = require(script.Parent.Promise) - local FFlagDevFrameworkUnhandledPromiseRejections = game:GetFastFlag("DevFrameworkUnhandledPromiseRejections") - describe("Promise.new", function() it("should instantiate with a callback", function() local promise = Promise.new(function() end) @@ -285,10 +281,6 @@ return function() -- onUnhandledRejection callback. describe("unhandled rejections", function() - if not FFlagDevFrameworkUnhandledPromiseRejections then - return - end - local calls local originalOnUnhandledRejection @@ -359,10 +351,8 @@ return function() expect(promise._unhandledRejection).to.equal(true) - local caught = false promise:catch(function(err) expect(err:find("it did not work")).to.be.ok() - caught = true end) waitUntilNextTick() diff --git a/BuiltInPlugins/PlayerEmulator/Packages/Framework/Util/StyleModifier.lua b/BuiltInPlugins/PlayerEmulator/Packages/Framework/Util/StyleModifier.lua index 2076dbf5ea..01ee072ce7 100644 --- a/BuiltInPlugins/PlayerEmulator/Packages/Framework/Util/StyleModifier.lua +++ b/BuiltInPlugins/PlayerEmulator/Packages/Framework/Util/StyleModifier.lua @@ -14,45 +14,12 @@ end) ]] -local doesNotExistError = [[ -The value '%s' does not exist in StyleModifier.]] - local Framework = script.Parent.Parent -local Symbol = require(Framework.Util.Symbol) -local Flags = require(Framework.Util.Flags) -local FlagsList = Flags.new({ - FFlagDevFrameworkEnumUtility = "DevFrameworkEnumUtility", +local enumerate = require(Framework.Util.enumerate) +return enumerate("StyleModifier", { + "Hover", + "Pressed", + "Selected", + "Disabled" }) - -if FlagsList:get("FFlagDevFrameworkEnumUtility") then - local enumerate = require(Framework.Util.enumerate) - return enumerate("StyleModifier", { - "Hover", - "Pressed", - "Selected", - "Disabled" - }) -else - local StyleModifier = { - Hover = Symbol.named("Hover"), - Pressed = Symbol.named("Pressed"), - Selected = Symbol.named("Selected"), - Disabled = Symbol.named("Disabled"), - } - - setmetatable(StyleModifier, { - __index = function(key) - if StyleModifier[key] then - return StyleModifier[key] - else - error(string.format(doesNotExistError, key)) - end - end, - __newindex = function(key) - error(string.format(doesNotExistError, key)) - end, - }) - - return StyleModifier -end diff --git a/BuiltInPlugins/PlayerEmulator/Packages/Framework/Util/Symbol.lua b/BuiltInPlugins/PlayerEmulator/Packages/Framework/Util/Symbol.lua index d9e26d9c65..2363bf1454 100644 --- a/BuiltInPlugins/PlayerEmulator/Packages/Framework/Util/Symbol.lua +++ b/BuiltInPlugins/PlayerEmulator/Packages/Framework/Util/Symbol.lua @@ -1,6 +1,5 @@ --[[ A 'Symbol' is an opaque marker type. - Symbols have the type 'userdata', but when printed to the console, the name of the symbol is shown. ]] @@ -9,7 +8,6 @@ local Symbol = {} --[[ Creates a Symbol with the given name. - When printed or coerced to a string, the symbol will turn into the string given as its name. ]] diff --git a/BuiltInPlugins/PlayerEmulator/Packages/Framework/Util/Symbol.spec.lua b/BuiltInPlugins/PlayerEmulator/Packages/Framework/Util/Symbol.spec.lua index cde9be065b..aa5ad93ef1 100644 --- a/BuiltInPlugins/PlayerEmulator/Packages/Framework/Util/Symbol.spec.lua +++ b/BuiltInPlugins/PlayerEmulator/Packages/Framework/Util/Symbol.spec.lua @@ -11,7 +11,7 @@ return function() it("should coerce to the given name", function() local symbol = Symbol.named("foo") - expect(tostring(symbol):find("foo")).to.be.ok() + expect((tostring(symbol):find("foo"))).to.be.ok() end) it("should be unique when constructed", function() @@ -42,4 +42,4 @@ return function() expect(symbolA).never.to.equal(symbolB) end) end) -end \ No newline at end of file +end diff --git a/BuiltInPlugins/PlayerEmulator/Packages/Framework/Util/Typecheck/DocParser.lua b/BuiltInPlugins/PlayerEmulator/Packages/Framework/Util/Typecheck/DocParser.lua index a7654d586d..c730d15d52 100644 --- a/BuiltInPlugins/PlayerEmulator/Packages/Framework/Util/Typecheck/DocParser.lua +++ b/BuiltInPlugins/PlayerEmulator/Packages/Framework/Util/Typecheck/DocParser.lua @@ -106,6 +106,7 @@ function DocParser:parseComments(comments) Required = {}, Optional = {}, Style = {}, + Summary = nil, } local parseMode = ParseMode.Summary diff --git a/BuiltInPlugins/PlayerEmulator/Packages/Framework/Util/Typecheck/FrameworkTypes.lua b/BuiltInPlugins/PlayerEmulator/Packages/Framework/Util/Typecheck/FrameworkTypes.lua index f0a8c83f91..3452568da5 100644 --- a/BuiltInPlugins/PlayerEmulator/Packages/Framework/Util/Typecheck/FrameworkTypes.lua +++ b/BuiltInPlugins/PlayerEmulator/Packages/Framework/Util/Typecheck/FrameworkTypes.lua @@ -9,7 +9,6 @@ local t = require(script.Parent.t) local FrameworkTypes = {} local Flags = require(Framework.Util.Flags) local FlagsList = Flags.new({ - FFlagDevFrameworkEnumUtility = "DevFrameworkEnumUtility", FFlagRefactorDevFrameworkContextItems = {"RefactorDevFrameworkContextItems"}, }) @@ -37,6 +36,14 @@ function FrameworkTypes.Theme(value) return true end +function FrameworkTypes.Stylizer(value) + local errMsg = "Stylizer expected, got %s." + if not t.table(value) or not t.callback(value.getConsumerItem) then + return false, errMsg:format(type(value)) + end + return true +end + function FrameworkTypes.Plugin(value) local errMsg = "Plugin expected, got %s." if not t.table(value) or not t.callback(value.get) then @@ -81,16 +88,8 @@ end function FrameworkTypes.StyleModifier(value) local errMsg = "StyleModifier expected, got %s." - if FlagsList:get("FFlagDevFrameworkEnumUtility") then - if StyleModifier.isEnumValue(value) then - return true - end - else - for _, v in pairs(StyleModifier) do - if value == v then - return true - end - end + if StyleModifier.isEnumValue(value) then + return true end return false, errMsg:format(type(value)) diff --git a/BuiltInPlugins/PlayerEmulator/Packages/Framework/Util/Typecheck/t.lua b/BuiltInPlugins/PlayerEmulator/Packages/Framework/Util/Typecheck/t.lua index 44c8d50af8..045d7ac0dd 100644 --- a/BuiltInPlugins/PlayerEmulator/Packages/Framework/Util/Typecheck/t.lua +++ b/BuiltInPlugins/PlayerEmulator/Packages/Framework/Util/Typecheck/t.lua @@ -1,3 +1,5 @@ +--!nocheck + --[[ DeveloperFramework uses t from Osyris https://github.com/osyrisrblx/t diff --git a/BuiltInPlugins/PlayerEmulator/Packages/Framework/Util/Typecheck/wrap.lua b/BuiltInPlugins/PlayerEmulator/Packages/Framework/Util/Typecheck/wrap.lua index 69b0b3a844..9384db05dd 100644 --- a/BuiltInPlugins/PlayerEmulator/Packages/Framework/Util/Typecheck/wrap.lua +++ b/BuiltInPlugins/PlayerEmulator/Packages/Framework/Util/Typecheck/wrap.lua @@ -21,7 +21,10 @@ Typecheck uses interfaces from Osyris's "t" library. For more info, see: https://github.com/osyrisrblx/t ]] - +local Util = script.Parent.Parent +local FlagsList = require(Util.Flags).new({ + FFlagRefactorDevFrameworkTheme = {"RefactorDevFrameworkTheme"}, +}) local DocParser = require(script.Parent.DocParser) local propsTraceback = [[%s @@ -52,7 +55,12 @@ local function validate(component, propsInterface, styleInterface) local success, errorMessage = propsInterface(self.props) if success then if styleInterface then - local style = self.props.Theme:getStyle("Framework", self) + local style + if FlagsList:get("FFlagRefactorDevFrameworkTheme") then + style = self.props.Stylizer + else + style = self.props.Theme:getStyle("Framework", self) + end success, errorMessage = styleInterface(style) if not success then errorMessage = styleTraceback:format(errorMessage, tostring(component)) diff --git a/BuiltInPlugins/PlayerEmulator/Packages/Framework/Util/Typecheck/wrap.spec.lua b/BuiltInPlugins/PlayerEmulator/Packages/Framework/Util/Typecheck/wrap.spec.lua index a9275f1b92..e9854468f0 100644 --- a/BuiltInPlugins/PlayerEmulator/Packages/Framework/Util/Typecheck/wrap.spec.lua +++ b/BuiltInPlugins/PlayerEmulator/Packages/Framework/Util/Typecheck/wrap.spec.lua @@ -3,7 +3,10 @@ Required Props: UDim2 Size: The size of the component. + + Optional Props: Theme Theme: The Theme ContextItem from mapToProps. + Stylizer Stylizer: A Stylizer ContextItem, which is provided via mapToProps. Style Values: Color3 Color: The color of the component. @@ -14,6 +17,11 @@ return function() local Roact = require(Framework.Parent.Roact) local ContextServices = require(Framework.ContextServices) local wrap = require(Framework.Util.Typecheck.wrap) + local StudioTheme = require(Framework.Style.Themes.StudioTheme) + local ui = require(Framework.Style.ComponentSymbols) + local FlagsList = require(Framework.Util.Flags).new({ + FFlagRefactorDevFrameworkTheme = {"RefactorDevFrameworkTheme"}, + }) local WrapTestComponent = Roact.PureComponent:extend("WrapTestComponent") wrap(WrapTestComponent, script) @@ -21,7 +29,12 @@ return function() function WrapTestComponent:render() local props = self.props local size = props.Size - local style = props.Theme:getStyle("Framework", self) + local style + if FlagsList:get("FFlagRefactorDevFrameworkTheme") then + style = props.Stylizer + else + style = props.Theme:getStyle("Framework", self) + end local color = style.Color return Roact.createElement("Frame", { @@ -31,19 +44,29 @@ return function() end ContextServices.mapToProps(WrapTestComponent, { - Theme = ContextServices.Theme, + Stylizer = FlagsList:get("FFlagRefactorDevFrameworkTheme") and ContextServices.Stylizer or nil, + Theme = (not FlagsList:get("FFlagRefactorDevFrameworkTheme")) and ContextServices.Theme or nil, }) local function createWrapTestComponent(props, styleTable) - local theme = ContextServices.Theme.new(function() - return { - Framework = { - WrapTestComponent = { - Default = styleTable, + local theme + if FlagsList:get("FFlagRefactorDevFrameworkTheme") then + theme = StudioTheme.mock() + ui:add("WrapTestComponent") + theme:extend({ + [ui.WrapTestComponent] = styleTable, + }) + else + theme = ContextServices.Theme.new(function() + return { + Framework = { + WrapTestComponent = { + Default = styleTable, + }, }, - }, - } - end) + } + end) + end return ContextServices.provide({theme}, { Test = Roact.createElement(WrapTestComponent, props), diff --git a/BuiltInPlugins/PlayerEmulator/Packages/Framework/Util/deepCopy.lua b/BuiltInPlugins/PlayerEmulator/Packages/Framework/Util/deepCopy.lua new file mode 100644 index 0000000000..9a1e54beea --- /dev/null +++ b/BuiltInPlugins/PlayerEmulator/Packages/Framework/Util/deepCopy.lua @@ -0,0 +1,22 @@ +--[[ + Copies a table and it's metatables. + Used in Stylizer to prevent mutating tables that convert Symbols into color values. +--]] +local function deepCopy(t) + if type(t) ~= "table" or type(t.render) == "function" then + return t + end + local meta = getmetatable(t) + local target = {} + for k, v in pairs(t) do + if type(v) == "table" then + target[k] = deepCopy(v) + else + target[k] = v + end + end + setmetatable(target, meta) + return target +end + +return deepCopy \ No newline at end of file diff --git a/BuiltInPlugins/PlayerEmulator/Packages/Framework/Util/deepCopy.spec.lua b/BuiltInPlugins/PlayerEmulator/Packages/Framework/Util/deepCopy.spec.lua new file mode 100644 index 0000000000..97052d6ebe --- /dev/null +++ b/BuiltInPlugins/PlayerEmulator/Packages/Framework/Util/deepCopy.spec.lua @@ -0,0 +1,37 @@ +return function() + local deepEqual = require(script.Parent.deepEqual) + local deepCopy = require(script.Parent.deepCopy) + + it("should fail when copied table and result are equal", function() + local originalTable = { + test = "hello" + } + + local copy = deepCopy(originalTable) + expect(copy).to.never.equal(originalTable) + end) + + it("should fail when copied inner tables are equal to the original", function() + local originalTable = { + string = "hello", + table = { + inner = "test", + } + } + + local copy = deepCopy(originalTable) + expect(copy.table).to.never.equal(originalTable.table) + end) + + it("should have all values in the table and its copy be equal", function() + local originalTable = { + string = "hello", + table = { + inner = "test", + } + } + local copy = deepCopy(originalTable) + expect(deepEqual(originalTable, copy)).to.equal(true) + end) + +end \ No newline at end of file diff --git a/BuiltInPlugins/PlayerEmulator/Packages/Framework/Util/deepJoin.lua b/BuiltInPlugins/PlayerEmulator/Packages/Framework/Util/deepJoin.lua new file mode 100644 index 0000000000..2151aa74b9 --- /dev/null +++ b/BuiltInPlugins/PlayerEmulator/Packages/Framework/Util/deepJoin.lua @@ -0,0 +1,31 @@ +local function deepJoin(t1, t2) + local new = {} + + for key, value in pairs(t1) do + if typeof(value) == "table" then + if t2[key] and typeof(t2[key]) == "table" then + new[key] = deepJoin(value, t2[key]) + else + -- this essentially acts like a deepcopy to prevent + -- references getting all tangled up + new[key] = deepJoin(value, {}) + end + else + new[key] = value + end + end + + for key, value in pairs(t2) do + if typeof(value) == "table" then + if not t1[key] then + new[key] = deepJoin(value, {}) + end + else + new[key] = value + end + end + + return new +end + +return deepJoin diff --git a/BuiltInPlugins/PlayerEmulator/Packages/Framework/Util/deepJoin.spec.lua b/BuiltInPlugins/PlayerEmulator/Packages/Framework/Util/deepJoin.spec.lua new file mode 100644 index 0000000000..05d07cc90b --- /dev/null +++ b/BuiltInPlugins/PlayerEmulator/Packages/Framework/Util/deepJoin.spec.lua @@ -0,0 +1,76 @@ +return function() + local Library = script.Parent + local deepJoin = require(Library.deepJoin) + + it("should join two tables together", function() + local tableA = {key1 = "Value1"} + local tableB = {key2 = "Value2"} + + local result = deepJoin(tableA, tableB) + + expect(result.key1).to.equal("Value1") + expect(result.key2).to.equal("Value2") + end) + + it("should add all the entries from the first table", function() + local tableA = {key1 = "Value1", key2 = "Value2"} + local tableB = {} + + local result = deepJoin(tableA, tableB) + + expect(result.key1).to.equal("Value1") + expect(result.key2).to.equal("Value2") + end) + + it("should add all the entries from the second table", function() + local tableA = {} + local tableB = {key1 = "Value1", key2 = "Value2"} + + local result = deepJoin(tableA, tableB) + + expect(result.key1).to.equal("Value1") + expect(result.key2).to.equal("Value2") + end) + + it("should join values in nested tables", function() + local tableA = { + set = { + key1 = "Value1", + }, + } + + local tableB = { + set = { + key2 = "Value2", + }, + } + + local result = deepJoin(tableA, tableB) + + expect(result.set).to.be.ok() + expect(result.set.key1).to.equal("Value1") + expect(result.set.key2).to.equal("Value2") + end) + + it("should prioritize the second table if values overlap", function() + local tableA = { + outsideKey = "Old", + set = { + insideKey = "Old", + }, + } + + local tableB = { + outsideKey = "New", + set = { + insideKey = "New", + }, + } + + local result = deepJoin(tableA, tableB) + + expect(result.outsideKey).to.equal("New") + expect(result.set).to.be.ok() + expect(result.set.insideKey).to.equal("New") + end) +end diff --git a/BuiltInPlugins/PlayerEmulator/Packages/Framework/Util/getTestVariation.lua b/BuiltInPlugins/PlayerEmulator/Packages/Framework/Util/getTestVariation.lua new file mode 100644 index 0000000000..d09873db1d --- /dev/null +++ b/BuiltInPlugins/PlayerEmulator/Packages/Framework/Util/getTestVariation.lua @@ -0,0 +1,30 @@ +-- This function hides ABTestService away and provides a quick interface for getting a test's variation. +-- Should the requested test not exist, it will return the Control group. + +local ABTestService = game:GetService("ABTestService") + +local AB_TEST_GROUP_CONTROL = "Control" + +-- abTestName : (string) the name of test. ex) "AllUsers.Studio.ExampleTestName" +-- abTestService : (table, optional) an optional override for ABTestService +-- RETURNS : (int) the variation that the user is in. 0 = Control +return function(abTestName, abTestService) + assert(type(abTestName) == "string", "Expected abTestName to be a string") + if abTestService then + assert(type(abTestService.GetVariant) == "function", "Expected the abTestService object to have a GetVariant member function") + else + abTestService = ABTestService + end + + local variation = abTestService:GetVariant(abTestName) + if variation == AB_TEST_GROUP_CONTROL then + return 0 + else + local _, _, variationNumber = string.find(variation, "Variation(%d+)") + if not variationNumber then + return 0 + end + + return tonumber(variationNumber) + end +end diff --git a/BuiltInPlugins/PlayerEmulator/Packages/Framework/Util/getTestVariation.spec.lua b/BuiltInPlugins/PlayerEmulator/Packages/Framework/Util/getTestVariation.spec.lua new file mode 100644 index 0000000000..5febceee91 --- /dev/null +++ b/BuiltInPlugins/PlayerEmulator/Packages/Framework/Util/getTestVariation.spec.lua @@ -0,0 +1,41 @@ +return function() + local getTestVariation = require(script.Parent.getTestVariation) + local VARIATION_NAME_CONTROL = "Control" + + local function createTestService(result) + local DebugABService = {} + + function DebugABService:GetVariant(testName) + return result + end + + return DebugABService + end + + it("should expect some test name", function() + expect(function() + getTestVariation() + end).to.throw() + end) + + it("should return zero when in the control group", function() + local debugABTestService = createTestService(VARIATION_NAME_CONTROL) + local variation = getTestVariation("AllUsers.Studio.ShouldShowZero", debugABTestService) + expect(variation).to.equal(0) + end) + + it("should return the number of the variation when not in the control group", function() + local debugABTestService = createTestService("Variation1") + local variation = getTestVariation("AllUsers.Studio.ShouldShowNumber", debugABTestService) + expect(variation).to.equal(1) + + -- there aren't any variations past 8, but it should work with any number + debugABTestService = createTestService("Variation23") + variation = getTestVariation("AllUsers.Studio.ShouldShowNumber", debugABTestService) + expect(variation).to.equal(23) + + debugABTestService = createTestService("Variation456") + variation = getTestVariation("AllUsers.Studio.ShouldShowNumber", debugABTestService) + expect(variation).to.equal(456) + end) +end \ No newline at end of file diff --git a/BuiltInPlugins/PlayerEmulator/Packages/Framework/Util/tableCache.lua b/BuiltInPlugins/PlayerEmulator/Packages/Framework/Util/tableCache.lua new file mode 100644 index 0000000000..34e844e1f2 --- /dev/null +++ b/BuiltInPlugins/PlayerEmulator/Packages/Framework/Util/tableCache.lua @@ -0,0 +1,31 @@ +--[[ + Returns a table of unique values keyed on name for each component. +]] + +local function createTableCache(tableName) + local UniqueTable = {} + + UniqueTable.__tostring = function(t) + return ("%s(%s)"):format(tableName, t.name) + end + + function UniqueTable:add(name) + assert(type(name) == "string", ("%s must be created using a string name!"):format(tableName)) + + if rawget(UniqueTable, name) then + return rawget(UniqueTable, name) + else + local result = setmetatable({ + name = name + }, UniqueTable) + + UniqueTable[name] = result + + return result + end + end + + return UniqueTable +end + +return createTableCache \ No newline at end of file diff --git a/BuiltInPlugins/PlayerEmulator/Packages/Framework/Util/tableCache.spec.lua b/BuiltInPlugins/PlayerEmulator/Packages/Framework/Util/tableCache.spec.lua new file mode 100644 index 0000000000..3c8f879956 --- /dev/null +++ b/BuiltInPlugins/PlayerEmulator/Packages/Framework/Util/tableCache.spec.lua @@ -0,0 +1,40 @@ +return function() + local tableCache = require(script.Parent.tableCache) + local TABLE_CACHE_NAME = "tableCacheTest" + + describe("add", function() + it("should coerce to the given name", function() + local symbol = tableCache(TABLE_CACHE_NAME):add("foo") + expect((tostring(symbol):find("foo"))).to.be.ok() + end) + + it("should have table entires", function() + local cache = tableCache(TABLE_CACHE_NAME) + local testA = cache:add("abc") + expect(typeof(testA)).to.equal("table") + expect(tostring(testA)).to.equal("tableCacheTest(abc)") + end) + + it("should not have duplicate entries", function() + local cache = tableCache(TABLE_CACHE_NAME .. "new") + local testA = cache:add("abc") + local testB = cache:add("abc") + expect(testA).to.equal(testB) + + local count = 0 + for _,v in pairs(cache) do + if typeof(v) ~= "function" then + count = count + 1 + end + end + expect(count).to.equal(1) + end) + + it("should get the same entry for the same lookup", function() + local cache = tableCache(TABLE_CACHE_NAME) + local testA = cache["abc"] + local testB = cache["abc"] + expect(testA).to.equal(testB) + end) + end) +end diff --git a/BuiltInPlugins/PlayerEmulator/Packages/UILibrary.lua b/BuiltInPlugins/PlayerEmulator/Packages/UILibrary.lua index 7769f987a4..08a7c2267b 100644 --- a/BuiltInPlugins/PlayerEmulator/Packages/UILibrary.lua +++ b/BuiltInPlugins/PlayerEmulator/Packages/UILibrary.lua @@ -4,7 +4,7 @@ local FFlagEnableToolboxVideos = game:GetFastFlag("EnableToolboxVideos") -local Src = script +local Src = script._internal local Components = Src.Components local Utils = Src.Utils @@ -24,8 +24,6 @@ local Favorites = require(Components.Preview.Favorites) local ImagePreview = require(Components.Preview.ImagePreview) local AudioPreview = require(Components.Preview.AudioPreview) local AudioControl = FFlagEnableToolboxVideos and nil or require(Components.Preview.AudioControl) --- TODO FFlagRemoveUILibraryTimeline remove import -local Keyframe = require(Components.Timeline.Keyframe) local InfiniteScrollingFrame = require(Components.InfiniteScrollingFrame) local LoadingBar = require(Components.LoadingBar) local LoadingIndicator = require(Components.LoadingIndicator) @@ -35,8 +33,6 @@ local RadioButtons = require(Components.RadioButtons) local RoundFrame = require(Components.RoundFrame) local RoundTextBox = require(Components.RoundTextBox) local RoundTextButton = require(Components.RoundTextButton) --- TODO FFlagRemoveUILibraryTimeline remove import -local Scrubber = require(Components.Timeline.Scrubber) local SearchBar = require(Components.SearchBar) local Separator = require(Components.Separator) local StyledDialog = require(Components.StyledDialog) @@ -69,9 +65,6 @@ local Signal = require(Utils.Signal) local Dialog = require(Components.PluginWidget.Dialog) -game:DefineFastFlag("RemoveUILibraryTimeline", false) -local FFlagRemoveUILibraryTimeline = game:GetFastFlag("RemoveUILibraryTimeline") - local function createStrictTable(t) return setmetatable(t, { __index = function(_, index) @@ -100,7 +93,6 @@ local UILibrary = createStrictTable({ AudioPreview = AudioPreview, AudioControl = AudioControl, InfiniteScrollingFrame = InfiniteScrollingFrame, - Keyframe = (not FFlagRemoveUILibraryTimeline) and Keyframe or nil, LoadingBar = LoadingBar, LoadingIndicator = LoadingIndicator, ModelPreview = ModelPreview, @@ -109,7 +101,6 @@ local UILibrary = createStrictTable({ RoundFrame = RoundFrame, RoundTextBox = RoundTextBox, RoundTextButton = RoundTextButton, - Scrubber = (not FFlagRemoveUILibraryTimeline) and Scrubber or nil, SearchBar = SearchBar, Separator = Separator, StyledDialog = StyledDialog, @@ -164,14 +155,4 @@ local UILibrary = createStrictTable({ createTheme = require(Src.createTheme), }) -local virtualFolder = Instance.new("Folder") -virtualFolder.Name = "UILibraryInternals-Do-Not-Access-Directly" --- The number of parents to the plugin cannot change since UILibrary components reach out of UILibrary --- to get the plugin's copy of Roact -virtualFolder.Parent = script.Parent - -for _,v in pairs(script:GetChildren()) do - v.Parent = virtualFolder -end - return UILibrary \ No newline at end of file diff --git a/BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Camera.lua b/BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Camera.lua new file mode 100644 index 0000000000..2075f7aba5 --- /dev/null +++ b/BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Camera.lua @@ -0,0 +1,29 @@ +local Library = script.Parent + +local Roact = require(Library.Parent.Parent.Roact) +local Symbol = require(Library.Utils.Symbol) +local cameraKey = Symbol.named("MarkeplaceCamera") + +local CameraProvider = Roact.PureComponent:extend("CameraProvider") + +function CameraProvider:init(prop) + local camera = Instance.new("Camera") + camera.Name = "MarketplaceCamera" + + self._context[cameraKey] = camera +end + +function CameraProvider:render() + return Roact.oneChild(self.props[Roact.Children]) +end + +local function getCamera(component) + assert(component._context[cameraKey] ~= nil, "No CameraProvider Found") + local camera = component._context[cameraKey] + return camera +end + +return { + Provider = CameraProvider, + getCamera = getCamera, +} \ No newline at end of file diff --git a/BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Components/BaseDialog.lua b/BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Components/BaseDialog.lua new file mode 100644 index 0000000000..edb28acf30 --- /dev/null +++ b/BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Components/BaseDialog.lua @@ -0,0 +1,128 @@ +--[[ + A basic dialog with content and a set of buttons. + Other dialogs can use this component to provide more specific implementations. + While this component allows the creation of any arbitrary buttons, in most + cases a StyledDialog is preferred if the normal UILibrary buttons are desired. + + Required Props: + array Buttons = An array of items used to render + the buttons for this dialog. + function RenderButton(button, index, activated) = A function + used to render a button. This function is called for each + item in the Buttons array. It should return a Roact component + that connects a signal to the activated parameter. + + Props: + Vector2 Size = The starting size of the dialog. + Vector2 MinSize = The minimum size of the dialog, if it is resizable. + bool Resizable = Whether the dialog can be resized. + int BorderPadding = The padding to add around the edges of the dialog. + int ButtonPadding = The padding to add between buttons. + int ButtonHeight = The height of the buttons in the dialog, in pixels. + string Title = The title to display at the top of the window. + + function OnClose = A callback for when the user closed the dialog by + clicking the X in the corner of the window. + function OnButtonClicked(button) = A callback for when the user clicked + a button in the dialog. Returns the button that was clicked. +]] + +local Library = script.Parent.Parent +local Roact = require(Library.Parent.Parent.Roact) + +local Theming = require(Library.Theming) +local withTheme = Theming.withTheme + +local Dialog = require(Library.Components.PluginWidget.Dialog) + +local BaseDialog = Roact.PureComponent:extend("BaseDialog") + +function BaseDialog:init() + self.buttonClicked = function(button) + if self.props.OnButtonClicked then + self.props.OnButtonClicked(button) + end + end +end + +function BaseDialog:render() + return withTheme(function(theme) + local props = self.props + + local title = props.Title + local size = props.Size + local minSize = props.MinSize + local resizable = props.Resizable + local borderPadding = props.BorderPadding or 0 + + local buttons = props.Buttons + local buttonPadding = props.ButtonPadding or 0 + local buttonHeight = props.ButtonHeight or 0 + local renderButton = props.RenderButton + + assert(buttons ~= nil and type(buttons) == "table", + "BaseDialog requires a Buttons table.") + assert(renderButton ~= nil and type(renderButton) == "function", + "BaseDialog requires a RenderButton function.") + assert(buttonHeight ~= nil and type(buttonHeight) == "number", + "BaseDialog requires a ButtonHeight value.") + + local buttonComponents = { + Layout = Roact.createElement("UIListLayout", { + SortOrder = Enum.SortOrder.LayoutOrder, + FillDirection = Enum.FillDirection.Horizontal, + HorizontalAlignment = Enum.HorizontalAlignment.Center, + Padding = UDim.new(0, buttonPadding), + }), + } + + for index, button in ipairs(buttons) do + table.insert(buttonComponents, renderButton(button, index, function() + self.buttonClicked(button) + end)) + end + + return Roact.createElement(Dialog, { + Options = { + Size = size, + Resizable = resizable, + MinSize = minSize, + Modal = true, + InitialEnabled = true, + }, + Title = title, + OnClose = props.OnClose, + }, { + Background = Roact.createElement("Frame", { + Size = UDim2.new(1, 0, 1, 0), + BorderSizePixel = 0, + BackgroundColor3 = theme.dialog.background, + }, { + Padding = Roact.createElement("UIPadding", { + PaddingTop = UDim.new(0, borderPadding), + PaddingBottom = UDim.new(0, borderPadding), + PaddingLeft = UDim.new(0, borderPadding), + PaddingRight = UDim.new(0, borderPadding), + }), + + Content = Roact.createElement("Frame", { + LayoutOrder = 1, + Size = UDim2.new(1, 0, 1, -(buttonHeight + borderPadding)), + AnchorPoint = Vector2.new(0.5, 0), + Position = UDim2.new(0.5, 0, 0, 0), + BackgroundTransparency = 1, + }, self.props[Roact.Children]), + + Buttons = Roact.createElement("Frame", { + LayoutOrder = 2, + Size = UDim2.new(1, 0, 0, buttonHeight), + AnchorPoint = Vector2.new(0.5, 1), + Position = UDim2.new(0.5, 0, 1, 0), + BackgroundTransparency = 1, + }, buttonComponents), + }) + }) + end) +end + +return BaseDialog diff --git a/BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Components/BaseDialog.spec.lua b/BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Components/BaseDialog.spec.lua new file mode 100644 index 0000000000..ff5f454355 --- /dev/null +++ b/BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Components/BaseDialog.spec.lua @@ -0,0 +1,121 @@ +return function() + local Library = script.Parent.Parent + local Roact = require(Library.Parent.Parent.Roact) + + local MockWrapper = require(Library.MockWrapper) + + local BaseDialog = require(script.Parent.BaseDialog) + + local function createTestBaseDialog(props, children, container) + return Roact.createElement(MockWrapper, { + Container = container, + }, { + BaseDialog = Roact.createElement(BaseDialog, props, children) + }) + end + + it("should create and destroy without errors", function() + local element = createTestBaseDialog({ + Buttons = {}, + RenderButton = function(item) + end, + }) + local instance = Roact.mount(element) + Roact.unmount(instance) + end) + + it("should render correctly", function() + local container = Instance.new("Folder") + + local element = createTestBaseDialog({ + Buttons = {}, + RenderButton = function(item) + end, + }, {}, container) + + local instance = Roact.mount(element, container) + + local gui = container:FindFirstChildOfClass("BillboardGui") + expect(gui).to.be.ok() + expect(gui.FocusProvider).to.be.ok() + expect(gui.FocusProvider.Padding).to.be.ok() + expect(gui.FocusProvider.Content).to.be.ok() + expect(gui.FocusProvider.Buttons).to.be.ok() + + Roact.unmount(instance) + end) + + it("should require a Buttons table", function() + local element = createTestBaseDialog({ + RenderButton = function(item) + end, + }) + expect(function() + Roact.mount(element) + end).to.throw() + + element = createTestBaseDialog({ + Buttons = true, + RenderButton = function(item) + end, + }) + expect(function() + Roact.mount(element) + end).to.throw() + end) + + it("should require a RenderButtons function", function() + local element = createTestBaseDialog({ + Buttons = {}, + }) + expect(function() + Roact.mount(element) + end).to.throw() + + element = createTestBaseDialog({ + Buttons = {}, + RenderButton = true, + }) + expect(function() + Roact.mount(element) + end).to.throw() + end) + + it("should render its buttons", function() + local container = Instance.new("Folder") + + local element = createTestBaseDialog({ + Buttons = {"Frame"}, + RenderButton = function() + return Roact.createElement("Frame") + end, + }, {}, container) + + local instance = Roact.mount(element, container) + + local gui = container:FindFirstChildOfClass("BillboardGui") + local buttonContainer = gui.FocusProvider.Buttons + expect(buttonContainer["1"]).to.be.ok() + + Roact.unmount(instance) + end) + + it("should render its children", function() + local container = Instance.new("Folder") + + local element = createTestBaseDialog({ + Buttons = {}, + RenderButton = function() + end, + }, { + Frame = Roact.createElement("Frame"), + }, container) + + local instance = Roact.mount(element, container) + + local gui = container:FindFirstChildOfClass("BillboardGui") + expect(gui.FocusProvider.Content.Frame).to.be.ok() + + Roact.unmount(instance) + end) +end \ No newline at end of file diff --git a/BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Components/BulletPoint.lua b/BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Components/BulletPoint.lua new file mode 100644 index 0000000000..8721cc0e00 --- /dev/null +++ b/BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Components/BulletPoint.lua @@ -0,0 +1,93 @@ +--[[ + A line of text prefaced with a bullet point. Useful for lists of entries. + + Props: + string Text = The text to display after the bullet point + int LayoutOrder = Order in which the element is placed + int TextSize = The size of text + bool TextWrapped = Sets text wrapped + bool TextTruncate = Sets text truncate +]] + +local TextService = game:GetService("TextService") + +local Library = script.Parent.Parent +local Roact = require(Library.Parent.Parent.Roact) + +local Theming = require(Library.Theming) +local withTheme = Theming.withTheme + +local BulletPoint = Roact.PureComponent:extend("BulletPoint") + +local TEXT_SIZE = 20 + +function BulletPoint:init() + self.frameRef = Roact.createRef() + self.textConnection = nil + + self.updateFrameSize = function() + local frame = self.frameRef.current + local sizeX = frame.AbsoluteSize.x + local textSize = TextService:GetTextSize( + self.props.Text, + self.props.TextSize or TEXT_SIZE, + Enum.Font.SourceSans, + Vector2.new(sizeX, math.huge) + ) + frame.Size = UDim2.new(1, 0, 0, textSize.y) + end +end + +function BulletPoint:didMount() + local frame = self.frameRef.current + self.textConnection = frame:GetPropertyChangedSignal("AbsoluteSize"):connect(self.updateFrameSize) + self.updateFrameSize() +end + +function BulletPoint:willUnmount() + self.textConnection:Disconnect() + self.textConnection = nil +end + +function BulletPoint:render() + return withTheme(function(theme) + + local textSize = self.props.TextSize or TEXT_SIZE + local text = self.props.Text or "" + + return Roact.createElement("Frame", { + BackgroundTransparency = 1, + LayoutOrder = self.props.LayoutOrder or 1, + Size = UDim2.new(1, 0, 0, 0), + + [Roact.Ref] = self.frameRef, + }, { + Text = Roact.createElement("TextLabel", { + BackgroundTransparency = 1, + Position = UDim2.new(0, 16, 0, -1), + Size = UDim2.new(1, -16, 1, 0), + Text = text, + Font = theme.bulletPoint.font, + TextColor3 = theme.bulletPoint.text, + TextXAlignment = Enum.TextXAlignment.Left, + TextYAlignment = Enum.TextYAlignment.Top, + TextWrapped = self.props.TextWrapped or nil, + TextSize = textSize, + TextTruncate = self.props.TextTruncate or nil, + }), + + Dot = Roact.createElement("TextLabel", { + BackgroundTransparency = 1, + Size = UDim2.new(0, 4, 0, 4), + AnchorPoint = Vector2.new(0, 0.5), + TextColor3 = theme.bulletPoint.text, + Text = "•", + TextYAlignment = Enum.TextYAlignment.Top, + Font = theme.bulletPoint.font, + TextSize = textSize, + }), + }) + end) +end + +return BulletPoint diff --git a/BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Components/BulletPoint.spec.lua b/BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Components/BulletPoint.spec.lua new file mode 100644 index 0000000000..f2ad833346 --- /dev/null +++ b/BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Components/BulletPoint.spec.lua @@ -0,0 +1,36 @@ +return function() + local Library = script.Parent.Parent + local Roact = require(Library.Parent.Parent.Roact) + + local MockWrapper = require(Library.MockWrapper) + + local BulletPoint = require(script.Parent.BulletPoint) + + local function createTestBulletPoint(container) + return Roact.createElement(MockWrapper, { + Container = container, + }, { + BulletPoint = Roact.createElement(BulletPoint, { + Text = "test", + TextSize = 20, + }), + }) + end + + it("should create and destroy without errors", function() + local element = createTestBulletPoint() + local instance = Roact.mount(element) + Roact.unmount(instance) + end) + + it("should render correctly", function() + local container = Instance.new("Folder") + local instance = Roact.mount(createTestBulletPoint(container), container) + local bulletPoint = container:FindFirstChildOfClass("Frame") + + expect(bulletPoint.Text).to.be.ok() + expect(bulletPoint.Dot).to.be.ok() + + Roact.unmount(instance) + end) +end \ No newline at end of file diff --git a/BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Components/Button.lua b/BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Components/Button.lua new file mode 100644 index 0000000000..65825419c4 --- /dev/null +++ b/BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Components/Button.lua @@ -0,0 +1,142 @@ +--!nolint UnknownGlobal +--^ DEVTOOLS-4930 + +--[[ + A button with rounded corners. Colors are based on Theme. + + Required Props: + function RenderContents(theme, hovered) = A function that returns the + contents that will display in the button. The parameters passed + allow the function to style the contents based on the button's current + theme and/or produce different contents if the button is hovered. + + Props: + string Style = The theme to use for this button. Ex. "Default", "Primary". + Styles for buttons can be found in createTheme.lua. + string StyleState = Normally controlled by the button (e.g. hovered), but can + be overwritten with something like 'disabled' to pull from override themes + + UDim2 Size = The size of the button. + UDim2 Position = The position of the button. + Vector2 AnchorPoint = The center point of the button. + int LayoutOrder = The order in which this button appears in a UILayout. + int ZIndex = The display index of this button. + int BorderSizePixel = Border size of the button +]] + +local Library = script.Parent.Parent + +local Roact = require(Library.Parent.Parent.Roact) + +local Theming = require(Library.Theming) +local withTheme = Theming.withTheme +local join = require(Library.join) + +local RoundFrame = require(Library.Components.RoundFrame) + +local Button = Roact.PureComponent:extend("Button") + +function Button:init(initialProps) + self.state = { + hovered = false, + pressed = false + } + + self.onClick = function() + if self.props.OnClick then + self.props.OnClick() + end + end + + self.mouseEnter = function() + self:setState({ + hovered = true, + }) + end + + self.mouseLeave = function() + self:setState({ + hovered = false, + pressed = false + }) + end + + self.onMouseDown = function() + self:setState({ + hovered = true, + pressed = true, + }) + end + + self.onMouseUp = function() + self:setState({ + pressed = false, + }) + end +end + +function Button:render() + return withTheme(function(theme) + local props = self.props + local state = self.state + local hovered = state.hovered + local style = props.Style + local styleState = props.StyleState + local size = props.Size + local position = props.Position + local anchorPoint = props.AnchorPoint + local layoutOrder = props.LayoutOrder + local renderContents = props.RenderContents + local zIndex = props.ZIndex + local borderSize = props.BorderSizePixel + + assert(renderContents ~= nil and type(renderContents) == "function", + "Button requires a RenderContents function.") + + local buttonTheme = style and theme.button[style] or theme.button.Default + if styleState then + buttonTheme = join(buttonTheme, buttonTheme[styleState]) + elseif pressed then + buttonTheme = join(buttonTheme, buttonTheme.pressed) + elseif hovered then + buttonTheme = join(buttonTheme, buttonTheme.hovered) + end + + local isRound = buttonTheme.isRound + local content = renderContents(buttonTheme, hovered, pressed) + + local buttonProps = { + Size = size, + Position = position, + AnchorPoint = anchorPoint, + LayoutOrder = layoutOrder, + ZIndex = zIndex, + + BackgroundColor3 = buttonTheme.backgroundColor, + BorderColor3 = buttonTheme.borderColor, + BorderSizePixel = borderSize, + } + + if isRound then + return Roact.createElement(RoundFrame, join(buttonProps, { + OnActivated = self.onClick, + OnMouseEnter = self.mouseEnter, + OnMouseLeave = self.mouseLeave, + [Roact.Event.MouseButton1Down] = self.onMouseDown, + [Roact.Event.MouseButton1Up] = self.onMouseUp, + }), content) + else + return Roact.createElement("ImageButton", join(buttonProps, { + AutoButtonColor = false, + + [Roact.Event.MouseEnter] = self.mouseEnter, + [Roact.Event.MouseLeave] = self.mouseLeave, + [Roact.Event.Activated] = self.onClick, + [Roact.Event.MouseButton1Down] = self.onMouseDown, + [Roact.Event.MouseButton1Up] = self.onMouseUp, + }), content) + end + end) +end + +return Button diff --git a/BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Components/Button.spec.lua b/BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Components/Button.spec.lua new file mode 100644 index 0000000000..3a47c1896a --- /dev/null +++ b/BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Components/Button.spec.lua @@ -0,0 +1,79 @@ +return function() + local Library = script.Parent.Parent + local Roact = require(Library.Parent.Parent.Roact) + + local MockWrapper = require(Library.MockWrapper) + + local Button = require(script.Parent.Button) + + local function createTestButton(props, container) + return Roact.createElement(MockWrapper, { + Container = container, + }, { + Button = Roact.createElement(Button, props) + }) + end + + it("should create and destroy without errors", function() + local element = createTestButton({ + RenderContents = function() + end, + }) + local instance = Roact.mount(element) + Roact.unmount(instance) + end) + + it("should render correctly", function() + local container = Instance.new("Folder") + + local element = createTestButton({ + RenderContents = function() + end, + }, container) + + local instance = Roact.mount(element, container) + + local button = container:FindFirstChildOfClass("ImageButton") + expect(button).to.be.ok() + expect(button.Border).to.be.ok() + + Roact.unmount(instance) + end) + + it("should require a RenderContents function", function() + local element = createTestButton() + expect(function() + Roact.mount(element) + end).to.throw() + + element = createTestButton({ + RenderContents = true, + }) + expect(function() + Roact.mount(element) + end).to.throw() + end) + + it("should render the children in RenderContents", function() + local container = Instance.new("Folder") + + local element = createTestButton({ + RenderContents = function() + return { + SomeFrame = Roact.createElement("Frame"), + OtherFrame = Roact.createElement("Frame"), + } + end, + }, container) + + local instance = Roact.mount(element, container) + + local button = container:FindFirstChildOfClass("ImageButton") + expect(button).to.be.ok() + expect(button.Border).to.be.ok() + expect(button.Border.SomeFrame).to.be.ok() + expect(button.Border.OtherFrame).to.be.ok() + + Roact.unmount(instance) + end) +end \ No newline at end of file diff --git a/BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Components/CheckBox.lua b/BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Components/CheckBox.lua new file mode 100644 index 0000000000..8c8bf6f16e --- /dev/null +++ b/BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Components/CheckBox.lua @@ -0,0 +1,96 @@ +--[[ + Clickable checkbox, from a CheckBoxSet. + + Props: + string Id = Unique identifier of this CheckBox + string Title = Text to display on this CheckBox + bool Selected = Whether to display this CheckBox as selected + bool Enabled = Whether this CheckBox accepts input + int Height = How big the CheckBox should be + int TextSize = How big the CheckBox's text should be + func OnActivated = What happens when the CheckBox is clicked + int titlePadding = How many pixels to the right of the icon the title is put +]] + +local TextService = game:GetService("TextService") + +local Library = script.Parent.Parent +local Roact = require(Library.Parent.Parent.Roact) +local Theming = require(Library.Theming) +local withTheme = Theming.withTheme + +local CheckBox = Roact.PureComponent:extend("CheckBox") + +function CheckBox:init() + self.onActivated = function() + if self.props.Enabled then + self.props.OnActivated() + end + end +end + +function CheckBox:render() + return withTheme(function(theme) + local props = self.props + + local title = props.Title + local height = props.Height + local enabled = props.Enabled + local layoutOrder = props.LayoutOrder + local selected = props.Selected + local textSize = props.TextSize + local titlePadding = props.TitlePadding or 5 + + local titleSize = TextService:GetTextSize( + title, + textSize, + theme.checkBox.font, + Vector2.new() + ) + local titleWidth = titleSize.X + + return Roact.createElement("Frame", { + Size = UDim2.new(1, 0, 0, height), + BackgroundTransparency = 1, + BorderSizePixel = 0, + LayoutOrder = layoutOrder or 1, + }, { + Background = Roact.createElement("ImageButton", { + Size = UDim2.new(0, height, 0, height), + BackgroundTransparency = 1, + ImageTransparency = enabled and 0 or 0.4, + Image = theme.checkBox.backgroundImage, + ImageColor3 = theme.checkBox.backgroundColor, + + [Roact.Event.Activated] = self.onActivated, + }, { + Selection = Roact.createElement("ImageLabel", { + BackgroundTransparency = 1, + Size = UDim2.new(1, 0, 1, 0), + Visible = enabled and selected, + Image = theme.checkBox.selectedImage, + }), + + TitleLabel = Roact.createElement("TextButton", { + BackgroundTransparency = 1, + BorderSizePixel = 0, + Size = UDim2.new(0, titleWidth, 1, 0), + AnchorPoint = Vector2.new(0, 0.5), + Position = UDim2.new(1, titlePadding, 0.5, 0), + + TextColor3 = theme.checkBox.titleColor, + Font = theme.checkBox.font, + TextSize = textSize, + TextXAlignment = Enum.TextXAlignment.Left, + TextYAlignment = Enum.TextYAlignment.Center, + TextTransparency = enabled and 0 or 0.5, + Text = title, + + [Roact.Event.Activated] = self.onActivated, + }), + }), + }) + end) +end + +return CheckBox \ No newline at end of file diff --git a/BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Components/CheckBox.spec.lua b/BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Components/CheckBox.spec.lua new file mode 100644 index 0000000000..2a2d149eb1 --- /dev/null +++ b/BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Components/CheckBox.spec.lua @@ -0,0 +1,64 @@ +return function() + local Library = script.Parent.Parent + local Roact = require(Library.Parent.Parent.Roact) + + local MockWrapper = require(Library.MockWrapper) + + local CheckBox = require(script.Parent.CheckBox) + + local function createTestCheckBox(enabled, selected) + return Roact.createElement(MockWrapper, {}, { + checkBox = Roact.createElement(CheckBox, { + Title = "Title", + TextSize = 24, + Enabled = enabled, + Selected = selected, + OnClicked = function() + end, + }) + }) + end + + it("should create and destroy without errors", function() + local element = createTestCheckBox(true, false) + local instance = Roact.mount(element) + Roact.unmount(instance) + end) + + it("should render correctly", function () + local container = Instance.new("Folder") + local instance = Roact.mount(createTestCheckBox(true, false), container) + local frame = container:FindFirstChildOfClass("Frame") + + expect(frame.Background).to.be.ok() + expect(frame.Background.Selection).to.be.ok() + expect(frame.Background.TitleLabel).to.be.ok() + + Roact.unmount(instance) + end) + + it("should change color when highlighted", function () + local container = Instance.new("Folder") + + -- selected + local instance = Roact.mount(createTestCheckBox(true, true), container) + local frame = container:FindFirstChildOfClass("Frame") + expect(frame.Background.Selection.Visible).to.equal(true) + + -- unselected + instance = Roact.update(instance, createTestCheckBox(true, false)) + expect(frame.Background.Selection.Visible).to.equal(false) + Roact.unmount(instance) + end) + + it("should gray out when disabled", function () + local container = Instance.new("Folder") + local instance = Roact.mount(createTestCheckBox(false, true), container) + local frame = container:FindFirstChildOfClass("Frame") + + expect(frame.Background.Selection.Visible).to.equal(false) + expect(frame.Background.TitleLabel.TextTransparency).never.to.equal(0) + + Roact.unmount(instance) + end) +end \ No newline at end of file diff --git a/BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Components/DetailedDropdown.lua b/BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Components/DetailedDropdown.lua new file mode 100644 index 0000000000..9221875dd1 --- /dev/null +++ b/BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Components/DetailedDropdown.lua @@ -0,0 +1,353 @@ +--[[ + A dropdown menu styled to match the Roblox Studio start page. + Consists of a button used to open the dropdown as well as the menu itself. + Note that the logic for opening and closing the menu is contained within this component, + but the consumer is responsible for showing the current value in the button. + + Required Props: + UDim2 Size = The size of the button that opens the dropdown. + UDim2 Position = The position of the button that opens the dropdown. + int DisplayTextSize = The size of the text in the dropdown and button. + int DescriptionTextSize = The size of the subtext in the dropdown + int ItemHeight = The height of each entry in the dropdown, in pixels. + string ButtonText = Text to display currently selected option in menu + array Items = An ordered array of each item that should appear in the dropdown. + The array is formatted like this: + { + {Key = "Item1", Display = "SomeLocalizedTextForItem1", Description = "SomeLocalizedDescriptionForItem1"}, + {Key = "Item2", Display = "SomeLocalizedTextForItem2", Description = "SomeLocalizedDescriptionForItem2"}, + {Key = "Item3", Display = "SomeLocalizedTextForItem3", Description = "SomeLocalizedDescriptionForItem3"}, + } + Key is how the item will be referenced in code. Text is what will appear to the user. + function OnItemClicked(item) = A callback when the user selects an item in the dropdown. + Returns the item as it was defined in the Items array. + bool Enabled = Enables component if true and accepts input + + Optional Props: + int MaxItems = The maximum number of entries that can display at a time. + If this is less than the number of entries in the dropdown, a scrollbar will appear. + bool ShowRibbon = Whether to show a colored ribbon next to the currently + hovered dropdown entry. Usually should be enabled for Light theme only. + int TextPadding = The amount of padding, in pixels, around the text elements. + int IconSize = The size of the arrow icon in the button. + int IconPadding = The distance from the right side of the arrow icon to the button edge. + int ScrollBarPadding = The padding which appears on either side of the scrollbar. + int ScrollBarThickness = The horizontal width of the scrollbar. +]] +local FFlagStudioFixUILibDropdownStyle = game:GetFastFlag("StudioFixUILibDropdownStyle") +local FFlagStudioFixUILibDropdownText = game:GetFastFlag("StudioFixUILibDropdownText") + +-- Defaults +local TEXT_PADDING = 10 +local ICON_SIZE = 12 +local ICON_PADDING = 4 + +local RIBBON_WIDTH = 5 +local VERTICAL_OFFSET = 2 + +local MAX_WIDTH = 300 + +local TextService = game:GetService("TextService") + +local Library = script.Parent.Parent +local Roact = require(Library.Parent.Parent.Roact) + +local Theming = require(Library.Theming) +local withTheme = Theming.withTheme + +local DropdownMenu = require(Library.Components.DropdownMenu) +local RoundFrame = require(Library.Components.RoundFrame) +local createFitToContent = require(Library.Components.createFitToContent) + +local DetailedDropdown = Roact.PureComponent:extend("DetailedDropdown") + +local FitToContent = createFitToContent("Frame", "UIListLayout", { + SortOrder = Enum.SortOrder.LayoutOrder, + Padding = UDim.new(0, TEXT_PADDING), +}) + +function DetailedDropdown:init() + self.state = { + showDropdown = false, + isButtonHovered = false, + dropdownItem = nil, + } + self.buttonRef = Roact.createRef() + + self.onItemClicked = function(item) + if self.props.Enabled then + self.props.OnItemClicked(item.Key) + self.hideDropdown() + end + end + + self.showDropdown = function() + if self.props.Enabled then + self:setState({ + showDropdown = true, + }) + end + end + + self.hideDropdown = function() + if self.props.Enabled then + self:setState({ + showDropdown = false, + }) + end + end + + self.onKeyMouseEnter = function(item) + if self.props.Enabled then + self:setState({ + dropdownItem = item, + }) + end + end + + self.onKeyMouseLeave = function(item) + if self.props.Enabled then + if self.state.dropdownItem == item then + self:setState({ + dropdownItem = Roact.None, + }) + end + end + end + + self.onMouseEnter = function() + if self.props.Enabled then + self:setState({ + isButtonHovered = true, + }) + end + end + + self.onMouseLeave = function() + if self.props.Enabled then + self:setState({ + isButtonHovered = false, + }) + end + end +end + +function DetailedDropdown:createMainTextLabel(key, displayText, displayTextSize, displayTextColor, textPadding, font, height) + return Roact.createElement("TextLabel", { + Size = UDim2.new(1, 0, 0, height), + Font = font, + TextSize = displayTextSize, + Text = displayText, + TextXAlignment = Enum.TextXAlignment.Left, + TextColor3 = displayTextColor, + BackgroundTransparency = 1, + TextWrapped = true, + LayoutOrder = 0, + }, { + Padding = Roact.createElement("UIPadding", { + PaddingTop = UDim.new(0, textPadding), + PaddingLeft = UDim.new(0, textPadding), + }), + }) +end + +function DetailedDropdown:createDescriptionTextLabel(key, descriptionText, descriptionTextSize, descriptionTextColor, textPadding, font, height) + return Roact.createElement("TextLabel", { + Size = UDim2.new(1, 0, 0, height), + Font = font, + TextSize = descriptionTextSize, + Text = descriptionText, + TextXAlignment = Enum.TextXAlignment.Left, + TextColor3 = descriptionTextColor, + BackgroundTransparency = 1, + TextWrapped = true, + LayoutOrder = 1, + }, { + Padding = Roact.createElement("UIPadding", { + PaddingLeft = UDim.new(0, textPadding), + PaddingRight = UDim.new(0, textPadding), + }), + }) +end + +function DetailedDropdown:render() + return withTheme(function(theme) + local props = self.props + local state = self.state + local dropdownTheme = theme.detailedDropdown + + local showDropdown = state.showDropdown + local buttonRef = self.buttonRef and self.buttonRef.current + local buttonExtents + if buttonRef then + local buttonMin = buttonRef.AbsolutePosition + local buttonSize = buttonRef.AbsoluteSize + local buttonMax = buttonMin + buttonSize + buttonExtents = Rect.new(buttonMin.X, buttonMin.Y, buttonMax.X, buttonMax.Y) + end + + local items = props.Items or {} + local selectedItem = props.SelectedItem + local size = props.Size + local position = props.Position + local displayTextSize = props.DisplayTextSize + local descriptionTextSize = props.DescriptionTextSize + local itemHeight = props.ItemHeight + local maxItems = props.MaxItems + local showRibbon = props.ShowRibbon + local enabled = props.Enabled + + local textPadding = props.TextPadding or TEXT_PADDING + local iconSize = props.IconSize or ICON_SIZE + local iconPadding = props.IconPadding or ICON_PADDING + local scrollBarPadding = props.ScrollBarPadding + local scrollBarThickness = props.ScrollBarThickness + + local dropdownItem = state.dropdownItem + local isButtonHovered = state.isButtonHovered + local buttonText = props.ButtonText + + local maxItemWidth = 0 + local maxWidth = props.MaxWidth or MAX_WIDTH + local maxHeight = maxItems and (maxItems * itemHeight) or nil + + for _, data in ipairs(items) do + local displayTextBound = TextService:GetTextSize(data.Display, + displayTextSize, dropdownTheme.font, Vector2.new(math.huge, math.huge)) + + local displayItemWidth = displayTextBound.X + textPadding * 2 + + local descriptionTextBound = TextService:GetTextSize(data.Description, + descriptionTextSize, dropdownTheme.font, Vector2.new(math.huge, math.huge)) + + local descriptionItemWidth = descriptionTextBound.X + textPadding * 2 + + maxItemWidth = math.max(maxItemWidth, displayItemWidth, descriptionItemWidth) + end + + maxWidth = math.min(maxItemWidth, maxWidth) + + local hoverTheme = dropdownTheme.selected + if FFlagStudioFixUILibDropdownStyle then + hoverTheme = dropdownTheme.hovered + end + + local buttonTheme = (showDropdown or isButtonHovered) and hoverTheme + or dropdownTheme + + return Roact.createElement("ImageButton", { + LayoutOrder = props.LayoutOrder or 0, + AnchorPoint = props.AnchorPoint or Vector2.new(0,0), + Size = size, + Position = position, + BackgroundTransparency = 1, + Image = "", + + [Roact.Ref] = self.buttonRef, + + [Roact.Event.Activated] = self.showDropdown, + [Roact.Event.MouseEnter] = self.onMouseEnter, + [Roact.Event.MouseLeave] = self.onMouseLeave, + }, { + RoundFrame = Roact.createElement(RoundFrame, { + Size = UDim2.new(1, 0, 1, 0), + BackgroundColor3 = enabled and buttonTheme.backgroundColor or buttonTheme.disabled, + BorderColor3 = buttonTheme.borderColor, + }), + + ArrowIcon = Roact.createElement("ImageLabel", { + Size = UDim2.new(0, iconSize, 0, iconSize), + Position = UDim2.new(1, -iconPadding, 0.5, 0), + AnchorPoint = Vector2.new(1, 0.5), + BackgroundTransparency = 1, + ImageColor3 = enabled and buttonTheme.displayText or buttonTheme.disabledText, + Image = dropdownTheme.arrowImage, + }), + + TextLabel = Roact.createElement("TextLabel", { + Size = UDim2.new(1, FFlagStudioFixUILibDropdownText and -iconSize or 0, 1, 0), + BackgroundTransparency = 1, + Font = dropdownTheme.font, + TextColor3 = enabled and buttonTheme.displayText or buttonTheme.disabledText, + TextSize = displayTextSize, + Text = buttonText, + TextXAlignment = Enum.TextXAlignment.Left, + TextTruncate = FFlagStudioFixUILibDropdownText and Enum.TextTruncate.AtEnd or nil, + }, { + Padding = Roact.createElement("UIPadding", { + PaddingLeft = UDim.new(0, textPadding), + }), + }), + + Dropdown = showDropdown and buttonRef and Roact.createElement(DropdownMenu, { + OnItemClicked = self.onItemClicked, + OnFocusLost = self.hideDropdown, + SourceExtents = buttonExtents, + Offset = Vector2.new(0, VERTICAL_OFFSET), + MaxHeight = maxHeight, + ShowBorder = false, + ScrollBarPadding = scrollBarPadding, + ScrollBarThickness = scrollBarThickness, + + Items = items, + RenderItem = function(item, index, activated) + local key = item.Key + local selected = key == selectedItem + local displayText = item.Display + local descriptionText = item.Description + local isHovered = dropdownItem == key + local displayTextColor = isHovered and dropdownTheme.hovered.displayText + or dropdownTheme.displayText + local descriptionTextColor = dropdownTheme.descriptionText + + local displayTextBound = TextService:GetTextSize(displayText, + displayTextSize, dropdownTheme.font, Vector2.new(maxWidth, math.huge)) + + local descriptionTextBound = TextService:GetTextSize(descriptionText, + descriptionTextSize, dropdownTheme.font, Vector2.new(maxWidth, math.huge)) + + local itemColor = dropdownTheme.backgroundColor + if FFlagStudioFixUILibDropdownStyle and selected then + itemColor = dropdownTheme.selected.backgroundColor + elseif isHovered then + itemColor = dropdownTheme.hovered.backgroundColor + end + + return Roact.createElement("ImageButton", { + Size = UDim2.new(0, maxWidth, 0, displayTextBound.Y + descriptionTextBound.Y + textPadding * 2), + BackgroundColor3 = itemColor, + BorderSizePixel = 0, + LayoutOrder = index, + AutoButtonColor = false, + [Roact.Event.Activated] = activated, + [Roact.Event.MouseEnter] = function() + self.onKeyMouseEnter(key) + end, + [Roact.Event.MouseLeave] = function() + self.onKeyMouseLeave(key) + end, + }, { + Roact.createElement(FitToContent, { + LayoutOrder = index, + BackgroundTransparency = 1, + } , { + Ribbon = isHovered and showRibbon and Roact.createElement("Frame", { + Size = UDim2.new(0, RIBBON_WIDTH, 1, 0), + BackgroundColor3 = dropdownTheme.selected.backgroundColor, + BorderSizePixel = 0, + }), + + MainTextLabel = self:createMainTextLabel(key, displayText, displayTextSize, displayTextColor, + textPadding, dropdownTheme.font, displayTextBound.Y), + + DescriptionTextLabel = self:createDescriptionTextLabel(key, descriptionText, descriptionTextSize, descriptionTextColor, + textPadding, dropdownTheme.font, descriptionTextBound.Y), + }) + }) + end, + }) + }) + end) +end + +return DetailedDropdown diff --git a/BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Components/DetailedDropdown.spec.lua b/BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Components/DetailedDropdown.spec.lua new file mode 100644 index 0000000000..80388c2ca8 --- /dev/null +++ b/BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Components/DetailedDropdown.spec.lua @@ -0,0 +1,34 @@ +return function() + local Library = script.Parent.Parent + local Roact = require(Library.Parent.Parent.Roact) + + local MockWrapper = require(Library.MockWrapper) + + local DetailedDropdown = require(script.Parent.DetailedDropdown) + + local function createTestDetailedDropdown(props, children) + return Roact.createElement(MockWrapper, {}, { + DetailedDropdown = Roact.createElement(DetailedDropdown, props, children) + }) + end + + it("should create and destroy without errors", function() + local element = createTestDetailedDropdown() + local instance = Roact.mount(element) + Roact.unmount(instance) + end) + + it("should render correctly", function() + local container = Instance.new("Folder") + local instance = Roact.mount(createTestDetailedDropdown(), container) + local button = container:FindFirstChildOfClass("ImageButton") + + expect(button).to.be.ok() + expect(button.RoundFrame).to.be.ok() + expect(button.ArrowIcon).to.be.ok() + expect(button.TextLabel).to.be.ok() + expect(button.TextLabel.Padding).to.be.ok() + + Roact.unmount(instance) + end) +end \ No newline at end of file diff --git a/BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Components/DragTarget.lua b/BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Components/DragTarget.lua new file mode 100644 index 0000000000..a4666cb555 --- /dev/null +++ b/BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Components/DragTarget.lua @@ -0,0 +1,58 @@ +--[[ + A component that can listen to change in mouse position while active, + and then has a callback for removal once the user is done dragging. + + Props: + function OnDragMoved(input) = A callback for when the user drags + the mouse. The input param is the InputObject from the InputChanged event. + + function OnDragEnded() = A callback for when the user has stopped dragging. + + Usage: + From a stateful component, hold on to a dragging state. When the user + presses the mouse on a draggable element, set the dragging state to + true. When dragging is true, render this element. Hook up this element's + OnDragEnded function to setting the dragging state to false. +]] + +local Library = script.Parent.Parent +local Roact = require(Library.Parent.Parent.Roact) + +local Focus = require(Library.Focus) +local CaptureFocus = Focus.CaptureFocus + +local DragTarget = Roact.PureComponent:extend("DragTarget") + +function DragTarget:init() + self.inputChanged = function(rbx, input) + if input.UserInputType == Enum.UserInputType.MouseMovement then + if self.props.OnDragMoved then + self.props.OnDragMoved(input) + end + end + end + + self.inputEnded = function() + if self.props.OnDragEnded then + self.props.OnDragEnded() + end + end +end + +function DragTarget:render() + return Roact.createElement(CaptureFocus, { + OnFocusLost = self.inputEnded, + }, { + DragListener = Roact.createElement("ImageButton", { + BackgroundTransparency = 1, + Size = UDim2.new(1, 0, 1, 0), + + [Roact.Event.InputChanged] = self.inputChanged, + [Roact.Event.InputEnded] = self.inputEnded, + [Roact.Event.MouseButton1Up] = self.inputEnded, + [Roact.Event.MouseButton2Up] = self.inputEnded, + }, self.props[Roact.Children]) + }) +end + +return DragTarget diff --git a/BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Components/DragTarget.spec.lua b/BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Components/DragTarget.spec.lua new file mode 100644 index 0000000000..e94a8dee44 --- /dev/null +++ b/BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Components/DragTarget.spec.lua @@ -0,0 +1,38 @@ +return function() + local Library = script.Parent.Parent + local Roact = require(Library.Parent.Parent.Roact) + + local MockWrapper = require(Library.MockWrapper) + + local DragTarget = require(script.Parent.DragTarget) + + local function createTestDragTarget(container) + return Roact.createElement(MockWrapper, { + Container = container, + }, { + DragTarget = Roact.createElement(DragTarget) + }) + end + + it("should create and destroy without errors", function() + local element = createTestDragTarget() + local instance = Roact.mount(element) + Roact.unmount(instance) + end) + + it("should render correctly", function() + local container = Instance.new("Folder") + + local element = createTestDragTarget(container) + + local instance = Roact.mount(element, container) + + local gui = container:FindFirstChild("MockGui") + expect(gui).to.be.ok() + expect(gui.TopLevelDetector).to.be.ok() + expect(gui.TopLevelDetector.ScrollBlocker).to.be.ok() + expect(gui.TopLevelDetector.ScrollBlocker.DragListener).to.be.ok() + + Roact.unmount(instance) + end) +end \ No newline at end of file diff --git a/BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Components/DropShadow.lua b/BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Components/DropShadow.lua new file mode 100644 index 0000000000..ace6eaede4 --- /dev/null +++ b/BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Components/DropShadow.lua @@ -0,0 +1,58 @@ +--[[ + A rectangular drop shadow that appears behind an element. + + Props: + Vector2 Offset = The offset of this drop shadow from the element it appears beneath. + float Transparency = The transparency of the drop shadow, from 0 to 1. + Color3 Color = The color of the drop shadow. + SizePixel = The size of the drop shadow, in pixels. + ZIndex = The render order of the drop shadow. Make sure it is behind your element. +]] + +local Library = script.Parent.Parent + +local Roact = require(Library.Parent.Parent.Roact) + +local Theming = require(Library.Theming) +local withTheme = Theming.withTheme + +local SLICE_SIZE = 8 +local DROP_SHADOW_SLICE = Rect.new(SLICE_SIZE, SLICE_SIZE, SLICE_SIZE, SLICE_SIZE) + +local DropShadow = Roact.PureComponent:extend("DropShadow") + +function DropShadow:render() + return withTheme(function(theme) + local props = self.props + local shadowTheme = theme.dropShadow + + local shadowColor = props.Color + local shadowTransparency = props.Transparency + local offset = props.Offset or Vector2.new() + local shadowSize = props.SizePixel or SLICE_SIZE + local zindex = props.ZIndex or 0 + + -- SliceScale is multiplicative, so we need to normalize to the slice size + local sliceScale = shadowSize / SLICE_SIZE + + return Roact.createElement("ImageLabel", { + Size = UDim2.new(1, shadowSize, 1, shadowSize), + AnchorPoint = Vector2.new(0.5, 0.5), + Position = UDim2.new(0.5, offset.X, 0.5, offset.Y), + ZIndex = zindex, + + BackgroundTransparency = 1, + BorderSizePixel = 0, + + Image = shadowTheme.image, + ImageColor3 = shadowColor, + ImageTransparency = shadowTransparency, + + ScaleType = Enum.ScaleType.Slice, + SliceCenter = DROP_SHADOW_SLICE, + SliceScale = sliceScale, + }) + end) +end + +return DropShadow diff --git a/BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Components/DropShadow.spec.lua b/BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Components/DropShadow.spec.lua new file mode 100644 index 0000000000..e3c71b1050 --- /dev/null +++ b/BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Components/DropShadow.spec.lua @@ -0,0 +1,30 @@ +return function() + local Library = script.Parent.Parent + local Roact = require(Library.Parent.Parent.Roact) + + local MockWrapper = require(Library.MockWrapper) + + local DropShadow = require(script.Parent.DropShadow) + + local function createTestDropShadow(props) + return Roact.createElement(MockWrapper, {}, { + DropShadow = Roact.createElement(DropShadow, props) + }) + end + + it("should create and destroy without errors", function() + local element = createTestDropShadow() + local instance = Roact.mount(element) + Roact.unmount(instance) + end) + + it("should render correctly", function () + local container = Instance.new("Folder") + local instance = Roact.mount(createTestDropShadow(), container) + local shadow = container:FindFirstChildOfClass("ImageLabel") + + expect(shadow).to.be.ok() + + Roact.unmount(instance) + end) +end \ No newline at end of file diff --git a/BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Components/DropdownMenu.lua b/BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Components/DropdownMenu.lua new file mode 100644 index 0000000000..506de613f3 --- /dev/null +++ b/BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Components/DropdownMenu.lua @@ -0,0 +1,241 @@ +--[[ + A generic dropdown menu interface which can accept any kind of components. + The consuming component is in charge of implementing the logic that dictates + when this dropdown menu should show and hide. + + This dropdown detects if it is too close to the corners of the gui and realigns if needed. + For example, if it is too close to the bottom of the gui to render all elements, it + renders its elements above the hosting button instead of below. + + For an example of how this component can be used, see StyledDropdown. + + Required Props: + Rect SourceExtents = A Rect representing the absolute position and size of + the button which is hosting this dropdown. + + table Items = An ordered array of each item that should appear in the dropdown. + Each item in the array can be of any format, and will be passed to the RenderItem function. + function RenderItem(item, index, activated) = A function used to render a dropdown item. + Item is an entry from the Items array that was passed into this component's props. + Index is the index of the current item in the Items array. + Activated is a callback that the item should connect if it is clickable. + + function OnItemClicked(item) = A callback for when the user selects a dropdown entry. + Returns the item as it was defined in the Items array. + function OnFocusLost = A callback for when the user clicks away from the dropdown + without selecting an item. + + Optional Props: + int MaxHeight = An optional maximum height for this dropdown. If the items surpass + the max height, a scrollbar will be added to the dropdown so all items are visible. + int ScrollBarPadding = The padding which appears on either side of the scrollbar. + int ScrollBarThickness = The horizontal width of the scrollbar. + bool ShowBorder = Whether to show a border around the elements in the dropdown. + Vector2 Offset = An offset from the button which is hosting this dropdown. + Note that the dropdown already takes into account the size of the hosting button, + and will already automatically place itself below the button. This offset is optional + and can be used to add some extra padding. + Enum.VerticalAlignment StartDirection=Bottom The direction the DropdownMenu will appear + from SourceExtents by default. This can only be Top/Bottom. This will not lock the + direction of the DropdownMenu. If there is not enough room in the default direction, + it will flip to the other direction +]] + +local ROUNDED_FRAME_SLICE = Rect.new(3, 3, 13, 13) +local SCROLLBAR_THICKNESS = 8 +local SCROLLBAR_PADDING = 2 + +local RunService = game:GetService("RunService") + +local Library = script.Parent.Parent +local Roact = require(Library.Parent.Parent.Roact) + +local Theming = require(Library.Theming) +local withTheme = Theming.withTheme + +local Focus = require(Library.Focus) +local CaptureFocus = Focus.CaptureFocus +local withFocus = Focus.withFocus + +local StyledScrollingFrame = require(Library.Components.StyledScrollingFrame) + +local DropdownMenu = Roact.PureComponent:extend("DropdownMenu") + +function DropdownMenu:init(props) + assert( props.StartDirection == Enum.VerticalAlignment.Top or + props.StartDirection == Enum.VerticalAlignment.Bottom or + props.StartDirection == nil, + + "StartDirection must be Enum.VerticalAlignment.Bottom, Enum.VerticalAlignment.Top, or nil. " + .."Got '"..tostring(props.StartDirection).."'" + ) + + self.direction = props.StartDirection or Enum.VerticalAlignment.Bottom + self.layout = 0 + + self.recalculateSize = function(rbx) + -- We have to wait one step to change the state here, or + -- we will change the state while the component is rendering + -- and the component won't move to the right location. + local nextStep + nextStep = RunService.Heartbeat:Connect(function() + nextStep:Disconnect() + + -- The component may have since been unmounted, in which case + -- we shouldn't update state or it will fail with an error + if not self.mounted then return end + + self:setState({ + menuSize = rbx.AbsoluteContentSize + }) + end) + end + + self.resetLayout = function() + self.layout = 0 + end + + self.nextLayout = function() + self.layout = self.layout + 1 + return self.layout + end +end + +function DropdownMenu:didMount() + self.mounted = true +end + +function DropdownMenu:willUnmount() + self.mounted = false +end + +function DropdownMenu:render() + return withTheme(function(theme) + return withFocus(function(pluginGui) + local props = self.props + local state = self.state + + local items = props.Items + local renderItem = self.props.RenderItem + local sourceExtents = props.SourceExtents + + assert(items ~= nil and type(items) == "table", + "DropdownMenu requires an Items table.") + assert(renderItem ~= nil and type(renderItem) == "function", + "DropdownMenu requires a RenderItem function.") + assert(sourceExtents ~= nil, + "DropdownMenu requires a SourceExtents prop.") + + local components = {} + + local dropdownTheme = theme.dropdownMenu + + local canRender = state.menuSize ~= nil + local menuSize = state.menuSize or Vector2.new() + local width = props.ListWidth or menuSize.X + local height = menuSize.Y + + local offset = props.Offset or Vector2.new() + local showBorder = props.ShowBorder + local scrollBarThickness = props.ScrollBarThickness or SCROLLBAR_THICKNESS + local scrollBarPadding = props.ScrollBarPadding or SCROLLBAR_PADDING + + local maxHeight = props.MaxHeight + if maxHeight == nil or maxHeight > height then + maxHeight = height + elseif maxHeight < height then + -- Add scrollbar gutter + width = width + scrollBarThickness + (scrollBarPadding * 2) + end + + local sourcePosition = sourceExtents.Min + local sourceSize = Vector2.new(sourceExtents.Width, sourceExtents.Height) + local guiSize = pluginGui.AbsoluteSize + + local xPos, yPos + if sourcePosition.X + offset.X + width <= guiSize.X then + xPos = sourcePosition.X + offset.X + else + xPos = sourcePosition.X + sourceSize.X + offset.X - width + end + + local enoughRoomOnBottom = sourcePosition.Y + sourceSize.Y + offset.Y + maxHeight < guiSize.Y + local enoughRoomOnTop = sourcePosition.Y - offset.Y - maxHeight > 0 + + -- Don't flip if there is not enough room on either side. This will just cause a spasm of + -- flip-flopping every render + if enoughRoomOnBottom or enoughRoomOnTop then + if self.direction == Enum.VerticalAlignment.Bottom and not enoughRoomOnBottom then + self.direction = Enum.VerticalAlignment.Top + elseif self.direction == Enum.VerticalAlignment.Top and not enoughRoomOnTop then + self.direction = Enum.VerticalAlignment.Bottom + end + end + + local verticalAlignment + if self.direction == Enum.VerticalAlignment.Bottom then + yPos = sourcePosition.Y + sourceSize.Y + offset.Y + verticalAlignment = Enum.VerticalAlignment.Top + else + yPos = sourcePosition.Y - offset.Y - maxHeight + verticalAlignment = Enum.VerticalAlignment.Bottom + end + + local position = UDim2.new(0, xPos, 0, yPos) + + components.Layout = Roact.createElement("UIListLayout", { + SortOrder = Enum.SortOrder.LayoutOrder, + FillDirection = Enum.FillDirection.Vertical, + VerticalAlignment = verticalAlignment, + [Roact.Change.AbsoluteContentSize] = self.recalculateSize, + }) + + for index, item in ipairs(items) do + table.insert(components, renderItem(item, index, function() + self.props.OnItemClicked(item) + end)) + end + + local contents = { + Border = showBorder and Roact.createElement("ImageLabel", { + Visible = canRender, + Position = position, + Size = UDim2.new(0, width, 0, maxHeight), + ZIndex = 3, + + BackgroundTransparency = 1, + ImageColor3 = dropdownTheme.borderColor, + + Image = dropdownTheme.borderImage, + ScaleType = Enum.ScaleType.Slice, + SliceCenter = ROUNDED_FRAME_SLICE, + }), + } + + if maxHeight and maxHeight < height then + contents.ScrollingContainer = Roact.createElement(StyledScrollingFrame, { + Visible = canRender, + Position = position, + Size = UDim2.new(0, width, 0, maxHeight), + BackgroundTransparency = 1, + CanvasSize = UDim2.new(0, 0, 0, height), + ScrollBarPadding = scrollBarPadding, + ScrollBarThickness = scrollBarThickness, + }, components) + else + contents.Container = Roact.createElement("Frame", { + Visible = canRender, + Position = position, + Size = UDim2.new(0, width, 0, height), + BackgroundTransparency = 1, + }, components) + end + + return Roact.createElement(CaptureFocus, { + OnFocusLost = props.OnFocusLost, + }, contents) + end) + end) +end + +return DropdownMenu diff --git a/BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Components/DropdownMenu.spec.lua b/BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Components/DropdownMenu.spec.lua new file mode 100644 index 0000000000..36fa138ed8 --- /dev/null +++ b/BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Components/DropdownMenu.spec.lua @@ -0,0 +1,228 @@ +return function() + local Library = script.Parent.Parent + local Roact = require(Library.Parent.Parent.Roact) + + local MockWrapper = require(Library.MockWrapper) + + local DropdownMenu = require(script.Parent.DropdownMenu) + + local sourceExtents = Rect.new(0, 0, 150, 150) + + local function createTestDropdownMenu(props, children, container) + return Roact.createElement(MockWrapper, { + Container = container, + }, { + DropdownMenu = Roact.createElement(DropdownMenu, props, children) + }) + end + + it("should create and destroy without errors", function() + local element = createTestDropdownMenu({ + SourceExtents = sourceExtents, + Items = {}, + RenderItem = function(item) + end, + }) + local instance = Roact.mount(element) + Roact.unmount(instance) + end) + + it("should render correctly", function() + local container = Instance.new("Folder") + + local element = createTestDropdownMenu({ + SourceExtents = sourceExtents, + Items = {}, + RenderItem = function(item) + end, + }, {}, container) + + local instance = Roact.mount(element, container) + + local gui = container:FindFirstChild("MockGui") + expect(gui).to.be.ok() + expect(gui.TopLevelDetector).to.be.ok() + expect(gui.TopLevelDetector.ScrollBlocker).to.be.ok() + expect(gui.TopLevelDetector.ScrollBlocker.Container).to.be.ok() + + local dropdownContainer = gui.TopLevelDetector.ScrollBlocker.Container + expect(dropdownContainer.Layout).to.be.ok() + + Roact.unmount(instance) + end) + + it("should require an Items table", function() + local element = createTestDropdownMenu({ + RenderItem = function() + end, + SourceExtents = sourceExtents, + }) + expect(function() + Roact.mount(element) + end).to.throw() + + element = createTestDropdownMenu({ + Items = true, + RenderItem = function() + end, + SourceExtents = sourceExtents, + }) + expect(function() + Roact.mount(element) + end).to.throw() + end) + + it("should require a RenderItem function", function() + local element = createTestDropdownMenu({ + Items = {}, + SourceExtents = sourceExtents, + }) + expect(function() + Roact.mount(element) + end).to.throw() + + element = createTestDropdownMenu({ + Items = {}, + RenderItem = true, + SourceExtents = sourceExtents, + }) + expect(function() + Roact.mount(element) + end).to.throw() + end) + + it("should require a SourceExtents prop", function() + local element = createTestDropdownMenu({ + Items = {}, + RenderItem = function() + end, + }) + expect(function() + Roact.mount(element) + end).to.throw() + end) + + it("should error if given invalid StartDirection", function() + local element = createTestDropdownMenu({ + Items = {}, + SourceExtents = sourceExtents, + RenderItem = function() + end, + StartDirection = 0, + }) + expect(function() + Roact.mount(element) + end).to.throw() + end) + + it("should render items", function() + local container = Instance.new("Folder") + + local element = createTestDropdownMenu({ + SourceExtents = sourceExtents, + Items = {"Frame"}, + RenderItem = function() + return Roact.createElement("Frame") + end, + }, {}, container) + + local instance = Roact.mount(element, container) + + local gui = container:FindFirstChild("MockGui") + local dropdownContainer = gui.TopLevelDetector.ScrollBlocker.Container + expect(dropdownContainer["1"]).to.be.ok() + + Roact.unmount(instance) + end) + + it("should respect the order of items", function() + local container = Instance.new("Folder") + + local element = createTestDropdownMenu({ + SourceExtents = sourceExtents, + Items = {"FirstFrame", "SecondFrame", "ThirdFrame"}, + RenderItem = function(item, index) + return Roact.createElement("TextLabel", { + LayoutOrder = index, + Text = item, + }) + end, + }, {}, container) + + local instance = Roact.mount(element, container) + + local gui = container:FindFirstChild("MockGui") + local dropdownContainer = gui.TopLevelDetector.ScrollBlocker.Container + expect(dropdownContainer["1"].LayoutOrder).to.equal(1) + expect(dropdownContainer["2"].LayoutOrder).to.equal(2) + expect(dropdownContainer["3"].LayoutOrder).to.equal(3) + expect(dropdownContainer["1"].Text).to.equal("FirstFrame") + expect(dropdownContainer["2"].Text).to.equal("SecondFrame") + expect(dropdownContainer["3"].Text).to.equal("ThirdFrame") + + Roact.unmount(instance) + end) + + it("should preserve menu direction when there is enough room", function() + local function getMenuDirection(listLayout) + return listLayout.VerticalAlignment == Enum.VerticalAlignment.Top and -1 or 1 + end + + local container = Instance.new("Folder") + + local elementAtTop = createTestDropdownMenu({ + SourceExtents = Rect.new(0, 0, 150, 150), + Items = {"FirstFrame"}, + RenderItem = function(item, index) + return Roact.createElement("TextLabel", { + LayoutOrder = index, + Text = item, + Size = UDim2.new(1, 0, 0, 20), + }) + end, + }, {}, container) + + local instance = Roact.mount(elementAtTop, container) + + local gui = container:FindFirstChild("MockGui") + local listLayout = gui.TopLevelDetector.ScrollBlocker.Container.Layout + + -- No way to get MockGui canvas size to dock SourceExtents at the bottom/middle unless + -- we mount it and then check the instance's size + local elementAtBottom = createTestDropdownMenu({ + SourceExtents = Rect.new(0, gui.AbsoluteSize.Y + 150, 150, 150), + Items = {"FirstFrame"}, + RenderItem = function(item, index) + return Roact.createElement("TextLabel", { + LayoutOrder = index, + Text = item, + Size = UDim2.new(1, 0, 0, 20), + }) + end, + }, {}, container) + local elementInMiddle = createTestDropdownMenu({ + SourceExtents = Rect.new(0, math.floor(gui.AbsoluteSize.Y/2), 150, 150), + Items = {"FirstFrame"}, + RenderItem = function(item, index) + return Roact.createElement("TextLabel", { + LayoutOrder = index, + Text = item, + Size = UDim2.new(1, 0, 0, 20), + }) + end, + }, {}, container) + + -- The default direction is down, so we should be displaying beneath SourceExtents + expect(getMenuDirection(listLayout)).to.equal(-1) + + -- There is not enough room below, so we flip to top + Roact.update(instance, elementAtBottom) + expect(getMenuDirection(listLayout)).to.equal(1) + + -- There is now enough room below, but we preserve direction, so we are still above SourceExtents + Roact.update(instance, elementInMiddle) + expect(getMenuDirection(listLayout)).to.equal(1) + + Roact.unmount(instance) + end) +end \ No newline at end of file diff --git a/BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Components/ExpandableList.lua b/BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Components/ExpandableList.lua new file mode 100644 index 0000000000..a29b4fdb27 --- /dev/null +++ b/BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Components/ExpandableList.lua @@ -0,0 +1,108 @@ +--[[ + A generic expandable list interface which can accept any kind of components. Intended to be a lightweight control + where it will toggle visibility of a frame which contains content that user will pass in + + Required Props: + table TopLevelItem = Property which takes in table of Roact elements to display top level button. + Will always be displayed and entire element(s) will be clickable to toggle dropdown visibility + table Content = Property which takes in table of Roact elements to display in dropdown area. + + LayoutOrder = props.LayoutOrder (Required, passed through) + Position = props.Position (Required, passed through) + AnchorPoint = props.AnchorPoint (Required, passed through) + + function OnExpandedStateChanged() - Invoked whenever the ExpandableList is opened/closed + bool IsExpanded - Whether the ExpandableList is expanded or not +]] + +local Library = script.Parent.Parent +local Roact = require(Library.Parent.Parent.Roact) + +local Theming = require(Library.Theming) +local withTheme = Theming.withTheme + +local join = require(Library.join) + +local createFitToContent = require(Library.Components.createFitToContent) + +local ExpandableList = Roact.PureComponent:extend("ExpandableList") + +local ContentFit = createFitToContent("Frame", "UIListLayout", { + SortOrder = Enum.SortOrder.LayoutOrder, + Padding = UDim.new(0, 0), +}) + +local TopLevelContentFit = createFitToContent("ImageButton", "UIListLayout", { + SortOrder = Enum.SortOrder.LayoutOrder, + Padding = UDim.new(0, 0), + FillDirection = Enum.FillDirection.Horizontal, +}) + +local TopLevelItem + +function ExpandableList:init() + self.state = { + isButtonHovered = false, + } + self.buttonRef = Roact.createRef() + + self.toggleList = function() + self.props.OnExpandedStateChanged() + end + + self.onMouseEnter = function() + self:setState({ + isButtonHovered = true, + }) + end + + self.onMouseLeave = function() + self:setState({ + isButtonHovered = false, + }) + end +end + + +function ExpandableList:render() + return withTheme(function(theme) + local props = self.props + local state = self.state + + local topLevelItem = props.TopLevelItem + local content = props.Content + + assert(topLevelItem ~= nil and type(topLevelItem) == "table", + "ExpandableList requires a TopLevelItem table.") + assert(content ~= nil and type(content) == "table", + "ExpandableList requires Content table.") + + return Roact.createElement(ContentFit, { + BackgroundTransparency = 1, + LayoutOrder = props.LayoutOrder or 0, + AnchorPoint = props.AnchorPoint or Vector2.new(0,0), + BorderSizePixel = 0, + Position = props.Position, + }, { + TopLevelItem = Roact.createElement(TopLevelContentFit, { + LayoutOrder = 0, + BorderSizePixel = 0, + BackgroundTransparency = 1, + Image = "", + + [Roact.Ref] = self.buttonRef, + [Roact.Event.Activated] = self.toggleList, + [Roact.Event.MouseEnter] = self.onMouseEnter, + [Roact.Event.MouseLeave] = self.onMouseLeave, + }, topLevelItem), + + ExpandableFrame = Roact.createElement(ContentFit, { + LayoutOrder = 1, + BackgroundTransparency = 1, + Visible = props.IsExpanded, + }, content), + }) + end) +end + +return ExpandableList diff --git a/BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Components/ExpandableList.spec.lua b/BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Components/ExpandableList.spec.lua new file mode 100644 index 0000000000..31f1645e77 --- /dev/null +++ b/BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Components/ExpandableList.spec.lua @@ -0,0 +1,143 @@ +return function() + local Library = script.Parent.Parent + local Roact = require(Library.Parent.Parent.Roact) + + local MockWrapper = require(Library.MockWrapper) + + local ExpandableList = require(script.Parent.ExpandableList) + + local function createTestExpandableList(props, children, container) + return Roact.createElement(MockWrapper, { + Container = container, + }, { + ExpandableList = Roact.createElement(ExpandableList, props, children) + }) + end + + it("should create and destroy without errors", function() + local element = createTestExpandableList({ + TopLevelItem = {}, + Content = {}, + }) + local instance = Roact.mount(element) + Roact.unmount(instance) + end) + + it("should require a top level item table", function() + local element = createTestExpandableList({ + Content = {}, + }) + expect(function() + Roact.mount(element) + end).to.throw() + local element = createTestExpandableList({ + TopLevelItem = true, + Content = {} + }) + expect(function() + Roact.mount(element) + end).to.throw() + end) + + it("should require a content table", function() + local element = createTestExpandableList({ + TopLevelItem = {}, + }) + expect(function() + Roact.mount(element) + end).to.throw() + local element = createTestExpandableList({ + TopLevelItem = {}, + Content = true + }) + expect(function() + Roact.mount(element) + end).to.throw() + end) + + it("should render correctly", function () + local container = Instance.new("Folder") + local instance = Roact.mount(createTestExpandableList({ + TopLevelItem = {}, + Content = {}, + }, {}, container), container) + local frame = container:FindFirstChildOfClass("Frame") + + expect(frame.TopLevelItem).to.be.ok() + expect(frame.ExpandableFrame).to.be.ok() + + Roact.unmount(instance) + end) + + it("items should be sized to contents", function () + local container = Instance.new("Folder") + local instance = Roact.mount(createTestExpandableList({ + TopLevelItem = { + ArrowIcon = Roact.createElement("TextLabel", { + Size = UDim2.new(0, 100, 0, 100), + }), + }, + Content = { + Label = Roact.createElement("TextLabel", { + Size = UDim2.new(0, 100, 0, 100), + }), + }, + }, {}, container), container) + local frame = container:FindFirstChildOfClass("Frame") + + expect(frame.TopLevelItem.Size.X.Scale).to.equal(1) + expect(frame.TopLevelItem.Size.Y.Offset).to.equal(100) + + expect(frame.ExpandableFrame.Size.X.Scale).to.equal(1) + expect(frame.ExpandableFrame.Size.Y.Offset).to.equal(100) + + + Roact.unmount(instance) + end) + + it("list should only show top item initially", function () + local container = Instance.new("Folder") + local instance = Roact.mount(createTestExpandableList({ + TopLevelItem = { + ArrowIcon = Roact.createElement("TextLabel", { + Size = UDim2.new(0, 100, 0, 100), + }), + }, + Content = { + Label = Roact.createElement("TextLabel", { + Size = UDim2.new(0, 100, 0, 100), + }), + }, + IsExpanded = false, + }, {}, container), container) + local frame = container:FindFirstChildOfClass("Frame") + + expect(frame.Size.X.Scale).to.equal(1) + expect(frame.Size.Y.Offset).to.equal(100) + + Roact.unmount(instance) + end) + + it("list should only show both items when expanded is true", function () + local container = Instance.new("Folder") + local instance = Roact.mount(createTestExpandableList({ + TopLevelItem = { + ArrowIcon = Roact.createElement("TextLabel", { + Size = UDim2.new(0, 100, 0, 100), + }), + }, + Content = { + Label = Roact.createElement("TextLabel", { + Size = UDim2.new(0, 100, 0, 100), + }), + }, + IsExpanded = true, + }, {}, container), container) + local frame = container:FindFirstChildOfClass("Frame") + + expect(frame.Size.X.Scale).to.equal(1) + expect(frame.Size.Y.Offset).to.equal(200) + + Roact.unmount(instance) + end) +end \ No newline at end of file diff --git a/BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Components/InfiniteScrollingFrame.lua b/BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Components/InfiniteScrollingFrame.lua new file mode 100644 index 0000000000..aff10bcdb8 --- /dev/null +++ b/BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Components/InfiniteScrollingFrame.lua @@ -0,0 +1,136 @@ +--[[ + A scroll frame that will check the left space between currently rendered asset + When scrolling down, it will try to re-render. If we found less than defined space or empty space + between the render asset and the canvas, then we will try to call request more function defined in the property + to fetch more assets. The function is responsible for the paging method to fetch more assets. + After the asset is returned, we will re-calculate canvase size. + This component will send out request to try to load more pages on didMount and after didUpdate. + + Required Properties: + function NextPageFunc - called during re-render when there is more empty spaces. This function should includes all the + parameters needed for the request except for the currentPage. Target page will be determined by the infiScroller. + + Optional Properties: + UDim2 Position - The position of the scrolling frame. + UDim2 Size - The size of the scrolling frame. + int LayoutOrder - sets order of element in layout + int NextPageRequestDistance - space left in layout before making request to fetch more elements + int CanvasHeight - used to specify height of canvas. + Roact ref LayoutRef - used to calculate the height of the canvas. +]] + +local Library = script.Parent.Parent +local Roact = require(Library.Parent.Parent.Roact) + +local StyledScrollingFrame = require(Library.Components.StyledScrollingFrame) + +local InfiniteScrollingFrame = Roact.PureComponent:extend("InfiniteScrollingFrame") + +local DEFAULT_CANVAS_HEIGHT = 900 + +local DEFAULT_REQUEST_DISTANCE = 0 + +local FFlagUILibraryInfiniteScrollingFrameRender = game:DefineFastFlag("UILibraryInfiniteScrollingFrameRender", false) + +function InfiniteScrollingFrame:init(props) + self.state = { + isRequestingNextPage = false, + } + + self.isRequestingNextPage = false + + self.scrollingFrameRef = Roact.createRef() + + self.checkCanvasAndRequest = function() + local scrollingFrame = self.scrollingFrameRef.current + if not scrollingFrame then return end + + local canvasY = scrollingFrame.CanvasPosition.Y + local windowHeight = scrollingFrame.AbsoluteWindowSize.Y + local canvasHeight = scrollingFrame.CanvasSize.Y.Offset + + local requestDistance = self.props.NextPageRequestDistance or DEFAULT_REQUEST_DISTANCE + + -- Where the bottom of the scrolling frame is relative to canvas size + local bottom = canvasY + windowHeight + local dist = canvasHeight - bottom + + if dist <= requestDistance and not self.state.isRequestingNextPage then + if FFlagUILibraryInfiniteScrollingFrameRender then + self.isRequestingNextPage = true + else + self:setState({ + isRequestingNextPage = true, + }) + end + self.requestNextPage() + end + end + + self.onScroll = function() + self.checkCanvasAndRequest(self) + end + + self.requestNextPage = function() + self.props.NextPageFunc() + end +end + +function InfiniteScrollingFrame:didMount() + self.checkCanvasAndRequest(self) +end + +function InfiniteScrollingFrame:didUpdate(previousProps, previousState) + -- check if request has fetched more children + if previousState.isRequestingNextPage then + for k,v in pairs(self.props[Roact.Children]) do + if v ~= previousProps[Roact.Children][k] then + if FFlagUILibraryInfiniteScrollingFrameRender then + self.isRequestingNextPage = false + else + self:setState({ + isRequestingNextPage = false, + }) + end + self.checkCanvasAndRequest(self) + end + end + end +end + +function InfiniteScrollingFrame:render() + local props = self.props + + local nextPageFunc = self.props.NextPageFunc + + assert(nextPageFunc ~= nil and type(nextPageFunc) == "function", + "InfiniteScrollingFrame requires a NextPageFunc function.") + + local position = props.Position + local size = props.Size + local layoutOrder = props.LayoutOrder + + local layout= props.LayoutRef and props.LayoutRef.current + local canvasHeight = DEFAULT_CANVAS_HEIGHT + if layout then + canvasHeight = layout.AbsoluteContentSize.Y + elseif props.CanvasHeight then + canvasHeight = props.CanvasHeight + end + + return Roact.createElement(StyledScrollingFrame, { + Position = position, + Size = size, + LayoutOrder = layoutOrder, + CanvasSize = UDim2.new(1, 0, 0, canvasHeight), + ZIndex = 1, + + ScrollingEnabled = true, + + OnScroll = self.onScroll, + + [Roact.Ref] = self.scrollingFrameRef, + }, props[Roact.Children]) +end + +return InfiniteScrollingFrame diff --git a/BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Components/InfiniteScrollingFrame.spec.lua b/BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Components/InfiniteScrollingFrame.spec.lua new file mode 100644 index 0000000000..a48c231372 --- /dev/null +++ b/BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Components/InfiniteScrollingFrame.spec.lua @@ -0,0 +1,69 @@ +return function() + local Library = script.Parent.Parent + local Roact = require(Library.Parent.Parent.Roact) + + local join = require(Library.join) + local MockWrapper = require(Library.MockWrapper) + + local InfiniteScrollingFrame = require(script.Parent.InfiniteScrollingFrame) + + local function createTestScrollingFrame(props, children) + props = join(props or {}, { + NextPageFunc = function() + return "foo" + end + }) + + return Roact.createElement(MockWrapper, {}, { + ScrollingFrame = Roact.createElement(InfiniteScrollingFrame, props, children) + }) + end + + it("should create and destroy without errors", function() + local element = createTestScrollingFrame() + local instance = Roact.mount(element) + Roact.unmount(instance) + end) + + it("should render correctly", function() + local container = Instance.new("Folder") + local instance = Roact.mount(createTestScrollingFrame(), container) + local frame = container:FindFirstChildOfClass("Frame") + + expect(frame).to.be.ok() + expect(frame.ScrollBarBackground).to.be.ok() + expect(frame.ScrollingFrame).to.be.ok() + + Roact.unmount(instance) + end) + + it("should render its children in the ScrollingFrame", function() + local container = Instance.new("Folder") + local instance = Roact.mount(createTestScrollingFrame({}, { + ChildFrame = Roact.createElement("Frame"), + }), container) + local frame = container:FindFirstChildOfClass("Frame") + + expect(frame).to.be.ok() + expect(frame.ScrollBarBackground).to.be.ok() + expect(frame.ScrollingFrame).to.be.ok() + expect(frame.ScrollingFrame.ChildFrame).to.be.ok() + + Roact.unmount(instance) + end) + + it("should add padding to both sides of the ScrollBar", function() + local container = Instance.new("Folder") + local instance = Roact.mount(createTestScrollingFrame({ + ScrollBarPadding = 2, + ScrollBarThickness = 8, + }), container) + local frame = container:FindFirstChildOfClass("Frame") + + expect(frame).to.be.ok() + expect(frame.ScrollBarBackground).to.be.ok() + expect(frame.ScrollBarBackground.Size.X.Offset).to.equal(12) + + Roact.unmount(instance) + end) +end \ No newline at end of file diff --git a/BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Components/LoadingBar.lua b/BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Components/LoadingBar.lua new file mode 100644 index 0000000000..08a000aba3 --- /dev/null +++ b/BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Components/LoadingBar.lua @@ -0,0 +1,115 @@ +--[[ + LoadingBar - creates a loading bar used for validation/publishing + + 1. loads to holdPercent + 2. waits for onFinish to be non-nil + 3. loads to 100% + 4. loads to 150% (so the user can see the finished loading bar for a short delay) + + Necessary Props: + string LoadingText - the loading bar text + number HoldPercent [0, 1] - percentage to wait at + number LoadingTime - total time it takes to load without waiting for onFinish + bool InstallationFinished - indicates whether or not the installation has fininshed. +]] + +local Library = script.Parent.Parent +local Roact = require(Library.Parent.Parent.Roact) + +local RunService = game:GetService("RunService") + +local RoundFrame = require(Library.Components.RoundFrame) + +local LOADING_TITLE_HEIGHT = 20 +local LOADING_TITLE_PADDING = 10 + +local Theming = require(Library.Theming) +local withTheme = Theming.withTheme + +local LoadingBar = Roact.Component:extend("LoadingBar") + +function LoadingBar:init(props) + self:setState({ + progress = 0, + time = 0, + }) +end + +function LoadingBar:loadUntil(percent) + while self.state.progress < percent do + local dt = RunService.RenderStepped:Wait() + if not self.isMounted then + break + end + local newTime = self.state.time + dt + self:setState({ + time = newTime, + progress = newTime/self.props.LoadingTime + }) + end +end + +function LoadingBar:didMount() + self.isMounted = true + spawn(function() + -- go to 92% + self:loadUntil(self.props.HoldPercent) + + -- wait until props.onFinish + while self.isMounted and not self.props.InstallationFinished do + RunService.RenderStepped:Wait() + end + + -- go to 100% + self:loadUntil(1) + + -- wait for a moment to show "full loading screen" + self:loadUntil(1.5) + end) +end + +function LoadingBar:willUnmount() + self.isMounted = false +end + +function LoadingBar:render() + return withTheme(function(theme) + local props = self.props + local state = self.state + + local progress = math.min(math.max(state.progress, 0), 1) + local loadingText = props.LoadingText .. " ( " .. math.floor((progress * 100) + 0.5) .. "% )" + + return Roact.createElement("Frame", { + BackgroundTransparency = 1, + Size = props.Size, + Position = props.Position, + }, { + LoadingTitle = Roact.createElement("TextLabel", { + BackgroundTransparency = 1, + Font = theme.loadingBar.font, + Position = UDim2.new(0, 0, 0, -(LOADING_TITLE_HEIGHT + LOADING_TITLE_PADDING)), + Size = UDim2.new(1, 0, 0, LOADING_TITLE_HEIGHT), + Text = loadingText, + TextColor3 = theme.loadingBar.text, + TextSize = theme.loadingBar.fontSize, + TextXAlignment = Enum.TextXAlignment.Center, + TextYAlignment = Enum.TextYAlignment.Center, + }), + + LoadingBackgroundBar = Roact.createElement(RoundFrame, { + BorderSizePixel = 0, + BackgroundColor3 = theme.loadingBar.bar.backgroundColor, + Size = UDim2.new(1, 0, 1, 0), + }, { + LoadingBar = Roact.createElement(RoundFrame, { + BorderSizePixel = 0, + BackgroundColor3 = theme.loadingBar.bar.foregroundColor, + Size = UDim2.new(progress, 0, 1, 0), + }), + }), + }) + end) +end + +return LoadingBar \ No newline at end of file diff --git a/BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Components/LoadingIndicator.lua b/BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Components/LoadingIndicator.lua new file mode 100644 index 0000000000..e75ebd6901 --- /dev/null +++ b/BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Components/LoadingIndicator.lua @@ -0,0 +1,139 @@ +--[[ + Loading indicator + + Props: + Vector2 AnchorPoint = Vector2.new(0, 0) + UDim2 Position = UDim2.new(0, 0, 0, 0) + UDim2 Size = UDim2.new(0, 92, 0, 24) + number ZIndex = 0 + boolean Visible = true + number Count = 3 : number of blocks in loading animation + number GapRatio = 1.5 : sets gap between blocks + number EndRatio = 0.25 : used for calculating block width +]] + +local Library = script.Parent.Parent +local Roact = require(Library.Parent.Parent.Roact) + +local Theming = require(Library.Theming) +local withTheme = Theming.withTheme + +local RunService = game:GetService("RunService") + +local LoadingIndicator = Roact.PureComponent:extend("LoadingIndicator") + +local ANIMATION_SPEED = 5 +local DEFAULT_BLOCK_COUNT = 3 + +function LoadingIndicator:init() + self.state = { + animationTime = math.pi / 2, + sinTime = 1, + direction = 1, + index = 1, + } + + self.animationConnection = RunService.RenderStepped:connect(function(deltaTime) + self:updateAnimation(deltaTime) + end) +end + +function LoadingIndicator:willUnmount() + if self.animationConnection then + self.animationConnection:Disconnect() + end +end + +function LoadingIndicator:updateAnimation(deltaTime) + self:setState(function(prevState, props) + local newAnimationTime = prevState.animationTime + deltaTime + local newSinTime = math.sin(newAnimationTime * ANIMATION_SPEED) + + local direction = prevState.direction + local newDirection = direction + local newIndex = prevState.index + + -- If sin has changed sign, move to the next block + if (direction > 0 and newSinTime < 0) or (direction < 0 and newSinTime > 0) then + newDirection = -direction + newIndex = newIndex + 1 + + if newIndex > (self.props.count or DEFAULT_BLOCK_COUNT) then + newIndex = 1 + end + end + + return { + animationTime = newAnimationTime, + sinTime = newSinTime, + direction = newDirection, + index = newIndex, + } + end) +end + +function LoadingIndicator:render() + return withTheme(function(theme) + local props = self.props + local state = self.state + + local loadingIndicatorTheme = theme.loadingIndicator + + local baseColor = loadingIndicatorTheme.baseColor + local endColor = loadingIndicatorTheme.endColor + + local anchorPoint = props.AnchorPoint or Vector2.new(0, 0) + local position = props.Position or UDim2.new(0, 0, 0, 0) + local size = props.Size or UDim2.new(0, 92, 0, 24) + local zindex = props.ZIndex or 0 + local visible = (props.Visible ~= nil and props.Visible) or (props.Visible == nil) + + local blockCount = props.Count or DEFAULT_BLOCK_COUNT + + local gapBetweenBlockRatio = props.GapRatio or 1.5 + local endRatio = props.EndRatio or 0.25 + + local blockWidth = 1 / (blockCount + (blockCount * gapBetweenBlockRatio) - gapBetweenBlockRatio + (2 * endRatio)) + local gapWidth = blockWidth * gapBetweenBlockRatio + + local smallHeight = 0.6 + + local children = { + UIListLayout = Roact.createElement("UIListLayout", { + SortOrder = Enum.SortOrder.LayoutOrder, + FillDirection = Enum.FillDirection.Horizontal, + Padding = UDim.new(gapWidth, 0), + HorizontalAlignment = Enum.HorizontalAlignment.Center, + VerticalAlignment = Enum.VerticalAlignment.Center, + }), + } + + local sinTime = math.abs(state.sinTime) + local index = state.index + + for i = 1, blockCount, 1 do + local height = i == index and smallHeight + ((1 - smallHeight) * sinTime) or smallHeight + + local color = i == index and baseColor:lerp(endColor, sinTime) or baseColor + + children["Frame" .. i] = Roact.createElement("Frame", { + Size = UDim2.new(blockWidth, 0, height, 0), + LayoutOrder = i, + BorderSizePixel = 0, + BackgroundColor3 = color, + }) + end + + return Roact.createElement("Frame", { + AnchorPoint = anchorPoint, + Position = position, + Size = size, + ZIndex = zindex, + BorderSizePixel = 0, + Visible = visible, + BackgroundTransparency = 1, + }, children) + end) +end + +return LoadingIndicator diff --git a/BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Components/LoadingIndicator.spec.lua b/BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Components/LoadingIndicator.spec.lua new file mode 100644 index 0000000000..3a99ec2859 --- /dev/null +++ b/BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Components/LoadingIndicator.spec.lua @@ -0,0 +1,16 @@ +return function() + local Library = script.Parent.Parent + local Roact = require(Library.Parent.Parent.Roact) + + local MockWrapper = require(Library.MockWrapper) + + local LoadingIndicator = require(script.Parent.LoadingIndicator) + + it("should create and destroy without errors", function() + local element = Roact.createElement(MockWrapper, {}, { + LoadingIndicator = Roact.createElement(LoadingIndicator), + }) + local instance = Roact.mount(element) + Roact.unmount(instance) + end) +end diff --git a/BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Components/MultilineTextEntry.lua b/BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Components/MultilineTextEntry.lua new file mode 100644 index 0000000000..aa221dcf69 --- /dev/null +++ b/BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Components/MultilineTextEntry.lua @@ -0,0 +1,143 @@ +--[[ + A multiline text entry with a dynmically appearing scrollbar. + Used in a RoundTextBox when Multiline is true. + + Props: + string Text = The text to display + bool Visible = Whether to display this component + int TextSize = The size of text + function SetText(text) = Callback to tell parent that text has changed + function FocusChanged(focus) = Callback to tell parent that this component has focus + function HoverChanged(hovered) = Callback when the mouse enters or leaves this component. +]] + +local TextService = game:GetService("TextService") + +local Library = script.Parent.Parent +local Roact = require(Library.Parent.Parent.Roact) + +local StyledScrollingFrame = require(Library.Components.StyledScrollingFrame) + +local SCROLL_BAR_OUTSET = 9 + +local MultilineTextEntry = Roact.PureComponent:extend("MultilineTextEntry") + +function MultilineTextEntry:init() + self.frameRef = Roact.createRef() + self.textBoxRef = Roact.createRef() + self.textConnections = nil + + -- TODO: Get rid of function and replace with API call CLIPLAYEREX-2806 when it ships + self.getPositionAtIndex = function(index) + local frame = self.frameRef.current + local sizeX = frame.AbsoluteSize.x - SCROLL_BAR_OUTSET + local textSize = TextService:GetTextSize( + string.sub(self.props.Text, 0, index), + self.props.TextSize, + Enum.Font.SourceSans, + Vector2.new(sizeX, math.huge) + ) + return textSize + end + + self.updateCanvas = function() + local frame = self.frameRef.current + local sizeX = frame.AbsoluteSize.x - SCROLL_BAR_OUTSET + local textBox = self.textBoxRef.current + local textSize = TextService:GetTextSize( + self.props.Text, + self.props.TextSize, + Enum.Font.SourceSans, + Vector2.new(sizeX, math.huge) + ) + frame.CanvasSize = UDim2.new(0, 0, 0, textSize.y) + frame.CanvasPosition = Vector2.new(0, self.getPositionAtIndex(textBox.CursorPosition).y - 2 * self.props.TextSize) + end + + self.textChanged = function(rbx) + if rbx.Text ~= self.props.Text then + self.props.SetText(rbx.Text) + end + end + + self.mouseEnter = function() + self.props.HoverChanged(true) + end + self.mouseLeave = function() + self.props.HoverChanged(false) + end +end + +function MultilineTextEntry:didMount() + local textBox = self.textBoxRef.current + local frame = self.frameRef.current + self.textConnections = { + textBox:GetPropertyChangedSignal("Text"):connect(self.updateCanvas), + frame:GetPropertyChangedSignal("AbsoluteSize"):connect(self.updateCanvas), + } + self.updateCanvas() +end + +function MultilineTextEntry:willUnmount() + for _, connection in ipairs(self.textConnections) do + connection:Disconnect() + end + self.textConnections = nil +end + +function MultilineTextEntry:render() + local visible = self.props.Visible + local text = self.props.Text + local textColor = self.props.TextColor3 + local textSize = self.props.TextSize + local font = self.props.Font + + + return Roact.createElement(StyledScrollingFrame, { + Size = UDim2.new(1, SCROLL_BAR_OUTSET, 1, 0), + BackgroundTransparency = 1, + ClipsDescendants = true, + ShowBackground = false, + + [Roact.Ref] = self.frameRef, + }, { + Padding = Roact.createElement("UIPadding", { + PaddingRight = UDim.new(0, SCROLL_BAR_OUTSET), + }), + + Text = Roact.createElement("TextBox", { + Visible = visible, + MultiLine = true, + TextWrapped = true, + + Size = UDim2.new(1, 0, 1, 0), + BackgroundTransparency = 1, + BorderSizePixel = 0, + + ClearTextOnFocus = false, + Font = font, + TextSize = textSize, + TextXAlignment = Enum.TextXAlignment.Left, + TextYAlignment = Enum.TextYAlignment.Top, + TextColor3 = textColor, + Text = text, + + [Roact.Event.Focused] = function() + self.props.FocusChanged(true) + end, + + [Roact.Event.FocusLost] = function() + self.props.FocusChanged(false) + end, + + [Roact.Event.MouseEnter] = self.mouseEnter, + [Roact.Event.MouseLeave] = self.mouseLeave, + + [Roact.Change.Text] = self.textChanged, + + [Roact.Ref] = self.textBoxRef, + }), + }) +end + +return MultilineTextEntry diff --git a/BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Components/MultilineTextEntry.spec.lua b/BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Components/MultilineTextEntry.spec.lua new file mode 100644 index 0000000000..30bf91066b --- /dev/null +++ b/BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Components/MultilineTextEntry.spec.lua @@ -0,0 +1,47 @@ +return function() + local Library = script.Parent.Parent + local Roact = require(Library.Parent.Parent.Roact) + + local MockWrapper = require(Library.MockWrapper) + + local MultilineTextEntry = require(script.Parent.MultilineTextEntry) + + local function createTestMultilineTextEntry(visible) + return Roact.createElement(MockWrapper, {}, { + MultilineTextEntry = Roact.createElement(MultilineTextEntry, { + Text = "Text", + Visible = visible, + TextSize = 22, + }), + }) + end + + it("should create and destroy without errors", function() + local element = createTestMultilineTextEntry(true) + local instance = Roact.mount(element) + Roact.unmount(instance) + end) + + it("should render correctly", function () + local container = Instance.new("Folder") + local instance = Roact.mount(createTestMultilineTextEntry(true), container) + local frame = container:FindFirstChildOfClass("Frame") + + expect(frame.ScrollingFrame).to.be.ok() + expect(frame.ScrollingFrame).to.be.ok() + expect(frame.ScrollingFrame.Padding).to.be.ok() + expect(frame.ScrollingFrame.Text).to.be.ok() + + Roact.unmount(instance) + end) + + it("should hide its text when not visible", function () + local container = Instance.new("Folder") + local instance = Roact.mount(createTestMultilineTextEntry(false), container) + local frame = container:FindFirstChildOfClass("Frame") + + expect(frame.ScrollingFrame.Text.Visible).to.equal(false) + + Roact.unmount(instance) + end) +end \ No newline at end of file diff --git a/BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Components/PluginWidget/Dialog.lua b/BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Components/PluginWidget/Dialog.lua new file mode 100644 index 0000000000..9e264c33c7 --- /dev/null +++ b/BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Components/PluginWidget/Dialog.lua @@ -0,0 +1,93 @@ +--[[ + Creates a QWidgetPluginGui dialog. + + Props: + table Options = An options table to pass to the Create function. + + bool Enabled = Whether the dialog is currently enabled. + string Title = The title to display at the top of the dialog. + string Name = The name of the dialog. + ZIndexBehavior ZIndexBehavior = The ordering behavior of elements + in the dialog based on ZIndex. + + function OnClose() = A callback for when the dialog closes. +]] + +local Library = script.Parent.Parent.Parent + +local HttpService = game:GetService("HttpService") + +local Plugin = require(Library.Plugin) +local getPlugin = Plugin.getPlugin +local Roact = require(Library.Parent.Parent.Roact) + +local Focus = require(Library.Focus) +local FocusProvider = Focus.Provider + +local Dialog = Roact.PureComponent:extend("Dialog") + +function Dialog:init(props) + local options = props.Options + local title = props.Title or "" + local name = props.Name or title + local id = title .. HttpService:GenerateGUID() + local zIndexBehavior = props.ZIndexBehavior or Enum.ZIndexBehavior.Sibling + + local plugin = getPlugin(self) + local widget = plugin:CreateQWidgetPluginGui(id, options) + widget.Name = name + widget.ZIndexBehavior = zIndexBehavior + self.widget = widget + + if props.OnClose and widget:IsA("PluginGui") then + widget:BindToClose(function() + props.OnClose() + end) + end +end + +function Dialog:updateWidget() + local props = self.props + local enabled = props.Enabled + local title = props.Title + + local widget = self.widget + if widget then + if enabled ~= nil then + widget.Enabled = enabled + end + + if title ~= nil and widget:IsA("PluginGui") then + widget.Title = title + end + end +end + +function Dialog:didMount() + self:updateWidget() +end + +function Dialog:didUpdate() + self:updateWidget() +end + +function Dialog:render() + return self.widget.Enabled and Roact.createElement(Roact.Portal, { + target = self.widget, + }, { + FocusProvider = Roact.createElement(FocusProvider, { + pluginGui = self.widget, + }, self.props[Roact.Children]), + }) +end + +function Dialog:willUnmount() + if self.changedConnection then + self.changedConnection:Disconnect() + end + if self.widget then + self.widget:Destroy() + end +end + +return Dialog \ No newline at end of file diff --git a/BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Components/Preview/ActionBar.lua b/BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Components/Preview/ActionBar.lua new file mode 100644 index 0000000000..63f1b0995a --- /dev/null +++ b/BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Components/Preview/ActionBar.lua @@ -0,0 +1,192 @@ +--[[ + This component is responsible for managing action bar, which provides two components. + Insert button and open more button. + + Necessary properties: + Position = UDim2 + Size = UDim2 + TryInsert = call back + Text = button text + Color = button color + + Optionlal properties: + LayoutOrder = num + AssetId = id, for analytics + InstallDisabled = true if we're a plugin and we are loading, disables install attempts while loading + DisplayResultOfInsertAttempt = if true, overwrites button color/text once you click it based on result of insert + ShowRobuxIcon = Whether to show a robux icon next to the text. +]] + +local Library = script.Parent.Parent.Parent + +local Roact = require(Library.Parent.Parent.Roact) + +local GetTextSize = require(Library.Utils.GetTextSize) + +local Theming = require(Library.Theming) +local withTheme = Theming.withTheme + +local RoundFrame = require(Library.Components.RoundFrame) + +local ActionBar = Roact.PureComponent:extend("ActionBar") + +local BUTTON_STATUS = { + default = 0, + hovered = 1, +} + + +function ActionBar:init(props) + self.state = { + insertButtonStatus = BUTTON_STATUS.default + } + + self.onInsertButtonEnter = function() + self:setState({ + insertButtonStatus = BUTTON_STATUS.hovered + }) + end + + self.onInsertButtonLeave = function() + self:setState({ + insertButtonStatus = BUTTON_STATUS.default + }) + end + + self.onShowMoreActiveted = function() + self.props.TryCreateContextMenu() + end + + self.onInsertActivated = function() + -- If we're working with a plugin, it might still be loading/already clicked and completed + -- In these cases, we do not want to allow an insert attempt + if self.props.InstallDisabled then + return + end + + self.props.TryInsert() + end +end + +function ActionBar:render() + return withTheme(function(theme) + local props = self.props + local size = props.Size + local position = props.Position + local anchorPoint = props.AnchorPoint + local showRobuxIcon = props.ShowRobuxIcon + local isDisabled = props.InstallDisabled + local layoutOrder = props.LayoutOrder + + local text = props.Text + + local actionBarTheme = theme.assetPreview.actionBar + + local color = actionBarTheme.button.backgroundColor + if isDisabled then + color = actionBarTheme.button.backgroundDisabledColor + elseif self.state.insertButtonStatus == BUTTON_STATUS.hovered then + color = actionBarTheme.button.backgroundHoveredColor + end + + local textColor = isDisabled and actionBarTheme.text.colorDisabled or actionBarTheme.text.color + local textWidth = GetTextSize(text, theme.assetPreview.textSizeLarge, theme.assetPreview.fontBold).X + + local padding = -(actionBarTheme.padding * 2 + actionBarTheme.centerPadding) + + return Roact.createElement("Frame", { + Size = size, + Position = position, + AnchorPoint = anchorPoint, + + BackgroundTransparency = 0, + BackgroundColor3 = actionBarTheme.background, + BorderSizePixel = 0, + + LayoutOrder = layoutOrder, + },{ + UIPadding = Roact.createElement("UIPadding", { + PaddingBottom = UDim.new(0, 12), + PaddingLeft = UDim.new(0, 12), + PaddingRight = UDim.new(0, 12), + PaddingTop = UDim.new(0, 12), + }), + + UIListLayout = Roact.createElement("UIListLayout", { + FillDirection = Enum.FillDirection.Horizontal, + HorizontalAlignment = Enum.HorizontalAlignment.Center, + VerticalAlignment = Enum.VerticalAlignment.Center, + SortOrder = Enum.SortOrder.LayoutOrder, + Padding = UDim.new(0, 10), + }), + + ShowMoreButton = Roact.createElement(RoundFrame, { + Size = UDim2.new(0, 28, 0, 28), + + BackgroundColor3 = actionBarTheme.showMore.backgroundColor, + BackgroundTransparency = 0, + BorderSizePixel = 1, + BorderColor3 = actionBarTheme.showMore.borderColor, + + OnActivated = self.onShowMoreActiveted, + + LayoutOrder = 1, + }, { + ShowMoreImageLabel = Roact.createElement("ImageLabel", { + AnchorPoint = Vector2.new(0.5, 0.5), + Position = UDim2.new(0.5, 0, 0.5, 0), + Size = UDim2.new(0, 16, 0, 16), + + Image = actionBarTheme.images.showMore, + BackgroundTransparency = 1, + }) + }), + + InsertButton = Roact.createElement(RoundFrame, { + Size = UDim2.new(1, padding, 1, 0), + BackgroundColor3 = color, + BackgroundTransparency = 0, + BorderSizePixel = 0, + + OnActivated = self.onInsertActivated, + OnMouseEnter = self.onInsertButtonEnter, + OnMouseLeave = self.onInsertButtonLeave, + + LayoutOrder = 2, + }, { + Layout = Roact.createElement("UIListLayout", { + SortOrder = Enum.SortOrder.LayoutOrder, + FillDirection = Enum.FillDirection.Horizontal, + VerticalAlignment = Enum.VerticalAlignment.Center, + HorizontalAlignment = Enum.HorizontalAlignment.Center, + Padding = UDim.new(0, 2) + }), + + Icon = showRobuxIcon and Roact.createElement("ImageLabel", { + LayoutOrder = 1, + Size = actionBarTheme.robuxSize, + BackgroundTransparency = 1, + Image = actionBarTheme.images.robuxSmall, + ImageColor3 = actionBarTheme.images.colorWhite, + }), + + InsertTextLabel = Roact.createElement("TextLabel", { + LayoutOrder = 2, + Size = UDim2.new(0, textWidth, 1, 0), + + Text = text, + Font = theme.assetPreview.fontBold, + TextSize = theme.assetPreview.textSizeMedium, + TextColor3 = textColor, + TextXAlignment = Enum.TextXAlignment.Center, + TextYAlignment = Enum.TextYAlignment.Center, + + BackgroundTransparency = 1, + BorderSizePixel = 0, + }), + }), + }) + end) +end + +return ActionBar \ No newline at end of file diff --git a/BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Components/Preview/ActionBar.spec.lua b/BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Components/Preview/ActionBar.spec.lua new file mode 100644 index 0000000000..5b94387e48 --- /dev/null +++ b/BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Components/Preview/ActionBar.spec.lua @@ -0,0 +1,26 @@ +return function() + local Library = script.Parent.Parent.Parent + local Roact = require(Library.Parent.Parent.Roact) + + local MockWrapper = require(Library.MockWrapper) + + local ActionBar = require(Library.Components.Preview.ActionBar) + + local function createTestAsset(container, name) + local element = Roact.createElement(MockWrapper, {}, { + Roact.createElement(ActionBar, { + Size = UDim2.new(1, 0, 1, 0), + Position = UDim2.new(0, 0, 0, 0), + AnchorPoint = Vector2.new(0, 1), + Text = "foo", + }) + }) + + return Roact.mount(element, container or nil, name or "") + end + + it("should create and destroy without errors", function() + local instance = createTestAsset() + Roact.unmount(instance) + end) +end \ No newline at end of file diff --git a/BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Components/Preview/AssetDescription.lua b/BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Components/Preview/AssetDescription.lua new file mode 100644 index 0000000000..c5344e5c6e --- /dev/null +++ b/BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Components/Preview/AssetDescription.lua @@ -0,0 +1,98 @@ +--[[ + This component handles the description rows of the assetPreview component. + It's in charge of showing category name in the left and content in the right. + + Required Properties: + Position = UDim2 + LeftContent = string, the name for the category. + RightContent = string, the name of the category. + + Optional Properties: + UseBoldLine = bool, decide if we bold the underlying line or not. + HideSeparator = bool, whether or not to hide the separator after the component + LayoutOrder = num +]] + +local Library = script.Parent.Parent.Parent + +local Roact = require(Library.Parent.Parent.Roact) +local join = require(Library.join) + +local Theming = require(Library.Theming) +local withTheme = Theming.withTheme + +local AssetDescription = Roact.PureComponent:extend("AssetDescription") + +function AssetDescription:render() + return withTheme(function(theme) + local props = self.props + local position = props.Position or UDim2.new(1, 0, 1, 0) + local leftContent = props.LeftContent or "" + local rightContent = props.RightContent or "" + + local useBoldLine = props.UseBoldLine or false + local hideSeparator = props.HideSeparator or false + + local descriptionTheme = theme.assetPreview.description + + local layoutOrder = props.LayoutOrder + + local children = join({ + -- Make sure left side and right side won't be cut off. + UIPadding = Roact.createElement("UIPadding", { + PaddingBottom = UDim.new(0, 0), + PaddingLeft = UDim.new(0, 1), + PaddingRight = UDim.new(0, 1), + PaddingTop = UDim.new(0, 0), + }), + + LeftContent = Roact.createElement("TextLabel", { + Size = UDim2.new(1, 0, 1, 0), + + Text = leftContent, + Font = theme.assetPreview.font, + TextColor3 = descriptionTheme.leftTextColor, + TextSize = theme.assetPreview.textSizeLarge, + TextXAlignment = Enum.TextXAlignment.Left, + + BackgroundTransparency = 1, + + AutoLocalize = false, + }), + + RightContent = Roact.createElement("TextLabel", { + Size = UDim2.new(1, 0, 1, 0), + + Text = rightContent, + Font = theme.assetPreview.font, + TextColor3 = descriptionTheme.rightTextColor, + TextSize = theme.assetPreview.textSizeLarge, + TextXAlignment = Enum.TextXAlignment.Right, + + BackgroundTransparency = 1, + + AutoLocalize = false, + }), + + ButtonLine = not hideSeparator and Roact.createElement("Frame", { + Position = UDim2.new(0, 0, 1, 3), + Size = UDim2.new(1, 0, 0, 1), + + BorderSizePixel = useBoldLine and 1 or 0, + BackgroundColor3 = descriptionTheme.lineColor, + BorderColor3 = descriptionTheme.lineColor, + }) + }, props[Roact.Children] or {}) + + return Roact.createElement("Frame", { + Position = position, + Size = UDim2.new(1, 0, 0, theme.assetPreview.description.height), + + BackgroundTransparency = 1, + BackgroundColor3 = descriptionTheme.background, + LayoutOrder = layoutOrder, + }, children) + end) +end + +return AssetDescription diff --git a/BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Components/Preview/AssetDescription.spec.lua b/BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Components/Preview/AssetDescription.spec.lua new file mode 100644 index 0000000000..2a4153e437 --- /dev/null +++ b/BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Components/Preview/AssetDescription.spec.lua @@ -0,0 +1,28 @@ +return function() + local Library = script.Parent.Parent.Parent + local Roact = require(Library.Parent.Parent.Roact) + + local MockWrapper = require(Library.MockWrapper) + + local AssetDescription = require(Library.Components.Preview.AssetDescription) + + local function createTestAsset(container, name) + local element = Roact.createElement(MockWrapper,{},{ + AssetDescription = Roact.createElement(AssetDescription, { + Position = UDim2.new(1, 0, 1, 0), + Size = UDim2.new(1, 0, 1, 0), + LeftContent = "", + RightContent = "", + + UseBoldLine = false, + }) + }) + + return Roact.mount(element, container or nil, name or "") + end + + it("should create and destroy without errors", function() + local instance = createTestAsset() + Roact.unmount(instance) + end) +end diff --git a/BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Components/Preview/AssetPreview.lua b/BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Components/Preview/AssetPreview.lua new file mode 100644 index 0000000000..69c9fcdbc6 --- /dev/null +++ b/BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Components/Preview/AssetPreview.lua @@ -0,0 +1,512 @@ +--[[ + AssetPreview component is responsible for manageing the models will be displaying on the + ViewPortFrame. + Models, detail information regarding the asset should be coming from the parent. Asnyc reqest for getting + the asset should also be done in the parent. + + Necessary property: + Position = UDim2, the position of Asset Preview with respect to it's parent. + AnchorPoint = Vector2, used to center Asset Preview with respect to it's parent. + MaxPreviewWidth = number, the maximum width allowed for this component. + MaxPreviewHeight = number, the maximum height allowed for this component. + + AssetData = table, a table contains asset data. + CurrentPreview = asset , the current asset displayed in AssetPreview both for 3D view and Tree View. + + ActionBarText = string, the text shown in the large button of the ActionBar. + TryInsert = callback, this determines the behavior of the large button in the ActionBar. + + OnFavoritedActivated = callback, this callback is invoked when the favorites button is clicked for the asset. + FavoriteCounts = number, the number of favorites that this asset has. + Favorited = boolean, whether or not the current user has this asset favorited. + + TryCreateContextMenu = callback, that creates a context menu in the triple dot (...) of the ActionBar. + OnTreeItemClicked = callback, that determines the behavior of when an item is clicked in the TreeView + The TreeView is a part of the PreviewController. + + Optional property: + InstallDisabled = boolean, used in PluginPurchaseFlow to disable the ActionBar install button + when the plugin is already installed. + PurchaseFlow = component, component which is the start of the PluginPurhaseFlow + SuccessDialog = component, success dialog shown at the end of the PluginPurchaseFlow + ShowRobuxIcon = boolean, to determine whether or not the Robux Icon should be shown in the ActionBar. + ShowInstallationBar = boolean, determines if the installation bar should be shown, this is used in PluginPurchaseFlow + LoadingBarText = string, the text that should be displayed with the loading/installation bar. + + HasRating = boolean, determines whether or not Voting and Favorites should be displayed. + Voting = table, table of voting information structed as: + { + UpVotes = number, + DownVotes = number, + } + OnVoteUp = callback, to be invoked when the vote up button is clicked in the Vote component. + OnVoteDown = callback, to be invoked when the vote down button is clicked in the Vote component. + + SearchByCreator = callback, to search for asset in the current Marketplace category + that are created by the same creator as current asset. + + ZIndex = num, used to override the zIndex depth of the base button. +]] + +local RunService = game:GetService("RunService") +local StudioService = game:GetService("StudioService") + +local Library = script.Parent.Parent.Parent + +local Roact = require(Library.Parent.Parent.Roact) + +local Favorites = require(Library.Components.Preview.Favorites) +local PreviewController = require(Library.Components.Preview.PreviewController) +local Vote = require(Library.Components.Preview.Vote) +local ActionBar = require(Library.Components.Preview.ActionBar) +local AssetDescription = require(Library.Components.Preview.AssetDescription) +local LoadingBar = require(Library.Components.LoadingBar) +local SearchLinkText = require(Library.Components.Preview.SearchLinkText) + +local LayoutOrderIterator = require(Library.Utils.LayoutOrderIterator) +local GetTextSize = require(Library.Utils.GetTextSize) + +local Theming = require(Library.Theming) +local withTheme = Theming.withTheme + +local AssetType = require(Library.Utils.AssetType) + +local AssetPreview = Roact.PureComponent:extend("AssetPreview") + +-- TODO: Later, I will need to move all the unchanged numbers +-- into the constants. +local TITLE_HEIGHT = 18 + +local VERTICAL_PADDING = 10 +local TOP_PADDING = 12 +local BOTTOM_PADDING = 20 + +local VOTE_HEIGHT = 36 + +local ACTION_BAR_HEIGHT = 52 +local INSTALLATION_BAR_SECTION_HEIGHT = 80 +local INSTALLATION_BAR_SECTION_PADDING = 16 +local INSTALLATION_BAR_HEIGHT = 6 +local INSTALLATION_ANIMATION_TIME = 1.0 --seconds + + +-- Multiply minimum treeview width by 2 to get minimum threshold +-- When the asset preview is twice the minimum width, then we +-- can split the view in half to show the treeview on the right. +local TREEVIEW_ON_BOTTOM_WIDTH_THRESHOLD = 242 * 2 + +local function getGenreString(genreArray) + local arraySize = #genreArray + if arraySize == 0 then + return "All" + else + return tostring(genreArray[1]) + end +end + +function AssetPreview:init(props) + self.state = { + enableScroller = true, + overrideEnableVoting = false, + } + + self.assetSizeInited = false + + self.baseScrollRef = Roact.createRef() + self.baseLayouterRef = Roact.createRef() + + self.assetBaseButtonRef = Roact.createRef() + + self.onModelPreviewFrameEntered = function() + self:setState({ + enableScroller = false + }) + end + + self.onModelPreviewFrameLeft = function() + self:setState({ + enableScroller = true + }) + end + + -- For first time setting the canvas size. + self.onScrollContentSizeChange = function(rbx) + local baseScroller = self.baseScrollRef.current + local listLayouter = self.baseLayouterRef.current + local absSize = listLayouter and listLayouter.AbsoluteContentSize or Vector2.new() + if baseScroller and listLayouter then + baseScroller.CanvasSize = UDim2.new(1, 0, 0, absSize.Y + BOTTOM_PADDING) + end + + self:adjustAssetHeight() + end + + self.adjustAssetHeight = function() + -- Init the total height of asset preview component + local listLayouter = self.baseLayouterRef.current + local assetBaseButton = self.assetBaseButtonRef.current + if assetBaseButton then + local absSize = listLayouter and listLayouter.AbsoluteContentSize or Vector2.new() + local assetHeight = math.min(absSize.Y + ACTION_BAR_HEIGHT + BOTTOM_PADDING, self.props.MaxPreviewHeight) + assetBaseButton.Size = UDim2.new(0, self.props.MaxPreviewWidth, 0, assetHeight) + end + end + + self.searchByCreator = function() + local assetData = props.AssetData + local creator = assetData.Creator + local creatorName = creator.Name + if self.props.SearchByCreator then + self.props.SearchByCreator(creatorName) + end + end + if self.props.ClearPurchaseFlow then + self.props.ClearPurchaseFlow(props.AssetData.Asset.Id) + end +end + +function AssetPreview:didMount() + --[[ + FIXME (psewell) + THIS IS A HACK! ScrollingFrames can sometimes render the scroll bar in the + wrong place. Because of this, we have to hide the ScrollingFrame for a step + so that the scroll bar appears in the right place when we make it visible. + + This is a temporary fix recommended by PlayerEx. + There is a permanent fix on the way for this bug in C++. + See https://jira.rbx.com/browse/CLIPLAYEREX-2494 + We will enable the flag FFlagStudioRemoveToolboxScrollingFrameHack when the fix is done. + ]] + + local scrollingFrame = self.baseScrollRef.current + local baseButton = self.assetBaseButtonRef.current + if scrollingFrame and baseButton then + local stepConnection + stepConnection = RunService.Heartbeat:Connect(function() + scrollingFrame.Visible = true + stepConnection:Disconnect() + end) + end +end + +function AssetPreview:didUpdate() + self:adjustAssetHeight() +end + +function AssetPreview:render() + return withTheme(function(theme) + -- TODO: Time to tide up the properties passed from the asset. + local props = self.props + + local assetPreviewTheme = theme.assetPreview + + local maxPreviewWidth = props.MaxPreviewWidth + local maxPreviewHeight = props.MaxPreviewHeight + + local position = props.Position + local anchorPoint = props.AnchorPoint + + local assetData = props.AssetData + + -- Data structure from the server + local Asset = assetData.Asset + local assetId = Asset.Id + local assetName = Asset.Name or "Test Name" + local detailDescription = Asset.Description + local created = Asset.Created + local updated = Asset.Updated + local assetGenres = Asset.AssetGenres + + local creator = assetData.Creator + local creatorName = creator.Name + + local typeId = assetData.Asset.TypeId or Enum.AssetType.Model.Value + + local currentPreview = props.CurrentPreview + local previewModel = props.PreviewModel + + local assetPreviewType + if (typeId == Enum.AssetType.Plugin.Value) then + assetPreviewType = AssetType:markAsPlugin() + else + assetPreviewType = AssetType:getAssetType(currentPreview) + end + + local isPluginAsset, isPluginInstalled + isPluginAsset = AssetType:isPlugin(assetPreviewType) + isPluginInstalled = isPluginAsset and StudioService:IsPluginInstalled(assetId) + + local installDisabled = props.InstallDisabled + local showRobuxIcon = props.ShowRobuxIcon or false + local purchaseFlow = props.PurchaseFlow or nil + local successDialog = props.SuccessDialog or nil + local showInstallationBar = props.ShowInstallationBar or false + + local hasRating = props.HasRating or false + + local voting = props.Voting or {} + local upVoteRate = 0 + if voting.UpVotes and voting.DownVotes then + local totalVotes = voting.UpVotes + voting.DownVotes + if totalVotes > 0 then + upVoteRate = voting.UpVotes / totalVotes + end + end + local rating = upVoteRate * 100 + + local putTreeviewOnBottom = maxPreviewWidth <= TREEVIEW_ON_BOTTOM_WIDTH_THRESHOLD + + local assetSize = UDim2.new(0, maxPreviewWidth, 0, maxPreviewHeight) + + local zIndex = props.ZIndex or 0 + + local onTreeItemClicked = props.OnTreeItemClicked + + local tryCreateContextMenu = props.TryCreateContextMenu + + local enableScroller = self.state.enableScroller + + local detailDescriptionWidth = props.MaxPreviewWidth - 4 * assetPreviewTheme.padding - 2 + local textSize = GetTextSize(detailDescription, + assetPreviewTheme.textSizeLarge, + assetPreviewTheme.font, + Vector2.new(detailDescriptionWidth, 9000)) + local detailDescriptionHeight = textSize.y + VERTICAL_PADDING + + local layoutIndex = LayoutOrderIterator.new() + + local closeImageSize = UDim2.new(0, 28, 0, 28) + + return Roact.createElement("ImageButton", { + Position = position, + Size = assetSize, + AnchorPoint = anchorPoint, + + ZIndex = zIndex, + + BackgroundTransparency = 0, + BackgroundColor3 = assetPreviewTheme.background, + AutoButtonColor = false, + BorderSizePixel = 0, + + [Roact.Ref] = self.assetBaseButtonRef, + },{ + CloseImage = Roact.createElement("ImageLabel", { + Position = UDim2.new(1, 0, 0, 0), + Size = closeImageSize, + AnchorPoint = Vector2.new(0, 1), + + Image = assetPreviewTheme.images.deleteButton, + BackgroundTransparency = 1, + }), + + BaseScrollFrame = Roact.createElement("ScrollingFrame", { + Size = UDim2.new(1, assetPreviewTheme.padding, 1, -ACTION_BAR_HEIGHT), + Visible = true, --See comment in didMount + + ScrollBarThickness = 8, + ScrollBarImageColor3 = theme.scrollingFrame.scrollbarImageColor, + BorderSizePixel = 0, + BackgroundTransparency = 1, + TopImage = assetPreviewTheme.images.scrollbarTopImage, + MidImage = assetPreviewTheme.images.scrollbarMiddleImage, + BottomImage = assetPreviewTheme.images.scrollbarBottomImage, + ScrollingEnabled = enableScroller, + + [Roact.Ref] = self.baseScrollRef, + },{ + UIPadding = Roact.createElement("UIPadding", { + PaddingBottom = UDim.new(0, BOTTOM_PADDING), + PaddingLeft = UDim.new(0, assetPreviewTheme.padding), + PaddingRight = UDim.new(0, assetPreviewTheme.padding * 2), + PaddingTop = UDim.new(0, TOP_PADDING), + }), + + UIListLayout = Roact.createElement("UIListLayout", { + FillDirection = Enum.FillDirection.Vertical, + HorizontalAlignment = Enum.HorizontalAlignment.Center, + VerticalAlignment = Enum.VerticalAlignment.Top, + SortOrder = Enum.SortOrder.LayoutOrder, + Padding = UDim.new(0, VERTICAL_PADDING), + + [Roact.Change.AbsoluteContentSize] = self.onScrollContentSizeChange, + [Roact.Ref] = self.baseLayouterRef, + }), + + AssetName = Roact.createElement("TextLabel", { + Size = UDim2.new(1, 0, 0, TITLE_HEIGHT), + + Text = assetName, + Font = assetPreviewTheme.fontBold, + TextSize = assetPreviewTheme.textSizeTitle, + TextXAlignment = Enum.TextXAlignment.Left, + TextColor3 = assetPreviewTheme.assetNameColor, + BackgroundTransparency = 1, + TextTruncate = Enum.TextTruncate.AtEnd, + + AutoLocalize = false, + + LayoutOrder = layoutIndex:getNextOrder(), + }), + + Rating = hasRating and Roact.createElement("Frame", { + BackgroundTransparency = 1, + Size = UDim2.new(1, 0, 0, 12), + + LayoutOrder = layoutIndex:getNextOrder(), + }, { + VoteIcon = Roact.createElement("ImageLabel", { + Size = UDim2.new(0, 16, 0, 16), + BackgroundTransparency = 1, + Image = assetPreviewTheme.images.thumbUpSmall, + }), + + VoteText = Roact.createElement("TextLabel", { + Size = UDim2.new(1, 0, 1, 0), + Position = UDim2.new(0, 22, 0, 3), + BackgroundTransparency = 1, + + Text = ("%d%%"):format(rating), + TextXAlignment = Enum.TextXAlignment.Left, + TextSize = assetPreviewTheme.textSizeMedium, + Font = assetPreviewTheme.Font, + TextColor3 = theme.assetPreview.vote.textColor, + + LayoutOrder = 1, + }), + }), + + PreviewController = Roact.createElement(PreviewController, { + Width = assetPreviewTheme.padding * 2, + + CurrentPreview = currentPreview, + PreviewModel = previewModel, + AssetPreviewType = assetPreviewType, + AssetId = assetId, + PutTreeviewOnBottom = putTreeviewOnBottom, + + OnTreeItemClicked = onTreeItemClicked, + OnModelPreviewFrameEntered = self.onModelPreviewFrameEntered, + OnModelPreviewFrameLeft = self.onModelPreviewFrameLeft, + + LayoutOrder = layoutIndex:getNextOrder(), + }), + + LoadingIndicator = showInstallationBar and Roact.createElement("Frame", { + Size = UDim2.new(1, 0, 0, INSTALLATION_BAR_SECTION_HEIGHT), + BackgroundTransparency = 1, + LayoutOrder = layoutIndex:getNextOrder(), + }, { + UIPadding = Roact.createElement("UIPadding", { + PaddingBottom = UDim.new(0, 0), + PaddingLeft = UDim.new(0, INSTALLATION_BAR_SECTION_PADDING), + PaddingRight = UDim.new(0, INSTALLATION_BAR_SECTION_PADDING), + PaddingTop = UDim.new(0, (INSTALLATION_BAR_SECTION_HEIGHT * 0.5) + 10), + }), + + LoadingBar = Roact.createElement(LoadingBar, { + LoadingText = self.props.LoadingBarText, + Size = UDim2.new(1, 0, 0, INSTALLATION_BAR_HEIGHT), + HoldPercent = 0.92, + LoadingTime = INSTALLATION_ANIMATION_TIME, + InstallationFinished = isPluginInstalled, + }), + }), + + Favorites = Roact.createElement(Favorites, { + Size = UDim2.new(1, 0, 0, 20), + + FavoriteCounts = self.props.FavoriteCounts, + Favorited = self.props.Favorited, + + OnActivated = self.props.OnFavoritedActivated, + + LayoutOrder = layoutIndex:getNextOrder(), + }), + + DetailDescription = Roact.createElement("TextLabel", { + Size = UDim2.new(1, 0, 0, detailDescriptionHeight), + + BackgroundTransparency = 1, + TextWrapped = true, + + Text = detailDescription, + TextSize = assetPreviewTheme.textSizeLarge, + Font = assetPreviewTheme.font, + TextColor3 = assetPreviewTheme.descriptionTextColor, + TextXAlignment = Enum.TextXAlignment.Left, + + LayoutOrder = layoutIndex:getNextOrder(), + }), + + Vote = hasRating and Roact.createElement(Vote, { + Size = UDim2.new(1, 0, 0, VOTE_HEIGHT), + + Voting = voting, + AssetId = assetId, + + OnVoteUpButtonActivated = props.OnVoteUp, + OnVoteDownButtonActivated = props.OnVoteDown, + + LayoutOrder = layoutIndex:getNextOrder(), + }), + + Developer = Roact.createElement(AssetDescription, { + LeftContent = "Creator", + RightContent = "", + + LayoutOrder = layoutIndex:getNextOrder(), + }, { + LinkText = Roact.createElement(SearchLinkText, { + Text = creatorName, + Position = UDim2.fromScale(1, 0.5), + AnchorPoint = Vector2.new(1, 0.5), + OnClick = self.searchByCreator, + }) + }), + + Category = Roact.createElement(AssetDescription, { + LeftContent = "Genre", + RightContent = getGenreString(assetGenres), + + LayoutOrder = layoutIndex:getNextOrder(), + }), + + Created = Roact.createElement(AssetDescription, { + LeftContent = "Created", + RightContent = created, + + LayoutOrder = layoutIndex:getNextOrder(), + }), + + Updated = Roact.createElement(AssetDescription, { + LeftContent = "Last Updated", + RightContent = updated, + HideSeparator = true, + + LayoutOrder = layoutIndex:getNextOrder(), + }) + }), + + ActionBar = Roact.createElement(ActionBar, { + Text = self.props.ActionBarText, + Size = UDim2.new(1, 0, 0, ACTION_BAR_HEIGHT), + Position = UDim2.new(0, 0, 1, 0), + AnchorPoint = Vector2.new(0, 1), + AssetId = assetId, + + Asset = Asset, + TryInsert = self.props.TryInsert, + TryCreateContextMenu = tryCreateContextMenu, + InstallDisabled = installDisabled, + ShowRobuxIcon = showRobuxIcon, + }), + + PurchaseFlow = purchaseFlow, + + SuccessDialog = successDialog, + }) + end) +end + +return AssetPreview \ No newline at end of file diff --git a/BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Components/Preview/AssetPreview.spec.lua b/BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Components/Preview/AssetPreview.spec.lua new file mode 100644 index 0000000000..c684365bf8 --- /dev/null +++ b/BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Components/Preview/AssetPreview.spec.lua @@ -0,0 +1,71 @@ +return function() + local Library = script.Parent.Parent.Parent + local Roact = require(Library.Parent.Parent.Roact) + + local MockWrapper = require(Library.MockWrapper) + + local AssetPreview = require(Library.Components.Preview.AssetPreview) + + local function createTestAsset(container, name) + local testModel = Instance.new("Model") + + local assetData = { + Asset = { + Id = 123456, + Description = "This is a test asset", + Created = "", + Updated = "", + AssetGenres = {}, + }, + + Creator = { + Name = "Roblox Studio", + }, + } + + local element = Roact.createElement(MockWrapper, {}, { + AssetPreview = Roact.createElement(AssetPreview, { + Position = UDim2.new(0.5, 0, 0.5, 0), + AnchorPoint = Vector2.new(0.5, 0.5), + AssetData = assetData, + CurrentPreview = testModel, + + ShowInstallationBar = true, + InstallDisabled = false, + ShowRobuxIcon = true, + LoadingBarText = "Installing", + + FavoriteCounts = 1000, + Favorited = true, + OnFavoritedActivated = function() end, + + OnTreeItemClicked = function() end, + TryCreateContextMenu = function() end, + SearchByCreator = function() end, + + Voting = {}, + OnVoteUp = function() end, + OnVoteDown = function() end, + + ActionBarText = "Insert", + CanInsertAsset = true, + TryInsert = function() end, + + MaxPreviewWidth = 250, + MaxPreviewHeight = 400, + + ZIndex = 0, + + PurchaseFlow = Roact.createElement("Frame"), + SuccessDialog = Roact.createElement("Frame"), + }) + }) + + return Roact.mount(element, container or nil, name or "") + end + + it("should create and destroy without errors", function() + local instance = createTestAsset() + Roact.unmount(instance) + end) +end \ No newline at end of file diff --git a/BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Components/Preview/AudioControl.lua b/BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Components/Preview/AudioControl.lua new file mode 100644 index 0000000000..42617ae0a1 --- /dev/null +++ b/BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Components/Preview/AudioControl.lua @@ -0,0 +1,137 @@ +--[[ + Audio control is a piece of control panel we used in asset preview to provide play, + pause function for giving assetId. And a time label to show time left. + Start time counter when it's playing. + + Necessary properties: + UDim2 position + UDim2 size + number audioControlOffset, used to control the position of the audio control depending if we show tree view. + number timeLength, length got from the sound instance. + bool isPlaying, come from audio preview, used to change the button control. + number timePassed, audio preview know's the time length, is suited to calculate this. + + function onResume, accept an assetId. + function onPause, pause + function onPlay, This one will reset time length and time remaining. + + the sound object inside the Toolbox plugin to play. We don't want to too many sound source. +]] +local FFlagEnableToolboxVideos = game:GetFastFlag("EnableToolboxVideos") + +local Library = script.Parent.Parent.Parent +local Roact = require(Library.Parent.Parent.Roact) + +local Theming = require(Library.Theming) +local withTheme = Theming.withTheme + +local getTimeString = require(Library.Utils.getTimeString) +local RoundButton = require(Library.Components.RoundFrame) + +local LoadingIndicator = require(Library.Components.LoadingIndicator) +local TIME_LABEL_HEIGHT = 15 +local BUTTON_SIZE = 28 + +local AudioControl = Roact.PureComponent:extend("AudioControl") +if FFlagEnableToolboxVideos then + return AudioControl +end +function AudioControl:init(props) + self.state = { + init = false; + } + + self.onActivated = function() + if not self.props.isLoaded then + return + end + if self.props.isPlaying then + self.pauseASound() + else + self.startPlaying() + end + end + + self.startPlaying = function() + if self.state.init then + props.onResume() + else + -- Will update the time length and time remaining. + props.onPlay() + self:setState({ + init = true + }) + end + end + + self.pauseASound = function() + props.onPause() + end +end + +function AudioControl:render() + return withTheme(function(theme) + local props = self.props + local size = props.size + local anchorPoint = props.anchorPoint + local position = props.position + local timeLength = props.timeLength + local audioPreviewTheme = theme.assetPreview.audioPreview + local audioControlOffset = props.audioControlOffset + local isPlaying = props.isPlaying + local isLoaded = props.isLoaded + + local timePassed = props.timePassed + local timeString = getTimeString(timePassed) .. '/' .. getTimeString(timeLength) + + return Roact.createElement("Frame", { + AnchorPoint = anchorPoint, + BackgroundTransparency = 1, + Position = position, + Size = size, + }, { + Button = Roact.createElement(RoundButton, { + AnchorPoint = Vector2.new(0.5, 0), + AutoButtonColor = false, + BackgroundColor3 = isLoaded and audioPreviewTheme.buttonBackgroundColor or audioPreviewTheme.buttonDisabledBackgroundColor, + BackgroundTransparency = isLoaded and 0 or audioPreviewTheme.buttonDisabledBackgroundTransparency, + BorderSizePixel = 0, + Position = UDim2.new(0, 24, 0, 0), + Size = UDim2.new(0, BUTTON_SIZE, 0, BUTTON_SIZE), + + OnActivated = self.onActivated, + }, { + PlayOrPauseIcon = Roact.createElement("ImageLabel", { + AnchorPoint = Vector2.new(0.5, 0.5), + BackgroundTransparency = 1, + Image = isPlaying and audioPreviewTheme.pauseButton or audioPreviewTheme.playButton, + ImageColor3 = audioPreviewTheme.buttonColor, + ImageTransparency = isLoaded and 0 or audioPreviewTheme.buttonDisabledBackgroundTransparency, + Position = UDim2.new(0.5, 0, 0.5, 0), + Size = UDim2.new(1, 0, 1, 0), + }), + }), + + TimeComponent = isLoaded and Roact.createElement("TextLabel", { + AnchorPoint = Vector2.new(1, 0.5), + Position = UDim2.new(1, -audioControlOffset, 0.5, 0), + Size = UDim2.new(0, 204, 0, TIME_LABEL_HEIGHT), + BorderSizePixel = 0, + BackgroundTransparency = 1, + Text = timeString, + Font = audioPreviewTheme.font, + TextSize = audioPreviewTheme.fontSize, + TextXAlignment = Enum.TextXAlignment.Right, + TextColor3 = audioPreviewTheme.textColor, + }), + + LoadingIndicator = (not isLoaded) and Roact.createElement(LoadingIndicator, { + AnchorPoint = Vector2.new(1, 0.5), + Position = UDim2.new(1, -audioControlOffset, 0.5, 0), + Size = UDim2.new(0, 50, 0, TIME_LABEL_HEIGHT), + }), + }) + end) +end + +return AudioControl \ No newline at end of file diff --git a/BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Components/Preview/AudioControl.spec.lua b/BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Components/Preview/AudioControl.spec.lua new file mode 100644 index 0000000000..4d3945b0f9 --- /dev/null +++ b/BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Components/Preview/AudioControl.spec.lua @@ -0,0 +1,41 @@ +return function() + local FFlagEnableToolboxVideos = game:GetFastFlag("EnableToolboxVideos") + if FFlagEnableToolboxVideos then + return + end + + local Library = script.Parent.Parent.Parent + local Roact = require(Library.Parent.Parent.Roact) + + local MockWrapper = require(Library.MockWrapper) + + local AudioControl = require(Library.Components.Preview.AudioControl) + + local function createTestAsset(container, name) + local emptyFunc = function() + end + + local element = Roact.createElement(MockWrapper, {}, { + Roact.createElement(AudioControl, { + position = UDim2.new(0, 0, 0, 0), + size = UDim2.new(1, 0, 1, 0), + audioControlOffset = 30, + assetId = 0, + timeLength = 1, + isPlaying = false, + isLoaded = true, + timePassed = 0, + onResume = emptyFunc, + onPause = emptyFunc, + onPlay = emptyFunc, + }) + }) + + return Roact.mount(element, container or nil, name or "") + end + + it("should create and destroy without errors", function() + local instance = createTestAsset() + Roact.unmount(instance) + end) +end \ No newline at end of file diff --git a/BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Components/Preview/AudioPreview.lua b/BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Components/Preview/AudioPreview.lua new file mode 100644 index 0000000000..8eb1c5239b --- /dev/null +++ b/BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Components/Preview/AudioPreview.lua @@ -0,0 +1,356 @@ +--[[ + This component will be used in the asset preview for audio asset. + It will provide a still image for sound asset and a progress bar. + The progress bar will keep moving if the sound is playing. + + Necessary properties: + number SoundId, used for play and pause the sound + bool ShowTreeView, used to adjust time label component for audio control based on if we are + showing tree view button or not. + + Optional properties: + UDim2 position, default to UDim2(0, 0, 0, 0) + UDim2 size, default to UDim2(1, 0, 1, 0) + number layoutOrder, used by the layouter to change the position of the component + callBack ReportPlay, analytics events. + callback ReportPause, + + Props automatically received from wrapMedia(): + number _CurrentTime: The time in seconds that the media's TimePosition should currently be. + boolean _IsPlaying: Whether or not the Sound or VideoFrame should be currently playing. + callback _MediaPlayingUpdateSignal: Called when the media's Changed event is fired. Sets the isPlaying state. + callback _OnMediaEnded: Called when the media's Ended event is fired. Resets the currentTime & stops playing. + callback _Pause: Called when clicking the pause button. + callback _Play: Called when clicking the play button. + callBack _SetCurrentTime: Called if the currentTime has been changed, such as when moving a progressbar slider. +]] +local FFlagHideOneChildTreeviewButton = game:GetFastFlag("HideOneChildTreeviewButton") +local FFlagEnableToolboxVideos = game:GetFastFlag("EnableToolboxVideos") + +local RunService = game:GetService("RunService") +local wrapMedia = require(script.Parent.wrapMedia) + +local Library = script.Parent.Parent.Parent +local Roact = require(Library.Parent.Parent.Roact) + +local Theming = require(Library.Theming) +local withTheme = Theming.withTheme + +local PluginContext = require(Library.Plugin) +local getPlugin = PluginContext.getPlugin + +local PROGRESS_BAR_HEIGHT = 6 +local AUDIO_CONTROL_HEIGHT = 35 +local AUDIO_CONTROL_WIDTH_OFFSET_WITH_TREE = 50 +local AUDIO_CONTROL_WIDTH_OFFSET_NO_TREE = 70 + +if FFlagHideOneChildTreeviewButton then + AUDIO_CONTROL_WIDTH_OFFSET_NO_TREE = 10 +end + +local AudioControl = FFlagEnableToolboxVideos and nil or require(Library.Components.Preview.AudioControl) +local MediaControl = require(Library.Components.Preview.MediaControl) + +local AudioPreview = Roact.PureComponent:extend("AudioPreview") + +AudioPreview.defaultProps = { + size = UDim2.new(1, 0, 1, 0), +} + +function AudioPreview:init(props) + local plugin = getPlugin(self) + self.soundRef = Roact.createRef() + + self.state = { + timeLength = 0, + isPlaying = FFlagEnableToolboxVideos and nil or false, + isLoaded = false, + currentTime = FFlagEnableToolboxVideos and nil or 0, + } + + self.playSound = function(assetId) + if FFlagEnableToolboxVideos then + return + end + local soundObj = self.soundRef.current + if soundObj then + soundObj.SoundId = self.props.SoundId + plugin:PlaySound(soundObj) + end + self:setState({ + isPlaying = true, + currentTime = 0, + }) + + if self.props.reportPlay then + self.props.ReportPlay() + end + end + + self.resumeSound = function(assetId) + if FFlagEnableToolboxVideos then + return + end + local soundObj = self.soundRef.current + if soundObj then + soundObj.SoundId = self.props.SoundId + plugin:ResumeSound(soundObj) + end + + self:setState({ + isPlaying = true, + timeLength = soundObj.TimeLength, + }) + + if self.props.reportPlay then + self.props.ReportPlay() + end + end + + self.pauseSound = function(assetId) + if FFlagEnableToolboxVideos then + return + end + local soundObj = self.soundRef.current + if soundObj then + plugin:PauseSound(soundObj) + end + + self:setState({ + isPlaying = false, + }) + + if self.props.ReportPause then + self.props.ReportPause() + end + end + + self.onSoundEnded = function(soundId) + if FFlagEnableToolboxVideos then + return + end + self:setState({ + isPlaying = false, + timeLength = 0, + currentTime = 0, + }) + end + + self.dispatchMediaPlayingUpdate = function(updateType) + local soundObj = self.soundRef.current + if not soundObj or not self.isMounted then + return + end + if updateType == "PLAY" then + soundObj.SoundId = self.props.SoundId + plugin:ResumeSound(soundObj) + if self.props.reportPlay then + self.props.ReportPlay() + end + elseif updateType == "PAUSE" then + plugin:PauseSound(soundObj) + if self.props.ReportPause then + self.props.ReportPause() + end + end + end + + self.onSoundChange = function(rbx, property) + local soundObj = self.soundRef.current + if not self.isMounted then + return + end + local isLoaded = soundObj and soundObj.IsLoaded + if property == "TimeLength" then + self:setState({ + isLoaded = isLoaded, + timeLength = soundObj.TimeLength, + }) + if FFlagEnableToolboxVideos then + self.props._SetTimeLength(soundObj.TimeLength) + end + elseif isLoaded ~= self.state.isLoaded then + self:setState({ + isLoaded = isLoaded, + }) + end + end + + self.getAudioLength = function() + local soundObj = self.soundRef.current + if soundObj then + return math.max(soundObj.TimeLength, 1) + end + end +end + +function AudioPreview:didMount() + self.isMounted = true + if FFlagEnableToolboxVideos then + self.mediaPlayingUpdateConnection = self.props._MediaPlayingUpdateSignal:connect(self.dispatchMediaPlayingUpdate) + else + local soundObj = self.soundRef.current + if soundObj then + soundObj.SoundId = self.props.SoundId + end + + self.runServiceConnection = RunService.RenderStepped:Connect(function(step) + if (not self.state.isPlaying) then + return + end + local state = self.state + local newTime = self.state.currentTime + step + + if newTime >= state.timeLength then + newTime = state.timeLength + end + + if self.isMounted then + self:setState({ + currentTime = newTime + }) + end + end) + end +end + +function AudioPreview:willUnmount() + self.isMounted = false + if FFlagEnableToolboxVideos then + if self.mediaPlayingUpdateConnection then + self.mediaPlayingUpdateConnection:disconnect() + self.mediaPlayingUpdateConnection = nil + end + else + if self.runServiceConnection then + self.runServiceConnection:Disconnect() + end + end +end + +function AudioPreview:render() + return withTheme(function(theme) + local props = self.props + local state = self.state + local position = props.position + local size = props.size + local audioPreviewTheme = theme.assetPreview.audioPreview + + local layoutOrder = props.layoutOrder + local soundId = props.SoundId + + local currentTime = FFlagEnableToolboxVideos and props._CurrentTime or state.currentTime + local pause = props._Pause + local play = props._Play + local onMediaEnded = props._OnMediaEnded + + local progress + if state.timeLength ~= nil and state.timeLength ~= 0 then + progress = currentTime / state.timeLength + else + progress = 0 + end + + local showTreeView = props.ShowTreeView + local audioControlOffset = showTreeView and AUDIO_CONTROL_WIDTH_OFFSET_WITH_TREE or AUDIO_CONTROL_WIDTH_OFFSET_NO_TREE + local timeLength = self.getAudioLength() or 0 + local isLoaded = state.isLoaded + local isPlaying = FFlagEnableToolboxVideos and props._IsPlaying or state.isPlaying + + return Roact.createElement("Frame", { + BackgroundTransparency = 1, + BackgroundColor3 = audioPreviewTheme.backgroundColor, + LayoutOrder = layoutOrder, + Position = position, + Size = size, + },{ + UIListLayout = Roact.createElement("UIListLayout", { + FillDirection = Enum.FillDirection.Vertical, + HorizontalAlignment = Enum.HorizontalAlignment.Left, + VerticalAlignment = Enum.VerticalAlignment.Center, + SortOrder = Enum.SortOrder.LayoutOrder, + Padding = UDim.new(0, 10), + }), + + AudioPlayerFrame = Roact.createElement("Frame", { + Size = UDim2.new(1, 0, 1, -PROGRESS_BAR_HEIGHT- AUDIO_CONTROL_HEIGHT), + BackgroundTransparency = 1, + LayoutOrder = 1, + }, { + AudioPlayerImage = Roact.createElement("ImageLabel", { + Size = UDim2.new(1, 0, 1, 0), + Image = audioPreviewTheme.audioPlay_BG, + ScaleType = Enum.ScaleType.Fit, + BackgroundTransparency = 1, + BorderSizePixel = 0, + ImageColor3 = audioPreviewTheme.audioPlay_BG_Color, + }) + }), + + ProgressBarFrame = Roact.createElement("Frame", { + AnchorPoint = Vector2.new(0, 0.5), + Position = UDim2.new(0, 0, 0.5, 0), + Size = UDim2.new(1, 0, 0, PROGRESS_BAR_HEIGHT), + + BackgroundColor3 = audioPreviewTheme.progressBar_BG_Color, + BorderSizePixel = 0, + BackgroundTransparency = 0, + + LayoutOrder = 2, + }, { + ProgressBar = Roact.createElement("Frame", { + AnchorPoint = Vector2.new(0, 0.5), + BackgroundColor3 = audioPreviewTheme.progressBar, + BackgroundTransparency = 0, + BorderSizePixel = 0, + Position = UDim2.new(0, 0, 0.5, 0), + Size = UDim2.new(progress, 0, 0, PROGRESS_BAR_HEIGHT), + }) + }), + + MediaControl = FFlagEnableToolboxVideos and Roact.createElement(MediaControl, { + LayoutOrder = 3, + IsPlaying = isPlaying, + IsLoaded = isLoaded, + OnPause = pause, + OnPlay = play, + ShowTreeView = showTreeView, + TimeLength = timeLength, + TimePassed = currentTime, + }), + + AudioControlBase = (not FFlagEnableToolboxVideos) and Roact.createElement("Frame", { + Size = UDim2.new(1, 0, 0, AUDIO_CONTROL_HEIGHT), + BackgroundTransparency = 1, + LayoutOrder = 3, + }, { + AudioControl = Roact.createElement(AudioControl, { + position = UDim2.new(0, 0, 0, 0), + size = UDim2.new(1, 0, 0, AUDIO_CONTROL_HEIGHT), + audioControlOffset = audioControlOffset, + timeLength = timeLength, + isPlaying = isPlaying, + isLoaded = isLoaded, + timePassed = state.currentTime, + onResume = self.resumeSound, + onPause = self.pauseSound, + onPlay = self.playSound, + }), + }), + + SoundObj = Roact.createElement("Sound", { + Looped = false, + SoundId = FFlagEnableToolboxVideos and soundId or nil, + [Roact.Ref] = self.soundRef, + [Roact.Event.Changed] = self.onSoundChange, + [Roact.Event.Ended] = FFlagEnableToolboxVideos and onMediaEnded or self.onSoundEnded, + }) + }) + end) +end + +if FFlagEnableToolboxVideos then + return wrapMedia(AudioPreview) +else + return AudioPreview +end \ No newline at end of file diff --git a/BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Components/Preview/AudioPreview.spec.lua b/BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Components/Preview/AudioPreview.spec.lua new file mode 100644 index 0000000000..fc0653e7ec --- /dev/null +++ b/BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Components/Preview/AudioPreview.spec.lua @@ -0,0 +1,25 @@ +return function() + local Library = script.Parent.Parent.Parent + local Roact = require(Library.Parent.Parent.Roact) + + local MockWrapper = require(Library.MockWrapper) + + local AudioPreview = require(Library.Components.Preview.AudioPreview) + + local function createTestAsset(container, name) + local element = Roact.createElement(MockWrapper, {}, { + Roact.createElement(AudioPreview, { + SoundId = 123, + ReportPlay = function() end, + ReportPause = function() end, + }) + }) + + return Roact.mount(element, container or nil, name or "") + end + + it("should create and destroy without errors", function() + local instance = createTestAsset() + Roact.unmount(instance) + end) +end \ No newline at end of file diff --git a/BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Components/Preview/Favorites.lua b/BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Components/Preview/Favorites.lua new file mode 100644 index 0000000000..357dab03cb --- /dev/null +++ b/BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Components/Preview/Favorites.lua @@ -0,0 +1,122 @@ +--[[ + This component is designed to show the favorites counts for the assetPreview. + It will send request to fetch the data when loaded. And update accordingly. + + Necessary Properties: + Size = UDim2, + + FavoriteCounts = number, the number of favorites this asset has. + Favorited = bool, does the current user have this asset favorited. + OnActivated = callback, function to invoke when the favorited button is clicked. + + LayoutOrder = number, +]] + +local Library = script.Parent.Parent.Parent + +local Roact = require(Library.Parent.Parent.Roact) + +local Theming = require(Library.Theming) +local withTheme = Theming.withTheme + +local Favorites = Roact.PureComponent:extend("Favorites") + +-- For less then 10k, we use , to seperate the number. +-- For larger than 10k, we use xxk+ +local function getFavoritesCountString(counts) + local countsString + if counts > 10000 then + countsString = ("%dk+"):format(math.floor(counts / 1000)) + else + if counts > 1000 then + countsString = ("%d,%d"):format(counts / 1000, counts % 1000) + else + countsString = tostring(counts) + end + end + + return countsString +end + +function Favorites:init(props) + self.state = { + hovered = false + } + + self.onMouseEnter = function(rbx, x, y) + self:setState({ + hovered = true + }) + end + + self.onMouseLeave = function(rbx, x, y) + self:setState({ + hovered = false + }) + end +end + +function Favorites:render() + return withTheme(function(theme) + local props = self.props + local state = self.state + + local favoriteCounts = props.FavoriteCounts or 0 + + local favoritesTheme = theme.assetPreview.favorites + local favoritesImage = (state.hovered or props.Favorited) and favoritesTheme.favorited or favoritesTheme.unfavorited + local textContent = getFavoritesCountString(tonumber(favoriteCounts)) + local contentColor = favoritesTheme.contentColor + local size = props.Size + + local layoutOrder = props.LayoutOrder + local verticalAlignment = props.VerticalAlignment or Enum.VerticalAlignment.Center + + return Roact.createElement("Frame", { + Size = size, + + BackgroundTransparency = 1, + LayoutOrder = layoutOrder, + }, { + UIListLayout = Roact.createElement("UIListLayout", { + FillDirection = Enum.FillDirection.Horizontal, + SortOrder = Enum.SortOrder.LayoutOrder, + VerticalAlignment = verticalAlignment, + Padding = UDim.new(0, 4), + }), + + ImageContent = Roact.createElement("ImageButton", { + Size = UDim2.new(0, 20, 0, 20), + + BackgroundTransparency = 1, + + Image = favoritesImage, + + [Roact.Event.MouseEnter] = self.onMouseEnter, + [Roact.Event.MouseLeave] = self.onMouseLeave, + [Roact.Event.Activated] = self.props.OnActivated, + + LayoutOrder = 1, + }), + + TextContent = Roact.createElement("TextLabel", { + Size = UDim2.new(1, -20, 1, 0), + + Text = tostring(textContent), + TextColor3 = contentColor, + Font = theme.assetPreview.font, + TextSize = theme.assetPreview.textSizeMedium, + TextXAlignment = Enum.TextXAlignment.Left, + TextYAlignment = Enum.TextYAlignment.Center, + + BackgroundTransparency = 1, + + LayoutOrder = 2, + }) + }) + end) +end + +return Favorites + + diff --git a/BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Components/Preview/Favorites.spec.lua b/BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Components/Preview/Favorites.spec.lua new file mode 100644 index 0000000000..d0b32e3a35 --- /dev/null +++ b/BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Components/Preview/Favorites.spec.lua @@ -0,0 +1,78 @@ +return function() + local Library = script.Parent.Parent.Parent + local Roact = require(Library.Parent.Parent.Roact) + + local MockWrapper = require(Library.MockWrapper) + + local Favorites = require(Library.Components.Preview.Favorites) + + local IMAGE_FAVORITED = "rbxasset://textures/StudioToolbox/AssetPreview/star_filled.png" + local IMAGE_UNFAVORITED = "rbxasset://textures/StudioToolbox/AssetPreview/star_stroke.png" + + local function createTestFavorites(container, name, props) + local testFavoriteActivationValue = false + local favorited = true + if props then + favorited = props.Favorited + end + + local element = Roact.createElement(MockWrapper, {}, { + Favorites = Roact.createElement(Favorites, { + Size = UDim2.new(1,0,1,0), + + FavoriteCounts = 1000, + Favorited = favorited, + OnActivated = function() + testFavoriteActivationValue = not testFavoriteActivationValue + end, + + LayoutOrder = 1, + }) + }) + + return Roact.mount(element, container or nil, name or "") + end + + it("should create and destroy without errors", function() + local instance = createTestFavorites() + Roact.unmount(instance) + end) + + it("should render correctly", function() + local container = Instance.new("Folder") + local instance = createTestFavorites(container) + + local element = container:FindFirstChildOfClass("Frame") + expect(element.TextContent).to.be.ok() + expect(element.ImageContent).to.be.ok() + end) + + it("should properly set the initial favorite counts", function() + local container = Instance.new("Folder") + local instance = createTestFavorites(container) + + local element = container:FindFirstChildOfClass("Frame") + + expect(element.TextContent.Text).to.be.equal("1000") + end) + + it("should display the correct icon for a favorited asset", function() + local container = Instance.new("Folder") + local instance = createTestFavorites(container, nil, { + Favorited = true + }) + + local element = container:FindFirstChildOfClass("Frame") + expect(element.ImageContent.Image).to.be.equal(IMAGE_FAVORITED) + end) + + it("should display the correct icon for an unfavorited asset", function() + local container = Instance.new("Folder") + local instance = createTestFavorites(container, nil, { + Favorited = false + }) + + local element = container:FindFirstChildOfClass("Frame") + expect(element.ImageContent.Image).to.be.equal(IMAGE_UNFAVORITED) + end) +end \ No newline at end of file diff --git a/BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Components/Preview/ImagePreview.lua b/BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Components/Preview/ImagePreview.lua new file mode 100644 index 0000000000..ad75bcd475 --- /dev/null +++ b/BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Components/Preview/ImagePreview.lua @@ -0,0 +1,57 @@ +--[[ + This component is used to display a single image in an AssetPreview. + + Necessary properties: + Position = UDim2 + Size = UDim2 + ImageContent = String, url/rbxassetid of the image object to shown, + e.g. http://www.roblox.com/asset/?id= + rbxassetid:// + ScaleType = Enum.ScaleType.*, scaling type to use +]] + +local Library = script.Parent.Parent.Parent + +local Roact = require(Library.Parent.Parent.Roact) + +local Theming = require(Library.Theming) +local withTheme = Theming.withTheme + +local ImagePreview = Roact.PureComponent:extend("ImagePreview") + +function ImagePreview:render() + return withTheme(function(theme) + local props = self.props + local imageContent = props.ImageContent + + local position = props.Position + local size = props.Size or UDim2.new(1, 0, 1, 0) + + local imagePreviewTheme = theme.assetPreview.imagePreview + local scaleType = props.ScaleType or Enum.ScaleType.Fit + + local layoutOrder = props.LayoutOrder + + return Roact.createElement("Frame", { + Position = position, + Size = size, + + BackgroundTransparency = 0, + BackgroundColor3 = imagePreviewTheme.background, + BorderSizePixel = 0, + + LayoutOrder = layoutOrder, + },{ + ImageContent = Roact.createElement("ImageLabel", { + Size = UDim2.new(1, 0, 1, 0), + ScaleType = scaleType, + + BackgroundTransparency = 1, + + Image = imageContent, + }), + }) + end) +end + +return ImagePreview \ No newline at end of file diff --git a/BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Components/Preview/ImagePreview.spec.lua b/BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Components/Preview/ImagePreview.spec.lua new file mode 100644 index 0000000000..e3555e89b5 --- /dev/null +++ b/BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Components/Preview/ImagePreview.spec.lua @@ -0,0 +1,27 @@ +return function() + local Library = script.Parent.Parent.Parent + local Roact = require(Library.Parent.Parent.Roact) + + local MockWrapper = require(Library.MockWrapper) + + local ImagePreview = require(Library.Components.Preview.ImagePreview) + + local function createTestAsset(container, name) + local image = "rbxasset://textures/AnimationEditor/animation_editor_blue.png" + local element = Roact.createElement(MockWrapper, {}, { + Roact.createElement(ImagePreview, { + ImageContent = image, + TextContent = "ImagePreviewTest", + Position = UDim2.new(1, 0, 1, 0), + Size = UDim2.new(0, 100, 0, 20) + }) + }) + + return Roact.mount(element, container or nil, name or "") + end + + it("should create and destroy without errors", function() + local instance = createTestAsset() + Roact.unmount(instance) + end) +end \ No newline at end of file diff --git a/BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Components/Preview/InstanceTreeViewItem.lua b/BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Components/Preview/InstanceTreeViewItem.lua new file mode 100644 index 0000000000..d12b79dc1f --- /dev/null +++ b/BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Components/Preview/InstanceTreeViewItem.lua @@ -0,0 +1,161 @@ +--[[ + A single item displayed in a TreeView component. + + Required Props: + element = instance, The instance to display. + indent = number, The level of indentation this item appears at. + canExpand = boolean, Whether this item has children and can be expanded. + isExpanded = boolean, Whether this item is showing its children. + isSelected = boolean, Whether this item is the selected item. + rowIndex = number, The order in which this item appears in the list. + toggleSelected = callback, A callback when this item is clicked. +]] +local FFlagAssetManagerLuaCleanup1 = settings():GetFFlag("AssetManagerLuaCleanup1") + +local Library = script.Parent.Parent.Parent + +local Roact = require(Library.Parent.Parent.Roact) + +local GetClassIcon = require(Library.Utils.GetClassIcon) +local TooltipWrapper = require(Library.Components.Tooltip) + +local Theming = require(Library.Theming) +local withTheme = Theming.withTheme + +local ELEMENT_PADDING = 5 + +local TreeViewItem = Roact.PureComponent:extend("TreeViewItem") + +function TreeViewItem:init() + self.state = { + Hovering = false, + } + + self.mouseEnter = function() + self:setState({ + Hovering = true, + }) + end + + self.mouseLeave = function() + self:setState({ + Hovering = false, + }) + end + + self.onClick = function() + self.props.toggleSelected() + end +end + +function TreeViewItem:render(props) + return withTheme(function(theme) + local treeViewTheme = theme.instanceTreeView + local instance = self.props.element + local name = instance.Name + local iconInfo = GetClassIcon(instance) + if FFlagAssetManagerLuaCleanup1 then + if typeof(instance) == "table" and instance.Icon then + iconInfo = instance.Icon + end + end + + local indent = self.props.indent + local expandable = self.props.canExpand + local expanded = self.props.isExpanded + local selected = self.props.isSelected + local layoutOrder = self.props.rowIndex or 1 + local height = treeViewTheme.treeItemHeight + local hover = self.state.Hovering + + local selectionOffset = height + + local labelOffset = selectionOffset + ELEMENT_PADDING + + (iconInfo and (height + treeViewTheme.treeViewIndent) or 0) + + return Roact.createElement("Frame", { + Size = UDim2.new(1, 0, 0, height), + BackgroundTransparency = 1, + LayoutOrder = layoutOrder, + }, { + Padding = Roact.createElement("UIPadding", { + PaddingLeft = UDim.new(0, indent * treeViewTheme.treeViewIndent), + }), + + Layout = Roact.createElement("UIListLayout", { + SortOrder = Enum.SortOrder.LayoutOrder, + Padding = UDim.new(0, ELEMENT_PADDING), + FillDirection = Enum.FillDirection.Horizontal, + }), + + Expand = Roact.createElement("ImageButton", { + LayoutOrder = 0, + Size = UDim2.new(0, height, 0, height), + BackgroundTransparency = 1, + ImageTransparency = 1, + + [Roact.Event.Activated] = self.props.toggleExpanded, + }, { + ExpandIcon = expandable and Roact.createElement("ImageLabel", { + BackgroundTransparency = 1, + ScaleType = Enum.ScaleType.Fit, + Size = UDim2.new(0, 9, 0, 9), + AnchorPoint = Vector2.new(0.5, 0.5), + Position = UDim2.new(0.5, 0, 0.5, 1), + ImageTransparency = expandable and 0 or 1, + Image = expanded and treeViewTheme.arrowExpanded or treeViewTheme.arrowCollapsed, + ImageColor3 = treeViewTheme.arrowColor, + }), + }), + + Icon = iconInfo and Roact.createElement("ImageLabel", { + ZIndex = 2, + LayoutOrder = 1, + Size = UDim2.new(0, height, 0, height), + BackgroundTransparency = 1, + Image = iconInfo.Image, + ImageRectSize = iconInfo.ImageRectSize, + ImageRectOffset = iconInfo.ImageRectOffset, + }), + + Name = Roact.createElement("TextLabel", { + ZIndex = 2, + LayoutOrder = 2, + BackgroundTransparency = 1, + Size = UDim2.new(1, -labelOffset, 0, height), + Font = treeViewTheme.font, + TextXAlignment = Enum.TextXAlignment.Left, + TextSize = treeViewTheme.textSize, + TextTruncate = Enum.TextTruncate.AtEnd, + Text = name, + TextColor3 = selected and treeViewTheme.selectedText or treeViewTheme.textColor, + BorderSizePixel = 0, + }, { + Tooltip = Roact.createElement(TooltipWrapper, { + Text = name, + Enabled = hover, + ShowDelay = treeViewTheme.tooltipShowDelay, + }), + }), + + -- We have to create a Folder so that the hover is not affected by the UIListLayout. + HoverFolder = Roact.createElement("Folder", {}, { + Hover = Roact.createElement("ImageButton", { + Size = UDim2.new(1, -selectionOffset, 1, 4), + Position = UDim2.new(0, selectionOffset, 0.5, 0), + AnchorPoint = Vector2.new(0, 0.5), + BackgroundTransparency = (hover or selected) and 0 or 1, + BackgroundColor3 = selected and treeViewTheme.selected or treeViewTheme.hover, + BorderSizePixel = 0, + AutoButtonColor = false, + + [Roact.Event.MouseEnter] = self.mouseEnter, + [Roact.Event.MouseLeave] = self.mouseLeave, + [Roact.Event.Activated] = self.onClick, + }), + }), + }) + end) +end + +return TreeViewItem \ No newline at end of file diff --git a/BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Components/Preview/InstanceTreeViewItem.spec.lua b/BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Components/Preview/InstanceTreeViewItem.spec.lua new file mode 100644 index 0000000000..e835de7900 --- /dev/null +++ b/BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Components/Preview/InstanceTreeViewItem.spec.lua @@ -0,0 +1,44 @@ +return function() + local Library = script.Parent.Parent.Parent + local Roact = require(Library.Parent.Parent.Roact) + + local MockWrapper = require(Library.MockWrapper) + + local TreeViewItem = require(Library.Components.Preview.InstanceTreeViewItem) + + it("should create and destroy without errors", function() + local element = Roact.createElement(MockWrapper, {}, { + TreeViewItem = Roact.createElement(TreeViewItem, { + element = Instance.new("Part"), + indent = 0, + }), + }) + local instance = Roact.mount(element) + Roact.unmount(instance) + end) + + it("should render correctly", function() + local element = Roact.createElement(MockWrapper, {}, { + TreeViewItem = Roact.createElement(TreeViewItem, { + element = Instance.new("Part"), + indent = 0, + canExpand = true, + }), + }) + local container = Instance.new("Folder") + local instance = Roact.mount(element, container, "TreeViewItem") + + local treeViewItem = container.TreeViewItem + expect(treeViewItem).to.be.ok() + expect(treeViewItem.Padding).to.be.ok() + expect(treeViewItem.Layout).to.be.ok() + expect(treeViewItem.Expand).to.be.ok() + expect(treeViewItem.Expand.ExpandIcon).to.be.ok() + expect(treeViewItem.Icon).to.be.ok() + expect(treeViewItem.Name).to.be.ok() + expect(treeViewItem.HoverFolder).to.be.ok() + expect(treeViewItem.HoverFolder.Hover).to.be.ok() + + Roact.unmount(instance) + end) +end diff --git a/BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Components/Preview/MediaControl.lua b/BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Components/Preview/MediaControl.lua new file mode 100644 index 0000000000..4482705399 --- /dev/null +++ b/BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Components/Preview/MediaControl.lua @@ -0,0 +1,128 @@ +--[[ + The control panel for Sounds and VideoFrames that provides a + play/pause button, progress bar, and a time label to show time left. + + Required Props: + boolean IsPlaying: Whether or not the Sound or VideoFrame is currently playing. + boolean IsLoaded: Whether or not the Sound or VideoFrame is loaded. + callback OnPause: Called when clicking the pause button. + callback OnPlay: Called when first clicking the play button. + boolean ShowTreeView: used to control the position of the play/pause button. + number TimeLength: The total Sound/VideoFrame length. + number TimePassed: How much time has passed since playing the Sound/VideoFrame. + + Optional Props: + Vector2 AnchorPoint: The AnchorPoint of the component + UDim2 LayoutOrder: The LayoutOrder of the component + UDim2 Position: The Position of the component +]] +local FFlagHideOneChildTreeviewButton = game:GetFastFlag("HideOneChildTreeviewButton") + +local Library = script.Parent.Parent.Parent +local Roact = require(Library.Parent.Parent.Roact) + +local Theming = require(Library.Theming) +local withTheme = Theming.withTheme + +local getTimeString = require(Library.Utils.getTimeString) +local RoundButton = require(Library.Components.RoundFrame) + +local LoadingIndicator = require(Library.Components.LoadingIndicator) + +local TIME_LABEL_HEIGHT = 15 +local BUTTON_SIZE = 28 +local AUDIO_CONTROL_HEIGHT = 35 +local AUDIO_CONTROL_WIDTH_OFFSET_WITH_TREE = 50 +local AUDIO_CONTROL_WIDTH_OFFSET_NO_TREE = 70 + +if FFlagHideOneChildTreeviewButton then + AUDIO_CONTROL_WIDTH_OFFSET_NO_TREE = 10 +end + +local MediaControl = Roact.PureComponent:extend("MediaControl") + +MediaControl.defaultProps = { + TimePassed = 0, +} + +function MediaControl:init() + self.onActivated = function() + if not self.props.IsLoaded then + return + end + + if self.props.IsPlaying then + self.props.OnPause() + else + self.props.OnPlay() + end + end +end + +function MediaControl:render() + return withTheme(function(theme) + local audioPreviewTheme = theme.assetPreview.audioPreview + local props = self.props + + local anchorPoint = props.AnchorPoint + local layoutOrder = props.LayoutOrder + local isLoaded = props.IsLoaded + local isPlaying = props.IsPlaying + local position = props.Position + local showTreeView = props.ShowTreeView + local timeLength = props.TimeLength + local timePassed = props.TimePassed + + local controlOffset = showTreeView and AUDIO_CONTROL_WIDTH_OFFSET_WITH_TREE or AUDIO_CONTROL_WIDTH_OFFSET_NO_TREE + local timeString = getTimeString(timePassed) .. '/' .. getTimeString(timeLength) + + return Roact.createElement("Frame", { + AnchorPoint = anchorPoint, + BackgroundTransparency = 1, + LayoutOrder = layoutOrder, + Position = position, + Size = UDim2.new(1, 0, 0, AUDIO_CONTROL_HEIGHT), + }, { + Button = Roact.createElement(RoundButton, { + AnchorPoint = Vector2.new(0.5, 0), + AutoButtonColor = false, + BackgroundColor3 = isLoaded and audioPreviewTheme.buttonBackgroundColor or audioPreviewTheme.buttonDisabledBackgroundColor, + BackgroundTransparency = isLoaded and 0 or audioPreviewTheme.buttonDisabledBackgroundTransparency, + BorderSizePixel = 0, + OnActivated = self.onActivated, + Position = UDim2.new(0, 24, 0, 0), + Size = UDim2.new(0, BUTTON_SIZE, 0, BUTTON_SIZE), + }, { + PlayOrPauseIcon = Roact.createElement("ImageLabel", { + AnchorPoint = Vector2.new(0.5, 0.5), + BackgroundTransparency = 1, + Image = isPlaying and audioPreviewTheme.pauseButton or audioPreviewTheme.playButton, + ImageColor3 = audioPreviewTheme.buttonColor, + ImageTransparency = isLoaded and 0 or audioPreviewTheme.buttonDisabledBackgroundTransparency, + Position = UDim2.new(0.5, 0, 0.5, 0), + Size = UDim2.new(1, 0, 1, 0), + }), + }), + + TimeComponent = isLoaded and Roact.createElement("TextLabel", { + AnchorPoint = Vector2.new(1, 0.5), + BackgroundTransparency = 1, + Font = audioPreviewTheme.font, + Position = UDim2.new(1, -controlOffset, 0.5, 0), + Size = UDim2.new(0, 204, 0, TIME_LABEL_HEIGHT), + Text = timeString, + TextColor3 = audioPreviewTheme.textColor, + TextSize = audioPreviewTheme.fontSize, + TextXAlignment = Enum.TextXAlignment.Right, + }), + + LoadingIndicator = (not isLoaded) and Roact.createElement(LoadingIndicator, { + AnchorPoint = Vector2.new(1, 0.5), + Position = UDim2.new(1, -controlOffset, 0.5, 0), + Size = UDim2.new(0, 50, 0, TIME_LABEL_HEIGHT), + }), + }) + end) +end + +return MediaControl \ No newline at end of file diff --git a/BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Components/Preview/MediaControl.spec.lua b/BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Components/Preview/MediaControl.spec.lua new file mode 100644 index 0000000000..3ff5a182f1 --- /dev/null +++ b/BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Components/Preview/MediaControl.spec.lua @@ -0,0 +1,34 @@ +return function() + local Library = script.Parent.Parent.Parent + local Roact = require(Library.Parent.Parent.Roact) + + local MockWrapper = require(Library.MockWrapper) + + local MediaControl = require(Library.Components.Preview.MediaControl) + + local function createTestAsset(container, name) + local emptyFunc = function() end + + local element = Roact.createElement(MockWrapper, {}, { + Roact.createElement(MediaControl, { + AnchorPoint = Vector2.new(0, 0), + IsPlaying = false, + IsLoaded = true, + LayoutOrder = 1, + OnPause = emptyFunc, + OnPlay = emptyFunc, + Position = UDim2.new(0, 0, 0, 0), + ShowTreeView = false, + TimeLength = 1, + TimePassed = 0, + }) + }) + + return Roact.mount(element, container or nil, name or "") + end + + it("should create and destroy without errors", function() + local instance = createTestAsset() + Roact.unmount(instance) + end) +end \ No newline at end of file diff --git a/BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Components/Preview/MediaProgressBar.lua b/BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Components/Preview/MediaProgressBar.lua new file mode 100644 index 0000000000..e7abdeafa7 --- /dev/null +++ b/BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Components/Preview/MediaProgressBar.lua @@ -0,0 +1,179 @@ +--[[ + A one-knob slider + TODO: DEVTOOLS-4330 - Replace this entire component with DevFramework's one-knob slider once assetPreview is out of UILibrary + + Required Props: + number Min: Min value of the slider + number Max: Max value of the slider + number CurrentValue: Current value for the lower range handle + callback OnValuesChanged: A callback that takes in params: minValue, maxValue. The callback is called whenever the min or max value changes. + + Optional Props: + Vector2 AnchorPoint: The anchorPoint of the component + boolean Disabled: Whether to render in the enabled/disabled state + number LayoutOrder: The layoutOrder of the component + UDim2 Position: The position of the component + number SnapIncrement: Incremental points that the slider's knob will snap to. A "0" snap increment means no snapping. + number VerticalDragTolerance: A vertical pixel height for allowing a pressed mouse to drag knobs on outside the component's size. + +]] +local Library = script.Parent.Parent.Parent +local Roact = require(Library.Parent.Parent.Roact) + +local Theming = require(Library.Theming) +local withTheme = Theming.withTheme + +local PROGRESS_BAR_HEIGHT = 6 +local knobSize = Vector2.new(15, 15) + +local MediaProgressBar = Roact.PureComponent:extend("MediaProgressBar") + +local function isUserInputTypeClick(inputType) + return (inputType == Enum.UserInputType.Touch) or (inputType == Enum.UserInputType.MouseButton1) +end + +MediaProgressBar.defaultProps = { + Disabled = false, + Size = UDim2.new(1, 0, 1, 0), + SnapIncrement = 0, + VerticalDragTolerance = 300, +} + +function MediaProgressBar:init() + self.sliderFrameRef = Roact.createRef() + + self.state = { + pressed = false + } + + self.getTotalRange = function() + return self.props.Max - self.props.Min + end + + self.getSnappedValue = function(value) + local snapIncrement = self.props.SnapIncrement + local min = self.props.Min + local max = self.props.Max + + if snapIncrement > 0 then + local prevSnap = math.max(snapIncrement * math.floor(value / snapIncrement), min) + local nextSnap = math.min(prevSnap + snapIncrement, max) + return math.abs(prevSnap-value) < math.abs(nextSnap-value) and prevSnap or nextSnap + end + + return math.clamp(value, min, max) + end + + self.getMouseClickValue = function(input) + local sliderFrameRef = self.sliderFrameRef.current + local inputHorizontalOffsetNormalized = (input.Position.X - sliderFrameRef.AbsolutePosition.X) / sliderFrameRef.AbsoluteSize.X + inputHorizontalOffsetNormalized = math.clamp(inputHorizontalOffsetNormalized, 0, 1) + local valueBeforeSnapping = self.props.Min + (inputHorizontalOffsetNormalized * self.getTotalRange()) + + return self.getSnappedValue(valueBeforeSnapping) + end + + self.setValuesFromInput = function(input) + local mouseClickValue = self.getMouseClickValue(input) + local clampedValue = math.clamp(mouseClickValue, self.props.Min, self.props.Max) + + self.props.OnValuesChanged(clampedValue) + end + + self.onInputBegan = function(rbx, input) + if self.props.Disabled then + return + + elseif isUserInputTypeClick(input.UserInputType) then + self:setState({ + pressed = true, + }) + self.setValuesFromInput(input) + end + end + + self.onInputChanged = function(rbx, input) + if self.props.Disabled then + return + + elseif self.state.pressed and input.UserInputType == Enum.UserInputType.MouseMovement then + self.setValuesFromInput(input) + end + end + + self.onInputEnded = function(rbx, input) + if not self.props.Disabled and isUserInputTypeClick(input.UserInputType) then + self.props.OnInputEnded() + self:setState({ + pressed = false, + }) + end + end +end + +function MediaProgressBar:render() + return withTheme(function(theme) + local audioPreviewTheme = theme.assetPreview.audioPreview + + local anchorPoint = self.props.AnchorPoint + local isDisabled = self.props.Disabled + local currentValue = self.props.CurrentValue + local layoutOrder = self.props.LayoutOrder + local min = self.props.Min + local position = self.props.Position + local verticalDragBuffer = self.props.VerticalDragTolerance + + local lowerFillPercent = (currentValue - min) / self.getTotalRange() + + return Roact.createElement("Frame", { + AnchorPoint = anchorPoint, + BackgroundTransparency = 1, + LayoutOrder = layoutOrder, + Position = position, + Size = UDim2.new(1, 0, 0, knobSize.X), + + [Roact.Ref] = self.sliderFrameRef, + }, { + ProgressBarBackground = Roact.createElement("Frame", { + AnchorPoint = Vector2.new(0, 0.5), + BackgroundColor3 = audioPreviewTheme.progressBar_BG_Color, + BackgroundTransparency = 0, + BorderSizePixel = 0, + Position = UDim2.new(0, 0, 0.5, 0), + Size = UDim2.new(1, 0, 0, PROGRESS_BAR_HEIGHT), + }, { + ProgressBarForeground = Roact.createElement("Frame", { + BackgroundColor3 = audioPreviewTheme.progressBar, + BackgroundTransparency = 0, + BorderSizePixel = 0, + Size = UDim2.new(lowerFillPercent, 0, 1, 0), + }), + }), + + Knob = Roact.createElement("ImageButton", { + AutoButtonColor = false, + AnchorPoint = Vector2.new(0, 0.5), + BackgroundTransparency = 1, + Image = audioPreviewTheme.progressKnob, + ImageColor3 = audioPreviewTheme.progressKnobColor, + Position = UDim2.new(lowerFillPercent, 0, 0.5, 0), + Size = UDim2.new(0, knobSize.X, 0, knobSize.Y), + ZIndex = 3, + }), + + ClickHandler = (not isDisabled) and Roact.createElement("ImageButton", { + AnchorPoint = Vector2.new(0.5, 0.5), + BackgroundTransparency = 1, + Position = UDim2.new(0.5, 0, 0.5, 0), + Size = UDim2.new(1, knobSize.X, 1, self.state.pressed and verticalDragBuffer or 0), + ZIndex = 4, + + [Roact.Event.InputBegan] = self.onInputBegan, + [Roact.Event.InputChanged] = self.onInputChanged, + [Roact.Event.InputEnded] = self.onInputEnded, + }), + }) + end) +end + +return MediaProgressBar \ No newline at end of file diff --git a/BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Components/Preview/MediaProgressBar.spec.lua b/BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Components/Preview/MediaProgressBar.spec.lua new file mode 100644 index 0000000000..d83b361d03 --- /dev/null +++ b/BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Components/Preview/MediaProgressBar.spec.lua @@ -0,0 +1,30 @@ +return function() + local Library = script.Parent.Parent.Parent + local Roact = require(Library.Parent.Parent.Roact) + + local MockWrapper = require(Library.MockWrapper) + + local MediaProgressBar = require(Library.Components.Preview.MediaProgressBar) + + local function createTestAsset(container, name) + local emptyFunc = function() end + + local element = Roact.createElement(MockWrapper, {}, { + Roact.createElement(MediaProgressBar, { + CurrentValue = 0, + LayoutOrder = 2, + Min = 0, + Max = 1, + OnValuesChanged = emptyFunc, + OnInputEnded = emptyFunc, + }) + }) + + return Roact.mount(element, container or nil, name or "") + end + + it("should create and destroy without errors", function() + local instance = createTestAsset() + Roact.unmount(instance) + end) +end \ No newline at end of file diff --git a/BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Components/Preview/ModelPreview.lua b/BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Components/Preview/ModelPreview.lua new file mode 100644 index 0000000000..8073f9373f --- /dev/null +++ b/BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Components/Preview/ModelPreview.lua @@ -0,0 +1,216 @@ +--[[ + This component handles the model rendering and input support for model preview. + + Necessary properties: + Position = UDim2 + Size = UDim2 + TargetModel = Model, the model is only for previewing. So, it contains the assetInstance with all he + scripts being disabled. + + Optional properties: + OnModelPreviewFrameEntered = callBack, we use those function to make sure input will be captured if + mouse is within the area of the frame. + OnModelPreviewFrameLeft = callBack +]] + +local Library = script.Parent.Parent.Parent + +local Roact = require(Library.Parent.Parent.Roact) + +local getCamera = require(Library.Camera).getCamera + +local Theming = require(Library.Theming) +local withTheme = Theming.withTheme + +local ModelPreview = Roact.PureComponent:extend("ModelPreview") + +-- Used for the inital camera position +local INSERT_CAMERA_DIST_MULT = 0.8 +local PAN_CAMERA_DIST_MULT = 0.1 +local DOUBLE_CLICK_TIME = 0.25 + +function ModelPreview:init() + self.orbitDrag = false + self.panDrag = false + self.doubleClickTimestamp = tick() + + -- Need reference to ViewportFrame so I can set the preview model to it. + self.VFRef = Roact.createRef() + + self.modelPreviewCamera = getCamera(self) + + -- This is the model that will be displayed on the ViewportFrame. + self.VFModel = nil + + self.onInputBegan = function(_, input) + if input.UserInputType == Enum.UserInputType.MouseButton1 then + self.orbitDrag = true + end + + if input.UserInputType == Enum.UserInputType.MouseButton3 or + input.UserInputType == Enum.UserInputType.MouseButton2 then + self.panDrag = true + end + end + + self.onInputChanged = function(_, input) + if input.UserInputType == Enum.UserInputType.MouseMovement + and self.orbitDrag or self.panDrag then + local camera = self.modelPreviewCamera + local targetFocus = camera.Focus + local targetCF = targetFocus:ToObjectSpace(camera.CFrame) + + if self.orbitDrag then + targetCF = CFrame.fromAxisAngle(targetCF.RightVector, input.Delta.y * -0.01) * targetCF + targetCF = CFrame.fromAxisAngle(Vector3.new(0, 1, 0), input.Delta.x * -0.01) * targetCF + elseif self.panDrag then + local dist = (targetCF.p - targetFocus.p).magnitude + dist = dist ~= dist and 0 or dist -- NaN check + local distanceFactor = PAN_CAMERA_DIST_MULT * (dist * 0.1) + local yOffset = targetCF.upVector.Unit * input.Delta.y * distanceFactor + local xOffset = -targetCF.rightVector.Unit * input.Delta.x * distanceFactor + targetCF = targetCF + yOffset + xOffset + targetFocus = targetFocus + yOffset + xOffset + end + + camera.CFrame = camera.Focus:ToWorldSpace(targetCF) + camera.Focus = targetFocus + end + end + + self.onInputEnded = function(_, input) + if input.UserInputType == Enum.UserInputType.MouseButton1 then + self.orbitDrag = false + if tick() < self.doubleClickTimestamp + DOUBLE_CLICK_TIME then + self.centerCamera() + end + self.doubleClickTimestamp = tick() + end + + if input.UserInputType == Enum.UserInputType.MouseButton3 or + input.UserInputType == Enum.UserInputType.MouseButton2 then + self.panDrag = false + end + end + + self.zoomCamera = function(zoomIn) + local zoomFactor = zoomIn and -1 or 1 + local camera = self.modelPreviewCamera + local current = camera.CFrame + local focus = camera.Focus + + local dist = (current.p - focus.p).magnitude + dist = dist ~= dist and 0 or dist -- NaN check + + local moveAmount = math.max(dist * 0.1, 0.1) + local targetCF = current * CFrame.new(0, 0, zoomFactor * moveAmount) + + camera.CFrame = targetCF + end + + self.onMouseWheelBackward = function() + self.zoomCamera(false) + end + + self.onMouseWheelForward = function() + self.zoomCamera(true) + end + + self.centerCamera = function() + local currentPreviewCopy = self.VFModel + local camera = self.modelPreviewCamera + + -- Move the model/part in front of the camera + local success, modelCf, size = pcall(function() return currentPreviewCopy:GetBoundingBox() end) + if not success then + size = currentPreviewCopy.Size + currentPreviewCopy.CFrame = currentPreviewCopy.CFrame - currentPreviewCopy.CFrame.p + else + currentPreviewCopy:TranslateBy(-modelCf.p) + end + + local cameraDistAway = size.magnitude * INSERT_CAMERA_DIST_MULT + local dir = Vector3.new(1, 1, 1).unit + camera.Focus = CFrame.new() + camera.CFrame = CFrame.new(cameraDistAway * dir, camera.Focus.p) + end + + -- Because we are using refs and not using state, this component will not + -- re-render unless we are viewing a different model. Every call to this + -- function assumes that a different model has been selected. + self.tryRenderModel = function() + local myVRFrame = self.VFRef.current + local currentPreviewCopy = self.VFModel + myVRFrame:ClearAllChildren() + currentPreviewCopy.Parent = myVRFrame + + self.centerCamera() + end +end + +function ModelPreview:makeViewportModel() + local currentPreview = self.props.TargetModel + if currentPreview:IsA("Model") or currentPreview:IsA("BasePart") then + self.VFModel = currentPreview:Clone() + else + self.VFModel = Instance.new("Model") + currentPreview:Clone().Parent = self.VFModel + end +end + +function ModelPreview:didMount() + self:makeViewportModel() + self.tryRenderModel() +end + +function ModelPreview:didUpdate() + self.tryRenderModel() +end + +function ModelPreview:willUnmount() + if self.VFModel then + self.VFModel:Destroy() + end +end + +function ModelPreview:render() + return withTheme(function(theme) + local props = self.props + local position = props.Position + local size = props.Size or UDim2.new(1, 0, 1, 0) + local ModelPreviewCamera = self.modelPreviewCamera + + local OnModelPreviewFrameEntered = props.OnModelPreviewFrameEntered + local OnModelPreviewFrameLeft = props.OnModelPreviewFrameLeft + + local layoutOrder = props.LayoutOrder + + self:makeViewportModel() + + -- The element we return is determined by object we receive. + return Roact.createElement("ViewportFrame", { + Position = position, -- We should avoid using relative position and size. + Size = size, + + BorderSizePixel = 0, + BackgroundTransparency = 0, + BackgroundColor3 = theme.assetPreview.modelPreview.background, + + CurrentCamera = ModelPreviewCamera, + [Roact.Ref] = self.VFRef, + + [Roact.Event.MouseEnter] = OnModelPreviewFrameEntered, + [Roact.Event.MouseLeave] = OnModelPreviewFrameLeft, + [Roact.Event.MouseWheelForward] = self.onMouseWheelForward, + [Roact.Event.MouseWheelBackward] = self.onMouseWheelBackward, + [Roact.Event.InputBegan] = self.onInputBegan, + [Roact.Event.InputEnded] = self.onInputEnded, + [Roact.Event.InputChanged] = self.onInputChanged, + [Roact.Event.TouchPinch] = self.onTouchPinch, + + LayoutOrder = layoutOrder, + }) + end) +end + +return ModelPreview diff --git a/BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Components/Preview/ModelPreview.spec.lua b/BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Components/Preview/ModelPreview.spec.lua new file mode 100644 index 0000000000..6a0d95fe7d --- /dev/null +++ b/BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Components/Preview/ModelPreview.spec.lua @@ -0,0 +1,28 @@ +return function() + local Library = script.Parent.Parent.Parent + local Roact = require(Library.Parent.Parent.Roact) + + local MockWrapper = require(Library.MockWrapper) + + local ModelPreview = require(Library.Components.Preview.ModelPreview) + + local function createTestAsset(container, name) + local testModel = Instance.new("Model") + + local element = Roact.createElement(MockWrapper, {}, { + ModelPreview = Roact.createElement(ModelPreview, { + TargetModel = testModel, + + Position = UDim2.new(1, 0, 1, 0), + Size = UDim2.new(1, 0, 1, 0), + }), + }) + + return Roact.mount(element, container or nil, name or "") + end + + it("should create and destroy without errors", function() + local instance = createTestAsset() + Roact.unmount(instance) + end) +end \ No newline at end of file diff --git a/BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Components/Preview/PreviewController.lua b/BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Components/Preview/PreviewController.lua new file mode 100644 index 0000000000..1c2ac3c7ed --- /dev/null +++ b/BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Components/Preview/PreviewController.lua @@ -0,0 +1,350 @@ +--!nolint DeprecatedGlobal +--^ enables migration with FFlagPreviewControllerUseOsClock; remove with that flag. + +--[[ + This component is used to render both mainPreview and TreeView. + MainView can be modelPreview, soundPreview, scriptPreview, imagePreview, otherPreview and audioPlay. + Note soundPreview and audioPlay are different here. Sound preview is uesed to preview sound object comes with + the model, audioPlay is the audioPlayer we made for the audio asset. Which actually supports play, pause. + + Required Props: + Width = number, + + CurrentPreview = Instance, this is the instance that is currently displayed in the preview. + PreviewModel = Instance, this is the top level asset that will be displayed in the InstanceTreeView + AssetPreviewType = AssetType.TYPES, custom category that will inform which preview will be displayed. + AssetId = number, + PutTreeViewOnBottom = boolean, this determines whether the TreeView will be displayed on the right or bottom. + + OnTreeItemClicked = callback, which sets the preview to show the new Tree Item, the callback takes 1 parameter of type instance. + OnModelPreviewFrameEntered = callback, this is a callback that disables the scrollbar + OnModelPreviewFrameLeft = callback, this is a callback that re-enables teh scollbar + + LayoutOrder = number, +]] +local FFlagStudioMinorFixesForAssetPreview = settings():GetFFlag("StudioMinorFixesForAssetPreview") +local FFlagHideOneChildTreeviewButton = game:GetFastFlag("HideOneChildTreeviewButton") +local FFlagStudioFixTreeViewForFlatList = settings():GetFFlag("StudioFixTreeViewForFlatList") +local FFlagStudioAssetPreviewTreeFix2 = game:DefineFastFlag("StudioAssetPreviewTreeFix2", false) +local FFlagEnableToolboxVideos = game:GetFastFlag("EnableToolboxVideos") + +local FFlagPreviewControllerUseOsClock = game:DefineFastFlag("PreviewControllerUseOsClock", false) + +local Library = script.Parent.Parent.Parent + +local Roact = require(Library.Parent.Parent.Roact) + +local ModelPreview = require(Library.Components.Preview.ModelPreview) +local ImagePreview = require(Library.Components.Preview.ImagePreview) +local ThumbnailIconPreview = require(Library.Components.Preview.ThumbnailIconPreview) +local TreeViewButton = require(Library.Components.Preview.TreeViewButton) +local AssetType = require(Library.Utils.AssetType) +local AudioPreview = require(Library.Components.Preview.AudioPreview) +local VideoPreview = require(Library.Components.Preview.VideoPreview) + +local TreeViewItem = require(Library.Components.Preview.InstanceTreeViewItem) +local TreeView = require(Library.Components.TreeView) + +local LoadingIndicator = require(Library.Components.LoadingIndicator) + +local Theming = require(Library.Theming) +local withTheme = Theming.withTheme + +local Urls = require(Library.Utils.Urls) + +local TREEVIEW_WIDTH = 242 +local PREVIEW_HEIGHT = 242 +local TREEVIEW_BOTTOM_HEIGHT = 120 +local MAINVIEW_BUTTONS_X_OFFSET = -7 +local MAINVIEW_BUTTONS_Y_OFFSET = -7 + +local MODAL_MIN_WIDTH = 235 + +local PreviewController = Roact.PureComponent:extend("PreviewController") + +local function getImage(instance) + if FFlagStudioMinorFixesForAssetPreview then + if typeof(instance) ~= "Instance" then + return nil + end + end + + if instance:IsA("Decal") or instance:IsA("Texture") then + return instance.Texture + elseif instance:IsA("Sky") then + return instance.SkyboxFt + else + return instance.Image + end +end + +local function getImageScaleType(instance) + if FFlagStudioMinorFixesForAssetPreview then + if typeof(instance) ~= "Instance" then + return Enum.ScaleType.Fit + end + end + if instance:IsA("Sky") then + return Enum.ScaleType.Crop + else + return Enum.ScaleType.Fit + end +end + +function PreviewController:createTreeView(previewModel, size) + return withTheme(function(theme) + local onTreeviewEntered = self.onTreeviewEntered + local onTreeviewLeft = self.onTreeviewLeft + + local dataTree + if FFlagStudioAssetPreviewTreeFix2 then + dataTree = previewModel + else + dataTree = FFlagStudioFixTreeViewForFlatList and self.props.CurrentPreview or previewModel + end + + return Roact.createElement("ImageButton", { + Size = size, + + BackgroundTransparency = 0, + BackgroundColor3 = theme.instanceTreeView.background, + BorderSizePixel = 0, + AutoButtonColor = false, + + [Roact.Event.MouseEnter] = onTreeviewEntered, + [Roact.Event.MouseLeave] = onTreeviewLeft, + + LayoutOrder = 2 + },{ + TreeViewFrame = Roact.createElement(TreeView, { + dataTree = dataTree, + onSelectionChanged = self.onTreeItemClicked, + + createFlatList = FFlagStudioFixTreeViewForFlatList and true or false, + + getChildren = function(instance) + return instance:GetChildren() + end, + + renderElement = function(properties) + return Roact.createElement(TreeViewItem, properties) + end, + }) + }) + end) +end + +function PreviewController:init(props) + self.state = { + -- This controls the treeview and full screen button's status + showTreeView = false, + } + + self.inModelPreview = false + self.inTreeview = false + self.totalTimeSpent = 0 + self.cacheTime = 0 + + self.onTreeviewStatusToggle = function(newStatus) + self:setState({ + showTreeView = newStatus, + }) + end + + self.onModelPreviewFrameEntered = function(...) + self.props.OnModelPreviewFrameEntered(...) + + self.onPreviewStatusChange(true, self.inTreeview) + end + + self.onModelPreviewFrameLeft = function(...) + self.props.OnModelPreviewFrameLeft(...) + + self.onPreviewStatusChange(false, self.inTreeview) + end + + self.onTreeviewEntered = function() + self.onPreviewStatusChange(self.inModelPreview, true) + end + + self.onTreeviewLeft = function() + self.onPreviewStatusChange(self.inModelPreview, false) + end + + self.onPreviewStatusChange = function(newModelStatus, newTreeStatus) + if (not self.inModelPreview and not self.inTreeview) and + (newModelStatus or newTreeStatus) then + -- Time to start the timer + self.cacheTime = FFlagPreviewControllerUseOsClock and os.clock() or elapsedTime() + end + + if (not newModelStatus and not newTreeStatus) and + (self.inModelPreview or self.inTreeview) then + local currentTime = FFlagPreviewControllerUseOsClock and os.clock() or elapsedTime() + local newTimeSpent = currentTime - self.cacheTime + if newTimeSpent > 0 then + self.totalTimeSpent = self.totalTimeSpent + math.floor(newTimeSpent * 1000) + end + end + + self.inModelPreview = newModelStatus + self.inTreeview = newTreeStatus + end + + self.onTreeItemClicked = function(instances) + if instances[1] then + self.props.OnTreeItemClicked(instances[1]) + end + end +end + +function PreviewController:render() + local props = self.props + local state = self.state + + local currentPreview = props.CurrentPreview + local previewModel = props.PreviewModel + local assetPreviewType = props.AssetPreviewType + local assetId = props.AssetId + local putTreeviewOnBottom = props.PutTreeviewOnBottom + local width = props.Width + local layoutOrder = props.LayoutOrder + + local isShowVideoPreview = FFlagEnableToolboxVideos and AssetType:isVideo(assetPreviewType) + local videoId + if isShowVideoPreview then + videoId = currentPreview.Video + end + + local showTreeView = state.showTreeView + local previewSize + local treeViewSize + local height + if showTreeView then + height = putTreeviewOnBottom and PREVIEW_HEIGHT + TREEVIEW_BOTTOM_HEIGHT or PREVIEW_HEIGHT + previewSize = putTreeviewOnBottom and UDim2.new(1, 0, 0, PREVIEW_HEIGHT) + or UDim2.new(1, -TREEVIEW_WIDTH, 0, PREVIEW_HEIGHT) + treeViewSize = putTreeviewOnBottom and UDim2.new(1, 0, 0, TREEVIEW_BOTTOM_HEIGHT) + or UDim2.new(0, TREEVIEW_WIDTH, 0, PREVIEW_HEIGHT) + else + height = PREVIEW_HEIGHT + previewSize = UDim2.new(1, 0, 0, PREVIEW_HEIGHT) + treeViewSize = UDim2.new() + end + + local showTreeViewButton = (not AssetType:isPlugin(assetPreviewType)) + if FFlagHideOneChildTreeviewButton then + local dataTree + if FFlagStudioAssetPreviewTreeFix2 then + dataTree = previewModel + else + dataTree = FFlagStudioFixTreeViewForFlatList and self.props.CurrentPreview or previewModel + end + local hasMultiplechildren = dataTree and (#dataTree:GetChildren() > 0) or false + showTreeViewButton = showTreeViewButton and hasMultiplechildren + end + + local onModelPreviewFrameEntered = self.onModelPreviewFrameEntered + local onModelPreviewFrameLeft = self.onModelPreviewFrameLeft + + local THUMBNAIL_HEIGHT = PREVIEW_HEIGHT < MODAL_MIN_WIDTH and PREVIEW_HEIGHT or MODAL_MIN_WIDTH + local showThumbnail = AssetType:isScript(assetPreviewType) or AssetType:isOtherType(assetPreviewType) + + local soundId + if AssetType:isAudio(assetPreviewType) and currentPreview then + -- It's wrong to get SoundId from currenttPreview, it should be previewModel. + soundId = currentPreview.SoundId + end + + local reportPlay = props.reportPlay + local reportPause = props.reportPause + + local isShowAudioPreview = AssetType:isAudio(assetPreviewType) + local mainViewButtonYOffset + if isShowAudioPreview then + mainViewButtonYOffset = 3 + else + mainViewButtonYOffset = MAINVIEW_BUTTONS_Y_OFFSET + end + + return Roact.createElement("Frame", { + Size = UDim2.new(1, width, 0, height), + + BackgroundTransparency = 1, + BorderSizePixel = 0, + + LayoutOrder = layoutOrder, + }, { + UIListLayout = Roact.createElement("UIListLayout", { + FillDirection = putTreeviewOnBottom and Enum.FillDirection.Vertical or Enum.FillDirection.Horizontal, + HorizontalAlignment = Enum.HorizontalAlignment.Left, + VerticalAlignment = Enum.VerticalAlignment.Top, + SortOrder = Enum.SortOrder.LayoutOrder, + }), + + MainView = Roact.createElement("Frame", { + Size = previewSize, + + BackgroundTransparency = 1, + BorderSizePixel = 0, + + LayoutOrder = 1, + }, { + PreviewLoading = AssetType:isLoading(assetPreviewType) and Roact.createElement(LoadingIndicator,{ + AnchorPoint = Vector2.new(0.5, 0.5), + Position = UDim2.new(0.5, 0, 0.5, 0), + }), + + ModelPreview = AssetType:isModel(assetPreviewType) and Roact.createElement(ModelPreview, { + TargetModel = currentPreview, + + OnModelPreviewFrameEntered = onModelPreviewFrameEntered, + OnModelPreviewFrameLeft = onModelPreviewFrameLeft, + }), + + ImagePreview = AssetType:isImage(assetPreviewType) and Roact.createElement(ImagePreview, { + ImageContent = getImage(currentPreview), + ScaleType = getImageScaleType(currentPreview), + }), + + AudioPreview = isShowAudioPreview and Roact.createElement(AudioPreview, { + SoundId = soundId or Urls.constructAssetIdString(assetId), + AssetId = assetId, + ShowTreeView = showTreeView, + ReportPlay = reportPlay, + ReportPause = reportPause, + }), + + VideoPreview = isShowVideoPreview and Roact.createElement(VideoPreview, { + VideoId = videoId or Urls.constructAssetIdString(assetId), + ShowTreeView = showTreeView, + }), + + PluginPreview = AssetType:isPlugin(assetPreviewType) and Roact.createElement("ImageLabel", { + Image = Urls.constructAssetThumbnailUrl(assetId, 420, 420), + Size = UDim2.new(0,THUMBNAIL_HEIGHT,0,THUMBNAIL_HEIGHT), + Position = UDim2.new(0.5,0,0,0), + AnchorPoint = Vector2.new(0.5,0), + }), + + -- Let the script and other share the same component for now + ThumbnailIconPreview = showThumbnail and Roact.createElement(ThumbnailIconPreview, { + TargetInstance = currentPreview, + AssetId = assetId, + ElementName = currentPreview.Name, + }), + + TreeViewButton = showTreeViewButton and Roact.createElement(TreeViewButton, { + Position = UDim2.new(1, MAINVIEW_BUTTONS_X_OFFSET, 1, mainViewButtonYOffset), + ZIndex = 2, + + ShowTreeView = state.showTreeView, + OnTreeviewStatusToggle = self.onTreeviewStatusToggle, + }) + }), + + TreeView = showTreeView and self:createTreeView(previewModel, treeViewSize) + }) +end + +return PreviewController diff --git a/BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Components/Preview/PreviewController.spec.lua b/BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Components/Preview/PreviewController.spec.lua new file mode 100644 index 0000000000..a8d1349fde --- /dev/null +++ b/BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Components/Preview/PreviewController.spec.lua @@ -0,0 +1,36 @@ +return function() + local Library = script.Parent.Parent.Parent + local Roact = require(Library.Parent.Parent.Roact) + + local MockWrapper = require(Library.MockWrapper) + + local PreviewController = require(Library.Components.Preview.PreviewController) + + local AssetType = require(Library.Utils.AssetType) + + -- PineTree + -- rbxassetid://183435411 + local function createTestAsset(container, name) + local assetId = 183435411 + local previewModel = Instance.new("Model") + + local element = Roact.createElement(MockWrapper, {}, { + PreviewController = Roact.createElement(PreviewController, { + width = 40, + + currentPreview = previewModel, + previewModel = previewModel, + assetPreviewType = AssetType.TYPES.ModelType, + assetId = assetId, + putTreeviewOnBottom = true, + }) + }) + + return Roact.mount(element, container or nil, name or "") + end + + it("should create and destroy without errors", function() + local instance = createTestAsset() + Roact.unmount(instance) + end) +end \ No newline at end of file diff --git a/BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Components/Preview/SearchLinkText.lua b/BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Components/Preview/SearchLinkText.lua new file mode 100644 index 0000000000..5a24fb81df --- /dev/null +++ b/BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Components/Preview/SearchLinkText.lua @@ -0,0 +1,141 @@ +--[[ + A component used for a link text with an animated search icon, + that appears when the user hovers over the link text. + + Necessary Properties: + Position = UDim2, the positon of this component. + AnchorPoint = Vector2, the centering of the component relative to its parent. + + Text = string, the Creator name to be shown in the text label. + OnClick = callback, A callback for when the link is clicked. + + Optional Properties: + number TweenTime = The time in seconds to play the hover animation. +]] + +local TweenService = game:GetService("TweenService") +local DEFAULT_TWEEN_TIME = 0.2 + +local Library = script.Parent.Parent.Parent + +local Roact = require(Library.Parent.Parent.Roact) + +local GetTextSize = require(Library.Utils.GetTextSize) + +local Theming = require(Library.Theming) +local withTheme = Theming.withTheme + +local SearchLinkText = Roact.PureComponent:extend("SearchLinkText") + +function SearchLinkText:init(props) + assert(type(props.OnClick) == "function", "SearchLinkText expects an 'OnClick' function.") + self.tweenInfo = TweenInfo.new(props.TweenTime or DEFAULT_TWEEN_TIME) + + self.textRef = Roact.createRef() + self.iconRef = Roact.createRef() + self.tweens = {} + + self.mouseEnter = function() + if self.tweens.TextEnter then + self.tweens.TextEnter:Play() + end + if self.tweens.IconEnter then + self.tweens.IconEnter:Play() + end + end + + self.mouseLeave = function() + if self.tweens.TextLeave then + self.tweens.TextLeave:Play() + end + if self.tweens.IconLeave then + self.tweens.IconLeave:Play() + end + end +end + +function SearchLinkText:didMount() + local text = self.textRef:getValue() + local icon = self.iconRef:getValue() + self.tweens.TextEnter = TweenService:Create(text, self.tweenInfo, { + Position = UDim2.fromScale(0, 0.5), + AnchorPoint = Vector2.new(0, 0.5), + }) + self.tweens.TextLeave = TweenService:Create(text, self.tweenInfo, { + Position = UDim2.fromScale(1, 0.5), + AnchorPoint = Vector2.new(1, 0.5), + }) + self.tweens.IconEnter = TweenService:Create(icon, self.tweenInfo, { + ImageTransparency = 0, + AnchorPoint = Vector2.new(1, 0.5), + }) + self.tweens.IconLeave = TweenService:Create(icon, self.tweenInfo, { + ImageTransparency = 1, + AnchorPoint = Vector2.new(0, 0.5), + }) +end + +function SearchLinkText:willUnmount() + for _, tween in pairs(self.tweens) do + tween:Cancel() + tween:Destroy() + end +end + +function SearchLinkText:render() + return withTheme(function(theme) + local props = self.props + local text = props.Text + local position = props.Position + local anchorPoint = props.AnchorPoint + + local searchLinkTextTheme = theme.assetPreview.description + + local textDimensions + local textExtents = GetTextSize(text, theme.assetPreview.textSizeLarge) + textDimensions = UDim2.fromOffset(textExtents.X, textExtents.Y) + + local fullWidth = textExtents.X + searchLinkTextTheme.searchBarIconSize + + searchLinkTextTheme.padding + + return Roact.createElement("TextButton", { + Size = UDim2.new(0, fullWidth, 1, 0), + Position = position, + AnchorPoint = anchorPoint, + BackgroundTransparency = 1, + + Text = "", + [Roact.Event.Activated] = props.OnClick, + [Roact.Event.MouseEnter] = self.mouseEnter, + [Roact.Event.MouseLeave] = self.mouseLeave, + }, { + Text = Roact.createElement("TextLabel", { + Size = textDimensions, + Position = UDim2.fromScale(1, 0.5), + AnchorPoint = Vector2.new(1, 0.5), + BackgroundTransparency = 1, + + Text = text, + Font = theme.assetPreview.font, + TextColor3 = searchLinkTextTheme.rightTextColor, + TextSize = theme.assetPreview.textSizeLarge, + [Roact.Ref] = self.textRef, + }), + + SearchIcon = Roact.createElement("ImageLabel", { + Size = UDim2.new(0, searchLinkTextTheme.searchBarIconSize, + 0, searchLinkTextTheme.searchBarIconSize), + Position = UDim2.fromScale(1, 0.5), + AnchorPoint = Vector2.new(0, 0.5), + BackgroundTransparency = 1, + ImageTransparency = 1, + + ImageColor3 = searchLinkTextTheme.rightTextColor, + Image = searchLinkTextTheme.images.searchIcon, + [Roact.Ref] = self.iconRef, + }), + }) + end) +end + +return SearchLinkText diff --git a/BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Components/Preview/SearchLinkText.spec.lua b/BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Components/Preview/SearchLinkText.spec.lua new file mode 100644 index 0000000000..c257b03a3e --- /dev/null +++ b/BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Components/Preview/SearchLinkText.spec.lua @@ -0,0 +1,51 @@ +return function() + local Library = script.Parent.Parent.Parent + local Roact = require(Library.Parent.Parent.Roact) + + local MockWrapper = require(Library.MockWrapper) + + local SearchLinkText = require(Library.Components.Preview.SearchLinkText) + + it("should expect an OnClick function", function() + local element = Roact.createElement(MockWrapper, {}, { + LinkText = Roact.createElement(SearchLinkText, { + Text = "Test", + }), + }) + expect(function() + local instance = Roact.mount(element) + Roact.unmount(instance) + end).to.throw() + end) + + it("should create and destroy without errors", function() + local element = Roact.createElement(MockWrapper, {}, { + LinkText = Roact.createElement(SearchLinkText, { + Text = "Test", + OnClick = function() + end, + }), + }) + local instance = Roact.mount(element) + Roact.unmount(instance) + end) + + it("should render correctly", function() + local element = Roact.createElement(MockWrapper, {}, { + LinkText = Roact.createElement(SearchLinkText, { + Text = "Test", + OnClick = function() + end, + }), + }) + + local container = Instance.new("Folder") + local instance = Roact.mount(element, container) + + local main = container:FindFirstChildOfClass("TextButton") + expect(main.Text).to.be.ok() + expect(main.SearchIcon).to.be.ok() + + Roact.unmount(instance) + end) +end diff --git a/BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Components/Preview/ThumbnailIconPreview.lua b/BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Components/Preview/ThumbnailIconPreview.lua new file mode 100644 index 0000000000..9770397bb2 --- /dev/null +++ b/BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Components/Preview/ThumbnailIconPreview.lua @@ -0,0 +1,91 @@ +--[[ + This component is the default shown for the 3D view in asset preview. This component shows the class + icon for that instance and the name of the element. + + Necessary properties: + Postion = UDim2 + Size = UDim2, this property determines the size of the preview. + ElementName = String, the name of the asset, this will be displayed below the icon. + TargetInstance = The instance to preview. + + Optional properties: + IconSize = number, will default to 16 unless otherwise specified, + this affects the dimensions of the icon representing the asset. + TextLabelHeight = number +]] + +local Library = script.Parent.Parent.Parent + +local Roact = require(Library.Parent.Parent.Roact) + +local Theming = require(Library.Theming) +local withTheme = Theming.withTheme + +local GetClassIcon = require(Library.Utils.GetClassIcon) + +local ThumbnailIconPreview = Roact.PureComponent:extend("ThumbnailIconPreview") + +function ThumbnailIconPreview:render() + return withTheme(function(theme) + local props = self.props + + local elementName = props.ElementName or "" + local instance = props.TargetInstance + local iconInfo = GetClassIcon(instance) + + local position = props.Position + local size = props.Size or UDim2.new(1, 0, 1, 0) + local thumbnailIconPreviewTheme = theme.assetPreview.thumbnailIconPreview + local iconSize = props.IconSize or thumbnailIconPreviewTheme.iconSize + local padding = thumbnailIconPreviewTheme.textLabelPadding + local textLabelHeight = props.TextLabelHeight or thumbnailIconPreviewTheme.defaultTextLabelHeight + + local layoutOrder = props.LayoutOrder + + return Roact.createElement("Frame", { + Position = position, + Size = size, + + BackgroundTransparency = 0, + BackgroundColor3 = thumbnailIconPreviewTheme.background, + BorderSizePixel = 0, + + LayoutOrder = layoutOrder, + },{ + UIListLayout = Roact.createElement("UIListLayout", { + SortOrder = Enum.SortOrder.LayoutOrder, + FillDirection = Enum.FillDirection.Vertical, + HorizontalAlignment = Enum.HorizontalAlignment.Center, + VerticalAlignment = Enum.VerticalAlignment.Center, + }), + + ImageContent = Roact.createElement("ImageLabel", { + Size = UDim2.new(0, iconSize, 0, iconSize), + + BackgroundTransparency = 1, + + Image = iconInfo.Image, + ImageRectSize = iconInfo.ImageRectSize, + ImageRectOffset = iconInfo.ImageRectOffset, + LayoutOrder = 1, + }), + + TextContent = Roact.createElement("TextLabel", { + Size = UDim2.new(1, -2 * padding, 0, textLabelHeight), + + Text = tostring(elementName), + TextColor3 = thumbnailIconPreviewTheme.textColor, + Font = theme.assetPreview.font, + TextSize = theme.assetPreview.textSize, + TextXAlignment = Enum.TextXAlignment.Center, + + BackgroundTransparency = 1, + LayoutOrder = 2, + }) + }) + end) +end + +return ThumbnailIconPreview + + diff --git a/BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Components/Preview/ThumbnailIconPreview.spec.lua b/BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Components/Preview/ThumbnailIconPreview.spec.lua new file mode 100644 index 0000000000..bbe29f9b00 --- /dev/null +++ b/BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Components/Preview/ThumbnailIconPreview.spec.lua @@ -0,0 +1,27 @@ +return function() + local Library = script.Parent.Parent.Parent + local Roact = require(Library.Parent.Parent.Roact) + + local MockWrapper = require(Library.MockWrapper) + + local ThumbnailIconPreview = require(Library.Components.Preview.ThumbnailIconPreview) + + local function createTestAsset(container, name) + local targetInstance = Instance.new("Script") + local element = Roact.createElement(MockWrapper, {}, { + Roact.createElement(ThumbnailIconPreview, { + TargetInstance = targetInstance, + TextContent = "ThumbnailIconPreviewTest", + Position = UDim2.new(1, 0, 1, 0), + Size = UDim2.new(0, 100, 0, 20) + }) + }) + + return Roact.mount(element, container or nil, name or "") + end + + it("should create and destroy without errors", function() + local instance = createTestAsset() + Roact.unmount(instance) + end) +end \ No newline at end of file diff --git a/BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Components/Preview/TreeViewButton.lua b/BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Components/Preview/TreeViewButton.lua new file mode 100644 index 0000000000..5a5688eb8f --- /dev/null +++ b/BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Components/Preview/TreeViewButton.lua @@ -0,0 +1,143 @@ +--[[ + This is a button component styled to represent toggling a tree view for Asset Preview + + Necessary properties: + Position = UDim2 + ZIndex = number, + ShowTreeView = boolean, represents whether or not the button is selected. + OnTreeviewStatusToggle = callback, this is thefunction that should be invoked by this button. + + Optionlal properties: + Size = number, This is the length and width of the button. +]] + +local Library = script.Parent.Parent.Parent + +local Roact = require(Library.Parent.Parent.Roact) + +local RoundButton = require(Library.Components.RoundFrame) + +local Theming = require(Library.Theming) +local withTheme = Theming.withTheme + +local MainViewButtons = Roact.PureComponent:extend("MainViewButtons") + +-- Determined by how many buttons we have and the padding. +local TOTAL_WIDTH = 28 +local TOTAL_HEIGHT = 28 + +local INLINE_PADDING = 6 + +local BUTTON_STATUS = { + default = 0, + hovered = 1, + disabled = 2, +} + +function MainViewButtons:init(props) + self.state = { + treeViewButtonStatus = BUTTON_STATUS.default, + } + + self.onTreeViewButtonActivated = function() + local newTreeViewStatus = not self.props.ShowTreeView + + self.props.OnTreeviewStatusToggle(newTreeViewStatus) + end + + self.onTreeViewButtonEnter = function() + self:setState({ + treeViewButtonStatus = BUTTON_STATUS.hovered + }) + end + + self.onTreeViewButtonLeave = function() + self:setState({ + treeViewButtonStatus = BUTTON_STATUS.default + }) + end +end + +local function getButtonBGColorAndTrans(buttonsTheme, buttonStatus, toggleStatus) + local buttonBGColor + local buttonTrans + + local defaultTrans = buttonsTheme.backgroundTrans + if toggleStatus then + defaultTrans = defaultTrans + 0.3 + end + + if buttonStatus == BUTTON_STATUS.default then + buttonBGColor = buttonsTheme.backgroundColor + buttonTrans = defaultTrans + elseif buttonStatus == BUTTON_STATUS.hovered then + buttonBGColor = buttonsTheme.backgroundColor + buttonTrans = defaultTrans + 0.3 + else -- BUTTON_STATUS.disabled + buttonBGColor = buttonsTheme.backgroundDisabledColor + buttonTrans = defaultTrans + end + + return buttonBGColor, buttonTrans +end + +function MainViewButtons:render() + return withTheme(function(theme) + local props = self.props + local state = self.state + + local buttonsTheme = theme.assetPreview.treeViewButton + + local position = props.Position or UDim2.new(1, 0, 1, 0) + local treeViewButtonSize = props.TreeViewButtonSize or buttonsTheme.buttonSize + + local treeViewButtonBGColor, treeViewbButtonTrans = getButtonBGColorAndTrans(buttonsTheme, + state.treeViewButtonStatus, + props.showTreeView) + + return Roact.createElement("Frame", { + Position = position, + AnchorPoint = Vector2.new(1, 1), + Size = UDim2.new(0, TOTAL_WIDTH, 0, TOTAL_HEIGHT), + ZIndex = props.ZIndex or 1, + + BackgroundTransparency = 1, + BorderSizePixel = 0, + },{ + UIListLayout = Roact.createElement("UIListLayout", { + FillDirection = Enum.FillDirection.Horizontal, + HorizontalAlignment = Enum.HorizontalAlignment.Right, + VerticalAlignment = Enum.VerticalAlignment.Center, + + SortOrder = Enum.SortOrder.LayoutOrder, + Padding = UDim.new(0, INLINE_PADDING), + }), + + TreeViewBGButton = Roact.createElement(RoundButton, { + Size = UDim2.new(0, treeViewButtonSize, 0, treeViewButtonSize), + + BackgroundTransparency = treeViewbButtonTrans, + BackgroundColor3 = treeViewButtonBGColor, + BorderSizePixel = 0, + + LayoutOrder = 1, + AutoButtonColor = false, + + OnActivated = self.onTreeViewButtonActivated, + OnMouseEnter = self.onTreeViewButtonEnter, + OnMouseLeave = self.onTreeViewButtonLeave, + }, { + TreeViewImageLabel = Roact.createElement("ImageLabel", { + AnchorPoint = Vector2.new(0.5, 0.5), + Position = UDim2.new(0.5, 0, 0.5, 0), + Size = UDim2.new(0, 16, 0, 16), + + Image = buttonsTheme.hierarchy, + BackgroundTransparency = 1, + }) + }), + }) + end) +end + +return MainViewButtons \ No newline at end of file diff --git a/BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Components/Preview/TreeViewButton.spec.lua b/BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Components/Preview/TreeViewButton.spec.lua new file mode 100644 index 0000000000..4906ad5a24 --- /dev/null +++ b/BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Components/Preview/TreeViewButton.spec.lua @@ -0,0 +1,26 @@ +return function() + local Library = script.Parent.Parent.Parent + local Roact = require(Library.Parent.Parent.Roact) + + local MockWrapper = require(Library.MockWrapper) + + local TreeViewButton = require(Library.Components.Preview.TreeViewButton) + + local function createTestAsset(container, name) + local element = Roact.createElement(MockWrapper, {}, { + Roact.createElement(TreeViewButton, { + Position = UDim2.new(1, 0, 1, 0), + + ShowTreeView = false, + OnTreeviewStatusToggle = nil, + }) + }) + + return Roact.mount(element, container or nil, name or "") + end + + it("should create and destroy without errors", function() + local instance = createTestAsset() + Roact.unmount(instance) + end) +end \ No newline at end of file diff --git a/BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Components/Preview/VideoPreview.lua b/BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Components/Preview/VideoPreview.lua new file mode 100644 index 0000000000..c27692db58 --- /dev/null +++ b/BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Components/Preview/VideoPreview.lua @@ -0,0 +1,271 @@ +--[[ + Provides logic for a VideoFrame and it's play/pause button, progress bar, and a time label. + + Required Props: + string VideoId: the Content string for VideoFrames. Should be formatted as a Content string "rbxassetid://123456". + boolean ShowTreeView: whether or not to show the TreeView button. It is used to + adjust the position of the time label and progress bar. + + Optional Props: + UDim2 LayoutOrder: The LayoutOrder of the component + UDim2 Position: The Position of the component + UDim2 Size: The Size of the component + callback OnPlay: Optional analytics call when clicking the play button + callback OnPause: Optional analytics call when clicking the pause button + + Props automatically received from wrapDraggableMedia(): + callback OnSliderInputChanged: Called when the progressbar slider input is changed. + callback OnSliderInputEnded: Called when the progressbar slider input ends. + + Props automatically received from wrapMedia(): + number _CurrentTime: The time in seconds that the media's TimePosition should currently be. + boolean _IsPlaying: Whether or not the Sound or VideoFrame should be currently playing. + callback _MediaPlayingUpdateSignal: Called when the media's Changed event is fired. Sets the isPlaying state. + callback _OnMediaEnded: Called when the media's Ended event is fired. Resets the currentTime & stops playing. + callback _Pause: Called when clicking the pause button. + callback _Play: Called when clicking the play button. + callBack _SetCurrentTime: Called if the currentTime has been changed, such as when moving a progressbar slider. +]] +local Library = script.Parent.Parent.Parent +local Roact = require(Library.Parent.Parent.Roact) + +local Theming = require(Library.Theming) +local withTheme = Theming.withTheme + +local wrapDraggableMedia = require(script.Parent.wrapDraggableMedia) + +local MediaControl = require(Library.Components.Preview.MediaControl) +local MediaProgressBar = require(Library.Components.Preview.MediaProgressBar) + +local PROGRESS_BAR_TOTAL_HEIGHT = 30 +local AUDIO_CONTROL_HEIGHT = 35 +local ICON_SIZE = 30 + +local VideoPreview = Roact.PureComponent:extend("VideoPreview") + +VideoPreview.defaultProps = { + Size = UDim2.new(1, 0, 1, 0), +} + +function VideoPreview:init() + self.layoutRef = Roact.createRef() + self.videoRef = Roact.createRef() + self.videoContainerRef = Roact.createRef() + + self.state = { + timeLength = 0, + isLoaded = false, + showOverlayPlayIcon = true, + resolution = Vector2.new(4, 3), + } + + self.dispatchMediaPlayingUpdate = function(updateType) + local videoObj = self.videoRef.current + if videoObj then + if updateType == "PLAY" then + videoObj:Play() + if self.props._OnPlay then + self.props._OnPlay() + end + elseif updateType == "PAUSE" then + videoObj:Pause() + if self.props._OnPause then + self.props._OnPause() + end + elseif updateType == "END" then + videoObj.Playing = false + videoObj.TimePosition = 0 + end + end + end + + self.onVideoPropertyChanged = function(rbx, property) + local videoObj = self.videoRef.current + if not videoObj or not self.isMounted then + return + end + if property == "TimeLength" then + self:setState({ + isLoaded = videoObj.IsLoaded, + timeLength = videoObj.TimeLength, + }) + self.props._SetTimeLength(videoObj.TimeLength) + elseif videoObj.IsLoaded ~= self.state.isLoaded then + self:setState({ + isLoaded = videoObj.IsLoaded, + }) + elseif property == "Resolution" then + self:setState({ + resolution = videoObj.Resolution, + }) + self.onResize() + end + end + + self.onResize = function() + local currentLayout = self.layoutRef.current + local videoFrame = self.videoRef.current + local videoContainer = self.videoContainerRef.current + if not videoFrame or not currentLayout or not videoContainer then + return + end + + local resolution = self.state.resolution + local height = videoContainer.AbsoluteSize.Y + local width = height * resolution.X / resolution.Y + if (currentLayout.AbsoluteContentSize.X < width) then + width = currentLayout.AbsoluteContentSize.X + height = width * resolution.Y / resolution.X + end + videoFrame.Size = UDim2.new(UDim.new(0, width), UDim.new(0, height)) + end + + self.onSliderInputChanged = function(newValue) + local videoFrame = self.videoRef.current + videoFrame.TimePosition = newValue or 0 + videoFrame.Playing = false + self:setState({ + showOverlayPlayIcon = false, + }) + + self.props._OnSliderInputChanged(newValue) + end + + self.onSliderInputEnded = function() + local videoFrameObj = self.videoRef.current + videoFrameObj.Playing = self.props._IsPlaying + self:setState({ + showOverlayPlayIcon = true, + }) + + self.props._OnSliderInputEnded() + end + + self.togglePlay = function() + if self.props._IsPlaying then + self.props._Pause() + else + self.props._Play() + end + end +end + +function VideoPreview:didMount() + self.isMounted = true + self.onResize() + self.mediaPlayingUpdateConnection = self.props._MediaPlayingUpdateSignal:connect(self.dispatchMediaPlayingUpdate) +end + +function VideoPreview:willUnmount() + self.isMounted = false + if self.mediaPlayingUpdateConnection then + self.mediaPlayingUpdateConnection:disconnect() + self.mediaPlayingUpdateConnection = nil + end +end + +function VideoPreview:render() + return withTheme(function(theme) + local VideoPreviewTheme = theme.assetPreview.videoPreview + + local props = self.props + local state = self.state + + local isLoaded = state.isLoaded + local timeLength = state.timeLength + local showOverlayPlayIcon = state.showOverlayPlayIcon + + local layoutOrder = props.LayoutOrder + local position = props.Position + local size = props.Size + local showTreeView = props.ShowTreeView + local videoId = props.VideoId + + -- Props passed from wrapDraggableMedia() and wrapMedia() + local currentTime = props._CurrentTime + local isPlaying = props._IsPlaying + local onMediaEnded = props._OnMediaEnded + local pause = props._Pause + local play = props._Play + + return Roact.createElement("Frame", { + BackgroundColor3 = VideoPreviewTheme.backgroundColor, + BackgroundTransparency = 0, + BorderSizePixel = 0, + LayoutOrder = layoutOrder, + Position = position, + Size = size, + }, { + UIListLayout = Roact.createElement("UIListLayout", { + FillDirection = Enum.FillDirection.Vertical, + SortOrder = Enum.SortOrder.LayoutOrder, + VerticalAlignment = Enum.VerticalAlignment.Center, + Padding = UDim.new(0, 10), + + [Roact.Change.AbsoluteContentSize] = self.onResize, + [Roact.Ref] = self.layoutRef, + }), + + VideoFrameButton = Roact.createElement("TextButton", { + AutoButtonColor = false, + BackgroundTransparency = 1, + BackgroundColor3 = VideoPreviewTheme.videoBackgroundColor, + LayoutOrder = 1, + Size = UDim2.new(1, 0, 1, -PROGRESS_BAR_TOTAL_HEIGHT - AUDIO_CONTROL_HEIGHT), + Text = "", + + [Roact.Ref] = self.videoContainerRef, + [Roact.Event.Activated] = self.togglePlay, + }, { + VideoFrameObj = Roact.createElement("VideoFrame", { + AnchorPoint = Vector2.new(0.5, 0), + BackgroundTransparency = 1, + Looped = false, + Position = UDim2.new(0.5, 0, 0, 0), + Size = UDim2.new(1, 0, 1, 0), + Video = videoId, + + [Roact.Ref] = self.videoRef, + [Roact.Event.Changed] = self.onVideoPropertyChanged, + [Roact.Event.Ended] = onMediaEnded, + }, { + PauseOverlay = (not isPlaying) and Roact.createElement("Frame", { + BackgroundColor3 = VideoPreviewTheme.pauseOverlayColor, + BackgroundTransparency = VideoPreviewTheme.pauseOverlayTransparency, + Size = UDim2.new(1, 0, 1, 0), + }, { + PlayVideoIcon = showOverlayPlayIcon and Roact.createElement("ImageLabel", { + AnchorPoint = Vector2.new(0.5, 0.5), + BackgroundTransparency = 1, + Image = VideoPreviewTheme.playButton, + Position = UDim2.new(0.5, 0, 0.5, 0), + Size = UDim2.new(0, ICON_SIZE, 0, ICON_SIZE), + }), + }), + }), + }), + + ProgressBar = Roact.createElement(MediaProgressBar, { + CurrentValue = currentTime, + LayoutOrder = 2, + Min = 0, + Max = timeLength, + OnValuesChanged = self.onSliderInputChanged, + OnInputEnded = self.onSliderInputEnded, + }), + + VideoControl = Roact.createElement(MediaControl, { + LayoutOrder = 3, + ShowTreeView = showTreeView, + IsPlaying = isPlaying, + IsLoaded = isLoaded, + OnPause = pause, + OnPlay = play, + TimeLength = timeLength, + TimePassed = currentTime, + }), + }) + end) +end + +return wrapDraggableMedia(VideoPreview) \ No newline at end of file diff --git a/BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Components/Preview/VideoPreview.spec.lua b/BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Components/Preview/VideoPreview.spec.lua new file mode 100644 index 0000000000..3878a7c0e5 --- /dev/null +++ b/BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Components/Preview/VideoPreview.spec.lua @@ -0,0 +1,26 @@ +return function() + local Library = script.Parent.Parent.Parent + local Roact = require(Library.Parent.Parent.Roact) + + local MockWrapper = require(Library.MockWrapper) + + local VideoPreview = require(Library.Components.Preview.VideoPreview) + + local function createTestAsset(container, name) + local element = Roact.createElement(MockWrapper, {}, { + Roact.createElement(VideoPreview, { + VideoId = 123, + ShowTreeView = false, + OnPlay = function() end, + OnPause = function() end, + }) + }) + + return Roact.mount(element, container or nil, name or "") + end + + it("should create and destroy without errors", function() + local instance = createTestAsset() + Roact.unmount(instance) + end) +end \ No newline at end of file diff --git a/BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Components/Preview/Vote.lua b/BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Components/Preview/Vote.lua new file mode 100644 index 0000000000..2f05a24b7b --- /dev/null +++ b/BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Components/Preview/Vote.lua @@ -0,0 +1,272 @@ +--[[ + This is the Vote component for AssetPreview component. + + Necessary properties: + Voting = table, a table contains the voting data + AssetId = num + OnVoteUpButtonActivated = callback, for the behavior when the Vote Up Button is clicked. + OnVoteDownButtonActivated = callback, for the behavior when the Vote Down Button is clicked. + + Optionlal properties: + Size = UDim2, + Position = UDim2, + layoutOrder = num +]] + +local Library = script.Parent.Parent.Parent + +local Roact = require(Library.Parent.Parent.Roact) + +local RoundButton = require(Library.Components.RoundFrame) +local RoundFrame = require(Library.Components.RoundFrame) + +local Theming = require(Library.Theming) +local withTheme = Theming.withTheme + +local Vote = Roact.PureComponent:extend("Vote") + +local INLINE_PADDING = 6 + +local BUTTON_STATUS = { + default = 0, + hovered = 1, +} + +function Vote:init(props) + self.state = { + voteUpStatus = BUTTON_STATUS.default, + voteDownStatus = BUTTON_STATUS.default, + } + + self.onVoteUpButtonActivated = function() + self.props.OnVoteUpButtonActivated(self.props.AssetId, self.props.Voting) + end + + self.onVoteDownButtonActivated = function() + self.props.OnVoteDownButtonActivated(self.props.AssetId, self.props.Voting) + end + + self.onVoteUpEnter = function() + self:setState({ + voteUpStatus = BUTTON_STATUS.hovered + }) + end + + self.onVoteUpLeave = function() + self:setState({ + voteUpStatus = BUTTON_STATUS.default + }) + end + + self.onVoteDownEnter = function() + self:setState({ + voteDownStatus = BUTTON_STATUS.hovered + }) + end + + self.onVoteDownLeave = function() + self:setState({ + voteDownStatus = BUTTON_STATUS.default + }) + end +end + +function Vote:render() + return withTheme(function(theme) + local props = self.props + local state = self.state + + local position = props.Position or UDim2.new(1, 0, 1, 0) + local size = props.Size or UDim2.new(0, 100, 0, 20) + + local voting = props.Voting + local canVote = voting.CanVote + local hasVoted = voting.HasVoted + local userVote = voting.UserVote + local upVotes = voting.UpVotes or 0 + local downVotes = voting.DownVotes or 0 + local totalVotes = 0 + local upVoteRate = 0 + + local voteTheme = theme.assetPreview.vote + + local voteUpBGColor = voteTheme.button.backgroundColor + local voteDownBGColor = voteTheme.button.backgroundColor + local voteUpBGTransparancy = voteTheme.button.backgroundTrans + local voteDownBGTransparancy = voteTheme.button.backgroundTrans + local voteUpStatus = state.voteUpStatus + local voteDownStatus = state.voteDownStatus + if not canVote then + voteUpBGColor = voteTheme.button.disabledColor + voteDownBGColor = voteTheme.button.disabledColor + else + if voteUpStatus == BUTTON_STATUS.hovered then + voteUpBGTransparancy = voteUpBGTransparancy + 0.3 + end + if voteDownStatus == BUTTON_STATUS.hovered then + voteDownBGTransparancy = voteDownBGTransparancy + 0.3 + end + + if hasVoted then + if userVote then + voteUpBGColor = voteTheme.voteUp.backgroundColor + else + voteDownBGColor = voteTheme.voteDown.backgroundColor + end + end + + totalVotes = upVotes + downVotes + if totalVotes > 0 then + upVoteRate = (upVotes / totalVotes) * 100 + end + end + + local layoutOrder = props.LayoutOrder + + return Roact.createElement(RoundFrame, { + Position = position, + Size = size, + + BackgroundTransparency = voteTheme.backgroundTrans, + BackgroundColor3 = voteTheme.background, + BorderColor3 = voteTheme.boderColor, + + LayoutOrder = layoutOrder, + },{ + VoteButtons = Roact.createElement("Frame", { + Position = UDim2.new(1, -64, 0, 0), + Size = UDim2.new(0, 64, 1, 0), + + BackgroundTransparency = 1, + BorderSizePixel = 0, + }, { + UIPadding = Roact.createElement("UIPadding", { + PaddingBottom = UDim.new(0, 0), + PaddingLeft = UDim.new(0, 0), + PaddingRight = UDim.new(0, INLINE_PADDING), + PaddingTop = UDim.new(0, 0), + }), + + UIListLayout = Roact.createElement("UIListLayout", { + FillDirection = Enum.FillDirection.Horizontal, + HorizontalAlignment = Enum.HorizontalAlignment.Right, + VerticalAlignment = Enum.VerticalAlignment.Center, + + SortOrder = Enum.SortOrder.LayoutOrder, + Padding = UDim.new(0, INLINE_PADDING), + }), + + + VoteDownButton = Roact.createElement(RoundButton, { + Active = canVote, + Size = UDim2.new(0, 28, 0, 28), + + BackgroundTransparency = voteDownBGTransparancy, + BackgroundColor3 = voteDownBGColor, + BorderColor3 = voteTheme.voteDown.borderColor, + + LayoutOrder = 1, + AutoButtonColor = false, + + OnActivated = self.onVoteDownButtonActivated, + OnMouseEnter = self.onVoteDownEnter, + OnMouseLeave = self.onVoteDownLeave, + }, { + VoteDownImageLabel = Roact.createElement("ImageLabel", { + Size = UDim2.new(1, 0, 1, 0), + + Image = voteTheme.images.voteDown, + BackgroundTransparency = 1, + }) + }), + + VoteUpButton = Roact.createElement(RoundButton, { + Active = canVote, + + Size = UDim2.new(0, 28, 0, 28), + + BackgroundTransparency = voteUpBGTransparancy, + BackgroundColor3 = voteUpBGColor, + BorderColor3 = voteTheme.voteUp.borderColor, + + LayoutOrder = 2, + AutoButtonColor = false, + + OnActivated = self.onVoteUpButtonActivated, + OnMouseEnter = self.onVoteUpEnter, + OnMouseLeave = self.onVoteUpLeave, + }, { + VoteUpImageLabel = Roact.createElement("ImageLabel", { + Size = UDim2.new(1, 0, 1, 0), + + Image = voteTheme.images.voteUp, + BackgroundTransparency = 1, + }) + }), + }), + + VoteInformations = Roact.createElement("Frame", { + Position = UDim2.new(0, INLINE_PADDING, 0, 0), + Size = UDim2.new(1, -64, 1, 0), + + BackgroundTransparency = 1, + BorderSizePixel = 0, + }, { + UIPadding = Roact.createElement("UIPadding", { + PaddingLeft = UDim.new(0, 6), + }), + + UIListLayout = Roact.createElement("UIListLayout", { + FillDirection = Enum.FillDirection.Horizontal, + HorizontalAlignment = Enum.HorizontalAlignment.Left, + VerticalAlignment = Enum.VerticalAlignment.Center, + + SortOrder = Enum.SortOrder.LayoutOrder, + Padding = UDim.new(0, 8), + }), + + VoteIcon = Roact.createElement("ImageLabel", { + Size = UDim2.new(0, 17, 0, 20), + BackgroundTransparency = 1, + Image = voteTheme.images.thumbUp, + }), + + VoteRatio = Roact.createElement("TextLabel", { + Size = UDim2.new(0, 28, 1, 0), + + TextXAlignment = Enum.TextXAlignment.Left, + TextYAlignment = Enum.TextYAlignment.Center, + + BackgroundTransparency = 1, + BorderSizePixel = 0, + + Text = ("%d%%"):format(upVoteRate), + TextSize = theme.assetPreview.textSizeLarge, + Font = theme.assetPreview.font, + TextColor3 = voteTheme.textColor, + + LayoutOrder = 1, + }), + + TotalVotes = Roact.createElement("TextLabel", { + Size = UDim2.new(1, 0, 1, 0), + + TextXAlignment = Enum.TextXAlignment.Left, + TextYAlignment = Enum.TextYAlignment.Center, + + BackgroundTransparency = 1, + BorderSizePixel = 0, + + Text = ("%d VOTES"):format(tostring(totalVotes)), + TextSize = theme.assetPreview.textSize, + Font = theme.assetPreview.font, + TextColor3 = voteTheme.subTextColor, + + LayoutOrder = 2, + }), + }), + }) + end) +end + +return Vote \ No newline at end of file diff --git a/BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Components/Preview/Vote.spec.lua b/BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Components/Preview/Vote.spec.lua new file mode 100644 index 0000000000..3c6cf34a54 --- /dev/null +++ b/BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Components/Preview/Vote.spec.lua @@ -0,0 +1,32 @@ +return function() + local Library = script.Parent.Parent.Parent + local Roact = require(Library.Parent.Parent.Roact) + + local MockWrapper = require(Library.MockWrapper) + + local Vote = require(Library.Components.Preview.Vote) + + local function createTestAsset(container, name) + local voting = { + CanVote = false, + HasVoted = false, + UserVote = false, + } + + local element = Roact.createElement(MockWrapper, {}, { + Vote = Roact.createElement(Vote, { + Position = UDim2.new(1, 0, 1, 0), + Size = UDim2.new(0, 100, 0, 20), + AssetId = 183435411, + Voting = voting, + }) + }) + + return Roact.mount(element, container or nil, name or "") + end + + it("should create and destroy without errors", function() + local instance = createTestAsset() + Roact.unmount(instance) + end) +end \ No newline at end of file diff --git a/BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Components/Preview/wrapDraggableMedia.lua b/BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Components/Preview/wrapDraggableMedia.lua new file mode 100644 index 0000000000..2d4cf2dc27 --- /dev/null +++ b/BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Components/Preview/wrapDraggableMedia.lua @@ -0,0 +1,67 @@ +--[[ + Wraps a component with logic required for a media's time interactable progressbar, play/pause button, + and time countdown. The wrapped component passes all props that wrapMedia in addition to interactable slider logic. + + Props automatically received from wrapMedia(): + boolean IsPlaying: Whether or not the Sound or VideoFrame is currently playing. + callback Pause: Called when clicking the pause button. + callback Play: Called when clicking the play button. + callBack SetCurrentTime: Called if the currentTime has been changed, such as when moving a progressbar slider. + + Returns: + callback _OnSliderInputChanged: Called when the progressbar slider input is changed. + callback _OnSliderInputEnded: Called when the progressbar slider input ends. + + number _CurrentTime: The time in seconds that the media's TimePosition should currently be. + boolean _IsPlaying: Whether or not the Sound or VideoFrame should be currently playing. + callback _MediaPlayingUpdateSignal: Called when the media's Changed event is fired. Sets the isPlaying state. + callback _OnMediaEnded: Called when the media's Ended event is fired. Resets the currentTime & stops playing. + callback _Pause: Called when clicking the pause button. + callback _Play: Called when clicking the play button. + callBack _SetCurrentTime: Called if the currentTime has been changed, such as when moving a progressbar slider. +]] +local Library = script.Parent.Parent.Parent +local Roact = require(Library.Parent.Parent.Roact) +local wrapMedia = require(script.Parent.wrapMedia) +local Immutable = require(Library.Utils.Immutable) + +local function wrapDraggableMedia(wrappedComponent) + local componentName = wrappedComponent and wrappedComponent.component and tostring(wrappedComponent.component) or "" + local DraggableMediaWrapper = Roact.PureComponent:extend(("DraggableMediaWrapper(%s)"):format(componentName)) + + function DraggableMediaWrapper:init() + self.isPlayingBeforeDrag = nil + + self.onSliderInputChanged = function(newValue) + local isPlaying = self.props._IsPlaying + if self.isPlayingBeforeDrag == nil then + self.isPlayingBeforeDrag = isPlaying + end + + if isPlaying then + self.props._Pause() + end + + self.props._SetCurrentTime(newValue) + end + + self.onSliderInputEnded = function() + if self.isPlayingBeforeDrag then + self.props._Play() + end + self.isPlayingBeforeDrag = nil + end + end + + function DraggableMediaWrapper:render() + local props = Immutable.JoinDictionaries(self.props, { + _OnSliderInputChanged = self.onSliderInputChanged, + _OnSliderInputEnded = self.onSliderInputEnded, + }) + return Roact.createElement(wrappedComponent, props) + end + + return wrapMedia(DraggableMediaWrapper) +end + +return wrapDraggableMedia \ No newline at end of file diff --git a/BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Components/Preview/wrapDraggableMedia.spec.lua b/BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Components/Preview/wrapDraggableMedia.spec.lua new file mode 100644 index 0000000000..af394c3897 --- /dev/null +++ b/BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Components/Preview/wrapDraggableMedia.spec.lua @@ -0,0 +1,38 @@ +return function() + local Library = script.Parent.Parent.Parent + local Roact = require(Library.Parent.Parent.Roact) + + local wrapDraggableMedia = require(script.Parent.wrapDraggableMedia) + + local function createTestComponent() + local TEST_WRAPPER = Roact.PureComponent:extend("TEST_WRAPPER") + function TEST_WRAPPER:render() + return Roact.createElement("Frame") + end + return TEST_WRAPPER + end + + it("should create and destroy without errors", function() + local element = wrapDraggableMedia(createTestComponent()) + local component = Roact.createElement(element, {}, {}) + local instance = Roact.mount(component) + + Roact.unmount(instance) + end) + + it("should add props into the component parameter", function() + local hasNewProps + local testComponent = createTestComponent() + function testComponent:init() + hasNewProps = (self.props._OnSliderInputChanged ~= nil) + hasNewProps = hasNewProps and (self.props._OnSliderInputEnded ~= nil) + end + + local element = wrapDraggableMedia(testComponent) + local component = Roact.createElement(element, {}, {}) + local instance = Roact.mount(component) + + Roact.unmount(instance) + expect(hasNewProps).to.be.equal(true) + end) +end \ No newline at end of file diff --git a/BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Components/Preview/wrapMedia.lua b/BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Components/Preview/wrapMedia.lua new file mode 100644 index 0000000000..78426ecd6c --- /dev/null +++ b/BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Components/Preview/wrapMedia.lua @@ -0,0 +1,116 @@ +--[[ + Wraps a component with logic required for a media's time progressbar, play/pause button, and time countdown. + + Returns: + number _CurrentTime: The time in seconds that the media's TimePosition should currently be. + boolean _IsPlaying: Whether or not the Sound or VideoFrame should be currently playing. + callback _MediaPlayingUpdateSignal: Should be called when the media's Changed event is fired. Sets the isPlaying state. + callback _OnMediaEnded: Should be called when the media's Ended event is fired. Resets the currentTime & stops playing. + callback _Pause: Should be called when clicking the pause button. + callback _Play: Should be called when clicking the play button. + callBack _SetCurrentTime: Should be called if the currentTime has been changed, such as when moving a progressbar slider. + callBack _SetTimeLength: Should be called if the timeLnegth has been changed, such as when a new audio or video is loaded. +]] +local RunService = game:GetService("RunService") + +local Library = script.Parent.Parent.Parent +local Roact = require(Library.Parent.Parent.Roact) + +local Signal = require(Library.Utils.Signal) +local Immutable = require(Library.Utils.Immutable) + +local function wrapMedia(wrappedComponent) + local MediaWrapper = Roact.PureComponent:extend(("MediaWrapper(%s)"):format(tostring(wrappedComponent))) + + function MediaWrapper:init() + self.state = { + currentTime = 0, + isPlaying = false, + timeLength = 0, + } + + self.mediaPlayingUpdateSignal = Signal.new() + + self.play = function() + self.mediaPlayingUpdateSignal:fire("PLAY") + self:setState({ + isPlaying = true, + }) + end + + self.pause = function() + self.mediaPlayingUpdateSignal:fire("PAUSE") + self:setState({ + isPlaying = false, + }) + end + + self.onMediaEnded = function() + self:setState({ + currentTime = 0, + isPlaying = false, + }) + end + + self.setCurrentTime = function(currentTime) + self:setState({ + currentTime = currentTime, + }) + end + + self.setTimeLength = function(timeLength) + self:setState({ + timeLength = timeLength, + }) + end + + self.onRenderStepped = function(deltaTime) + if not self.isMounted or not self.state.isPlaying then + return + end + + local newTime = self.state.currentTime + deltaTime + + if newTime >= self.state.timeLength then + self.onMediaEnded() + self.mediaPlayingUpdateSignal:fire("END") + else + self:setState({ + currentTime = newTime, + }) + end + end + end + + function MediaWrapper:didMount() + self.isMounted = true + self.runServiceConnection = RunService.RenderStepped:Connect(self.onRenderStepped) + end + + function MediaWrapper:willUnmount() + self.isMounted = false + if self.runServiceConnection then + self.runServiceConnection:Disconnect() + self.runServiceConnection = nil + end + end + + function MediaWrapper:render() + local props = Immutable.JoinDictionaries(self.props, { + _CurrentTime = self.state.currentTime, + _IsPlaying = self.state.isPlaying, + _MediaPlayingUpdateSignal = self.mediaPlayingUpdateSignal, + _OnMediaEnded = self.onMediaEnded, + _Play = self.play, + _Pause = self.pause, + _SetCurrentTime = self.setCurrentTime, + _SetTimeLength = self.setTimeLength, + }) + + return Roact.createElement(wrappedComponent, props) + end + + return MediaWrapper +end + +return wrapMedia \ No newline at end of file diff --git a/BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Components/Preview/wrapMedia.spec.lua b/BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Components/Preview/wrapMedia.spec.lua new file mode 100644 index 0000000000..374ec29804 --- /dev/null +++ b/BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Components/Preview/wrapMedia.spec.lua @@ -0,0 +1,49 @@ +return function() + local Library = script.Parent.Parent.Parent + local Roact = require(Library.Parent.Parent.Roact) + + local wrapMedia = require(script.Parent.wrapMedia) + + local function createTestComponent() + local TEST_WRAPPER = Roact.PureComponent:extend("TEST_WRAPPER") + function TEST_WRAPPER:render() + return Roact.createElement("Frame") + end + return TEST_WRAPPER + end + + it("should create and destroy without errors", function() + local element = wrapMedia(createTestComponent()) + local component = Roact.createElement(element, {}, {}) + local instance = Roact.mount(component) + + Roact.unmount(instance) + end) + + it("should add props into the component parameter", function() + local hasNewProps + local testComponent = createTestComponent() + function testComponent:init() + local expectedProps = { + "_CurrentTime", + "_IsPlaying", + "_MediaPlayingUpdateSignal", + "_OnMediaEnded", + "_Pause", + "_Play", + "_SetCurrentTime", + } + hasNewProps = (self.props._CurrentTime ~= nil) + for _,value in pairs(expectedProps) do + hasNewProps = hasNewProps and (self.props[value] ~= nil) + end + end + + local element = wrapMedia(testComponent) + local component = Roact.createElement(element, {}, {}) + local instance = Roact.mount(component) + + Roact.unmount(instance) + expect(hasNewProps).to.be.equal(true) + end) +end \ No newline at end of file diff --git a/BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Components/RadioButtons.lua b/BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Components/RadioButtons.lua new file mode 100644 index 0000000000..bb2cfa5907 --- /dev/null +++ b/BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Components/RadioButtons.lua @@ -0,0 +1,140 @@ +--[[ + A set of an arbitrary number of Radio buttons. Automatically scales to fit + the number of buttons contained in this component. + + Props: + Table Buttons: A table of buttons to use. + Format: {{Key = "Key1", Text = "Text1"}, ...} + string Selected = The current button that is selected. + Enum.FillDirection FillDirection = if the buttons should be in a vertical or horizontal layout + int LayoutOrder = The layout order of the frame, if in a Layout. + + function onButtonClicked(string key) = A callback for when a user selects a button. +]] + +local NO_WRAP = Vector2.new(1000000, 50) +local BUTTON_HEIGHT_SCALE = 0.4 + +local TextService = game:GetService("TextService") + +local Library = script.Parent.Parent + +local Roact = require(Library.Parent.Parent.Roact) + +local createFitToContent = require(Library.Components.createFitToContent) + +local Theming = require(Library.Theming) +local withTheme = Theming.withTheme + +local RadioButtons = Roact.PureComponent:extend("RadioButtons") + +function RadioButtons:init() + self.layoutRef = Roact.createRef() + self.containerRef = Roact.createRef() + self.currentLayout = 0 + + self.onButtonClicked = function(key, index) + if self.props.onButtonClicked then + self.props.onButtonClicked(key, index) + end + end +end + +function RadioButtons:createButton(key, text, index, selected, theme) + local textWidth = TextService:GetTextSize(text, theme.radioButton.textSize, theme.radioButton.font, NO_WRAP).X + local buttonHeight = theme.radioButton.buttonHeight + + local buttonSize = UDim2.new(1, 0, 0, buttonHeight) + if self.props.FillDirection == Enum.FillDirection.Horizontal then + buttonSize = UDim2.new(0, textWidth + buttonHeight, 0, buttonHeight) + end + + return Roact.createElement("Frame", { + LayoutOrder = self:nextLayout(), + BackgroundTransparency = 1, + Size = buttonSize, + }, { + UIListLayout = Roact.createElement("UIListLayout", { + FillDirection = Enum.FillDirection.Horizontal, + VerticalAlignment = Enum.VerticalAlignment.Center, + SortOrder = Enum.SortOrder.LayoutOrder, + Padding = UDim.new(0, theme.radioButton.buttonPadding), + }), + + Background = Roact.createElement("ImageButton", { + LayoutOrder = 1, + Size = UDim2.new(0, buttonHeight, 0, buttonHeight), + BackgroundTransparency = 1, + ImageColor3 = theme.radioButton.radioButtonColor, + Image = theme.radioButton.radioButtonBackground, + + [Roact.Event.Activated] = function() + self.onButtonClicked(key, index) + end, + }, { + Highlight = selected and Roact.createElement("ImageLabel", { + Size = UDim2.new(BUTTON_HEIGHT_SCALE, 0, BUTTON_HEIGHT_SCALE, 0), + AnchorPoint = Vector2.new(0.5, 0.5), + Position = UDim2.new(0.5, 0, 0.5, 0), + BackgroundTransparency = 1, + Image = theme.radioButton.radioButtonSelected, + }), + }), + + Text = Roact.createElement("TextButton", { + LayoutOrder = 2, + Text = text, + Size = UDim2.new(0, textWidth, 1, 0), + BackgroundTransparency = 1, + Font = theme.radioButton.font, + TextSize = theme.radioButton.textSize, + TextColor3 = theme.radioButton.textColor, + TextXAlignment = Enum.TextXAlignment.Left, + + [Roact.Event.Activated] = function() + self.onButtonClicked(key, index) + end, + }), + }) +end + +function RadioButtons:resetLayout() + self.currentLayout = 0 +end + +function RadioButtons:nextLayout() + self.currentLayout = self.currentLayout + 1 + return self.currentLayout +end + +function RadioButtons:render() + return withTheme(function(theme) + local props = self.props + + local buttons = props.Buttons + local layoutOrder = props.LayoutOrder + local selected = props.Selected + local fillDirection = props.FillDirection + + local fitToContent = createFitToContent("Frame", "UIListLayout", { + FillDirection = fillDirection or Enum.FillDirection.Vertical, + Padding = UDim.new(0, theme.radioButton.contentPadding), + SortOrder = Enum.SortOrder.LayoutOrder, + }) + + self:resetLayout() + + local children = {} + for index, button in ipairs(buttons) do + children[button.Key] = self:createButton(button.Key, button.Text, index, + selected == button.Key, theme) + end + + return Roact.createElement(fitToContent, { + BackgroundTransparency = 1, + LayoutOrder = layoutOrder + }, children) + end) +end + +return RadioButtons diff --git a/BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Components/RadioButtons.spec.lua b/BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Components/RadioButtons.spec.lua new file mode 100644 index 0000000000..eb1f95c71e --- /dev/null +++ b/BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Components/RadioButtons.spec.lua @@ -0,0 +1,47 @@ +return function() + local Library = script.Parent.Parent + local Roact = require(Library.Parent.Parent.Roact) + + local MockWrapper = require(Library.MockWrapper) + + local RadioButtons = require(script.Parent.RadioButtons) + + it("should create and destroy without errors", function() + local element = Roact.createElement(MockWrapper, {}, { + Buttons = Roact.createElement(RadioButtons, { + Buttons = { + {Key = "Button1", Text = "Button 1"}, + {Key = "Button2", Text = "Button 2"}, + }, + Selected = "Button1", + }), + }) + local instance = Roact.mount(element) + Roact.unmount(instance) + end) + + it("should render correctly", function() + local element = Roact.createElement(MockWrapper, {}, { + Buttons = Roact.createElement(RadioButtons, { + Buttons = { + {Key = "Button1", Text = "Button 1"}, + {Key = "Button2", Text = "Button 2"}, + }, + Selected = "Button1", + }), + }) + + local container = Instance.new("Folder") + local instance = Roact.mount(element, container, "Buttons") + + local buttons = container.Buttons + expect(buttons.Layout).to.be.ok() + expect(buttons.Button1).to.be.ok() + expect(buttons.Button1.UIListLayout).to.be.ok() + expect(buttons.Button1.Background).to.be.ok() + expect(buttons.Button1.Background.Highlight).to.be.ok() + expect(buttons.Button1.Text).to.be.ok() + + Roact.unmount(instance) + end) +end diff --git a/BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Components/RoundFrame.lua b/BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Components/RoundFrame.lua new file mode 100644 index 0000000000..b781c5ab08 --- /dev/null +++ b/BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Components/RoundFrame.lua @@ -0,0 +1,107 @@ +--[[ + An element with rounded corners. + Designed to function almost identically to a standard roblox Frame. + + Props: + Color3 BackgroundColor3 = The background color of the frame. + float BackgroundTransparency = The transparency of the frame's background and border. + Color3 BorderColor = The border color of the frame. + int BorderSizePixel: + If == 0, the border will not render. If ~= 0, the border will render. + + UDim2 Size = The size of the frame. + UDim2 Position = The position of the frame. + Vector2 AnchorPoint = The center point of this frame. + int LayoutOrder = The layout order of the frame, if in a Layout. + int ZIndex = The draw index of the frame. + + function OnActivated = A callback fired when the user clicks the frame. + function OnMouseEnter = A callback fired when the mouse enters the frame. + function OnMouseLeave = A callback fired when the mouse leaves the frame. + + [Roact.Change.AbsoluteSize] = An event that fires when the frame's AbsoluteSize changes + [Roact.Change.AbsolutePosition] = An event that fires when the frame's AbsolutePosition changes +]] + +local Library = script.Parent.Parent + +local Roact = require(Library.Parent.Parent.Roact) + +local Theming = require(Library.Theming) +local withTheme = Theming.withTheme + +local ROUNDED_FRAME_SLICE = Rect.new(3, 3, 13, 13) +local DEFAULT_BORDER_COLOR = Color3.fromRGB(27, 42, 53) +local DEFAULT_SIZE = UDim2.new(0, 100, 0, 100) + +local RoundFrame = Roact.PureComponent:extend("RoundFrame") + +function RoundFrame:init(initialProps) + local isButton = initialProps.OnActivated ~= nil + self.elementType = isButton and "ImageButton" or "ImageLabel" +end + +function RoundFrame:render() + return withTheme(function(theme) + local props = self.props + local roundFrameTheme = theme.roundFrame + + local backgroundColor = props.BackgroundColor3 + local backgroundTransparency = props.BackgroundTransparency + local borderColor = props.BorderColor3 or DEFAULT_BORDER_COLOR + local borderSize = props.BorderSizePixel or 1 + local size = props.Size or DEFAULT_SIZE + local position = props.Position + local anchorPoint = props.AnchorPoint + local layoutOrder = props.LayoutOrder + local zindex = props.ZIndex + local activatedCallback = props.OnActivated + local mouseEnterCallback = props.OnMouseEnter + local mouseLeaveCallback = props.OnMouseLeave + + local borderTransparency + if borderSize == 0 then + borderTransparency = 1 + else + borderTransparency = backgroundTransparency + end + + return Roact.createElement(self.elementType, { + Size = size, + Position = position, + AnchorPoint = anchorPoint, + LayoutOrder = layoutOrder, + ZIndex = zindex, + + BackgroundTransparency = 1, + ImageColor3 = backgroundColor, + ImageTransparency = backgroundTransparency, + + Image = roundFrameTheme.backgroundImage, + ScaleType = Enum.ScaleType.Slice, + SliceCenter = ROUNDED_FRAME_SLICE, + + [Roact.Event.MouseEnter] = mouseEnterCallback, + [Roact.Event.MouseLeave] = mouseLeaveCallback, + [Roact.Event.Activated] = activatedCallback, + + [Roact.Change.AbsoluteSize] = props[Roact.Change.AbsoluteSize], + [Roact.Change.AbsolutePosition] = props[Roact.Change.AbsolutePosition], + }, { + Border = Roact.createElement("ImageLabel", { + Size = UDim2.new(1, 0, 1, 0), + + BackgroundTransparency = 1, + ImageColor3 = borderColor, + ImageTransparency = borderTransparency, + + Image = roundFrameTheme.borderImage, + ScaleType = Enum.ScaleType.Slice, + SliceCenter = ROUNDED_FRAME_SLICE, + SliceScale = borderSize, + }, props[Roact.Children]) + }) + end) +end + +return RoundFrame diff --git a/BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Components/RoundFrame.spec.lua b/BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Components/RoundFrame.spec.lua new file mode 100644 index 0000000000..b7c5babed6 --- /dev/null +++ b/BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Components/RoundFrame.spec.lua @@ -0,0 +1,79 @@ +return function() + local Library = script.Parent.Parent + local Roact = require(Library.Parent.Parent.Roact) + + local MockWrapper = require(Library.MockWrapper) + + local RoundFrame = require(script.Parent.RoundFrame) + + local function createTestRoundFrame(props, children) + return Roact.createElement(MockWrapper, {}, { + RoundFrame = Roact.createElement(RoundFrame, props, children) + }) + end + + it("should create and destroy without errors", function() + local element = createTestRoundFrame() + local instance = Roact.mount(element) + Roact.unmount(instance) + end) + + it("should render correctly", function () + local container = Instance.new("Folder") + local instance = Roact.mount(createTestRoundFrame(), container) + local frame = container:FindFirstChildOfClass("ImageLabel") + + expect(frame).to.be.ok() + expect(frame.Border).to.be.ok() + + Roact.unmount(instance) + end) + + it("should hide its border if BorderSizePixel == 0", function () + local container = Instance.new("Folder") + local instance = Roact.mount(createTestRoundFrame({ + BorderSizePixel = 0, + }), container) + local frame = container:FindFirstChildOfClass("ImageLabel") + + expect(frame.Border.ImageTransparency).to.equal(1) + + Roact.unmount(instance) + end) + + it("should be an ImageLabel if OnActivated is undefined", function () + local container = Instance.new("Folder") + local instance = Roact.mount(createTestRoundFrame(), container) + local frame = container:FindFirstChildOfClass("ImageLabel") + + expect(frame:IsA("ImageLabel")).to.equal(true) + + Roact.unmount(instance) + end) + + it("should be an ImageButton if OnActivated is defined", function () + local container = Instance.new("Folder") + local instance = Roact.mount(createTestRoundFrame({ + OnActivated = function() + end, + }), container) + local frame = container:FindFirstChildOfClass("ImageButton") + + expect(frame:IsA("ImageButton")).to.equal(true) + + Roact.unmount(instance) + end) + + it("should accept children", function () + local container = Instance.new("Folder") + local instance = Roact.mount(createTestRoundFrame({}, { + Child = Roact.createElement("Frame"), + }), container) + local frame = container:FindFirstChildOfClass("ImageLabel") + + expect(frame.Border).to.be.ok() + expect(frame.Border.Child).to.be.ok() + + Roact.unmount(instance) + end) +end \ No newline at end of file diff --git a/BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Components/RoundTextBox.lua b/BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Components/RoundTextBox.lua new file mode 100644 index 0000000000..2150859fde --- /dev/null +++ b/BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Components/RoundTextBox.lua @@ -0,0 +1,194 @@ +--[[ + A TextBox with rounded corners that allows single-line or multiline entry, + maximum character count, and error messages. + + Props: + bool Active = Whether this component can be interacted with. + int MaxLength = The maximum number of characters allowed in the TextBox. + bool Multiline = Whether this TextBox allows a single line of text or multiple. + int Height = The vertical size of this TextBox, in pixels. + int WidthOffset = the horizontal offset size of this TextBox, in pixels. + int LayoutOrder = The sort order of this component in a UIListLayout. + int TextSize = The size of text + + boolean ErrorBorder = puts red border around text box + string ErrorMessage = A general override message used to display an error. A non-nil ErrorMessage will border the TextBox in red. + + string Text = The text to display in the TextBox + string PlaceholderText = text to display when TextBox is empty/in default state + boolean ShowToolTip = do we want to show anything beneath the rounded text box (defaults to true) + boolean ShowErrors = do we want to show any error text beneath the rounded text box, or change the border to indicate an error (defaults to true) + function SetText(text) = Callback to tell parent that text has changed + function FocusChanged(focused) = Callback when this TextBox is focused. + function HoverChanged(hovering) = Callback when the mouse enters or leaves this TextBox. +]] + +local StudioUILibraryRoundTextBoxNoTooltip = settings():GetFFlag("StudioUILibraryRoundTextBoxNoTooltip") + +local DEFAULT_HEIGHT = 42 +local PADDING = UDim.new(0, 10) + +local Library = script.Parent.Parent +local Roact = require(Library.Parent.Parent.Roact) +local Theming = require(Library.Theming) +local withTheme = Theming.withTheme + +local TextEntry = require(Library.Components.TextEntry) +local MultilineTextEntry = require(Library.Components.MultilineTextEntry) + +local RoundTextBox = Roact.PureComponent:extend("RoundTextBox") + +function RoundTextBox:init() + self.state = { + Focused = false, + } + + self.focusChanged = function(focused) + if self.props.Active then + if self.props.FocusChanged then + self.props.FocusChanged(focused) + end + self:setState({ + Focused = focused, + }) + end + end + + self.mouseHoverChanged = function(hovering) + if self.props.Active then + if self.state.Focused and self.props.HoverChanged then + self.props.HoverChanged(hovering) + end + end + end +end + +function RoundTextBox:render() + return withTheme(function(theme) + local active = self.props.Active + local focused = self.state.Focused + local multiline = self.props.Multiline + local textLength = utf8.len(self.props.Text) + local pastMaxLength = self.props.MaxLength and textLength > self.props.MaxLength + local errorState = self.props.ErrorMessage + or pastMaxLength + + if StudioUILibraryRoundTextBoxNoTooltip then + errorState = errorState or self.props.ErrorBorder + end + + local size = self.props.Size or UDim2.new(1, self.props.WidthOffset or 0, 0, self.props.Height or DEFAULT_HEIGHT) + + local backgroundProps = { + -- Necessary to make the rounded background + BackgroundTransparency = 1, + Image = theme.roundFrame.backgroundImage, + ImageTransparency = 0, + ImageColor3 = active and theme.textBox.background or theme.textBox.disabled, + ScaleType = Enum.ScaleType.Slice, + SliceCenter = theme.roundFrame.slice, + + Position = UDim2.new(0, 0, 0, 0), + Size = size, + + LayoutOrder = self.props.LayoutOrder or 1, + } + + local showToolTip = true + if StudioUILibraryRoundTextBoxNoTooltip then + if nil ~= self.props.ShowToolTip then + showToolTip = self.props.ShowToolTip + end + end + + local tooltipText + if active then + if StudioUILibraryRoundTextBoxNoTooltip then + if showToolTip then + if errorState and self.props.ErrorMessage then + tooltipText = self.props.ErrorMessage + else + tooltipText = textLength .. "/" .. self.props.MaxLength + end + else + tooltipText = "" + end + else + if errorState and self.props.ErrorMessage then + tooltipText = self.props.ErrorMessage + else + tooltipText = textLength .. "/" .. self.props.MaxLength + end + end + else + tooltipText = "" + end + + local borderColor + if active then + if errorState then + borderColor = theme.textBox.error + elseif focused then + borderColor = theme.textBox.borderHover + else + borderColor = theme.textBox.borderDefault + end + else + borderColor = theme.textBox.borderDefault + end + + local textEntryProps = { + Visible = self.props.Active, + Text = self.props.Text, + PlaceholderText = self.props.PlaceholderText, + FocusChanged = self.focusChanged, + HoverChanged = self.mouseHoverChanged, + SetText = self.props.SetText, + TextColor3 = theme.textBox.text, + Font = theme.textBox.font, + TextSize = self.props.TextSize, + } + + local textEntry + if multiline then + textEntry = Roact.createElement(MultilineTextEntry, textEntryProps) + else + textEntry = Roact.createElement(TextEntry, textEntryProps) + end + + return Roact.createElement("ImageLabel", backgroundProps, { + Tooltip = Roact.createElement("TextLabel", { + BackgroundTransparency = 1, + Position = UDim2.new(0, 2, 1, 2), + Size = UDim2.new(1, 0, 0, 10), + + Font = Enum.Font.SourceSans, + TextSize = 16, + TextXAlignment = Enum.TextXAlignment.Left, + TextYAlignment = Enum.TextYAlignment.Top, + TextColor3 = (active and errorState and theme.textBox.error) or theme.textBox.tooltip, + Text = tooltipText, + Visible = (not StudioUILibraryRoundTextBoxNoTooltip) or showToolTip + }), + + Border = Roact.createElement("ImageLabel", { + Size = UDim2.new(1, 0, 1, 0), + BackgroundTransparency = 1, + Image = theme.roundFrame.borderImage, + ImageColor3 = borderColor, + ScaleType = Enum.ScaleType.Slice, + SliceCenter = theme.roundFrame.slice, + }, { + Padding = Roact.createElement("UIPadding", { + PaddingLeft = PADDING, + PaddingRight = PADDING, + PaddingTop = PADDING, + PaddingBottom = PADDING, + }), + Text = textEntry, + }), + }) + end) +end + +return RoundTextBox diff --git a/BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Components/RoundTextBox.spec.lua b/BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Components/RoundTextBox.spec.lua new file mode 100644 index 0000000000..356cfa3221 --- /dev/null +++ b/BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Components/RoundTextBox.spec.lua @@ -0,0 +1,71 @@ +return function() + local Library = script.Parent.Parent + local Roact = require(Library.Parent.Parent.Roact) + + local MockWrapper = require(Library.MockWrapper) + + local RoundTextBox = require(script.Parent.RoundTextBox) + + local function createTestRoundTextBox(active, errorMessage) + return Roact.createElement(MockWrapper, {}, { + RoundTextbox = Roact.createElement(RoundTextBox, { + Active = active, + MaxLength = 50, + Multiline = false, + Text = "Text", + ErrorMessage = errorMessage, + }), + }) + end + + it("should create and destroy without errors", function() + local element = createTestRoundTextBox(true) + local instance = Roact.mount(element) + Roact.unmount(instance) + end) + + it("should render correctly", function () + local container = Instance.new("Folder") + local instance = Roact.mount(createTestRoundTextBox(true), container) + local background = container:FindFirstChildOfClass("ImageLabel") + + expect(background.Border).to.be.ok() + expect(background.Border.Padding).to.be.ok() + expect(background.Border.Text).to.be.ok() + expect(background.Tooltip).to.be.ok() + + Roact.unmount(instance) + end) + + describe("Tooltip", function() + it("should show the correct length of the text", function () + local container = Instance.new("Folder") + local instance = Roact.mount(createTestRoundTextBox(true), container) + local background = container:FindFirstChildOfClass("ImageLabel") + + expect(background.Tooltip.Text).to.equal("4/50") + + Roact.unmount(instance) + end) + + it("should show an error message if one exists", function () + local container = Instance.new("Folder") + local instance = Roact.mount(createTestRoundTextBox(true, "Error"), container) + local background = container:FindFirstChildOfClass("ImageLabel") + + expect(background.Tooltip.Text).to.equal("Error") + + Roact.unmount(instance) + end) + + it("should be empty if component is inactive", function () + local container = Instance.new("Folder") + local instance = Roact.mount(createTestRoundTextBox(false), container) + local background = container:FindFirstChildOfClass("ImageLabel") + + expect(background.Tooltip.Text).to.equal("") + + Roact.unmount(instance) + end) + end) +end \ No newline at end of file diff --git a/BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Components/RoundTextButton.lua b/BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Components/RoundTextButton.lua new file mode 100644 index 0000000000..ccc20fade6 --- /dev/null +++ b/BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Components/RoundTextButton.lua @@ -0,0 +1,118 @@ +--[[ + A button with rounded corners. + + Supports one of two styles: + "Blue": A blue button with white text and no border. + "White": A white button with black text and a black border. + + Props: + bool Active = Whether or not this button can be clicked. + UDim2 Size = UDim2.new(0, Constants.BUTTON_WIDTH, 0, Constants.BUTTON_HEIGHT) + int LayoutOrder = The order this RoundTextButton will sort to when placed in a UIListLayout. + string Name = The text to display in this Button. + function OnClicked = The function that will be called when this button is clicked. + variant Value = Data that can be accessed from the OnClicked callback. + int TextSize = The size of text + table Style = { + ButtonColor, + ButtonColor_Hover, + ButtonColor_Disabled, + TextColor, + TextColor_Disabled, + BorderColor, + } +]] + +local Library = script.Parent.Parent +local Roact = require(Library.Parent.Parent.Roact) +local Theming = require(Library.Theming) +local withTheme = Theming.withTheme + +local BUTTON_WIDTH = 125 +local BUTTON_HEIGHT = 35 + +local RoundTextButton = Roact.PureComponent:extend("RoundTextButton") + +function RoundTextButton:init() + self.state = { + Hovering = false, + } + + self.mouseEnter = function() + self:setState({ + Hovering = true, + }) + end + + self.mouseLeave = function() + self:setState({ + Hovering = false, + }) + end +end + +function RoundTextButton:render() + return withTheme(function(theme) + local active = self.props.Active + local hovering = self.state.Hovering + local style = self.props.Style + local match = self.props.BorderMatchesBackground + local textSize = self.props.TextSize + + local backgroundProps = { + -- Necessary to make the rounded background + BackgroundTransparency = 1, + Image = theme.roundFrame.backgroundImage, + ImageTransparency = 0, + ScaleType = Enum.ScaleType.Slice, + SliceCenter = theme.roundFrame.slice, + + Position = self.props.Position or UDim2.new(0, 0, 0, 0), + Size = self.props.Size or UDim2.new(0, BUTTON_WIDTH, 0, BUTTON_HEIGHT), + AnchorPoint = self.props.AnchorPoint or Vector2.new(0, 0), + + LayoutOrder = self.props.LayoutOrder or 1, + ZIndex = self.props.ZIndex or 1, + + [Roact.Event.MouseEnter] = self.mouseEnter, + [Roact.Event.MouseLeave] = self.mouseLeave, + + [Roact.Event.Activated] = function() + if active then + self.props.OnClicked(self.props.Value) + end + end, + } + + if active then + backgroundProps.ImageColor3 = hovering and style.ButtonColor_Hover or style.ButtonColor + else + backgroundProps.ImageColor3 = style.ButtonColor_Disabled + end + + return Roact.createElement("ImageButton", backgroundProps, { + Border = Roact.createElement("ImageLabel", { + Size = UDim2.new(1, 0, 1, 0), + BackgroundTransparency = 1, + Image = theme.roundFrame.borderImage, + ImageColor3 = match and backgroundProps.ImageColor3 or style.BorderColor, + ScaleType = Enum.ScaleType.Slice, + SliceCenter = theme.roundFrame.slice, + ZIndex = self.props.ZIndex or 1, + }), + + Text = Roact.createElement("TextLabel", { + Size = UDim2.new(1, 0, 1, 0), + BackgroundTransparency = 1, + BorderSizePixel = 0, + Font = theme.textButton.font, + TextColor3 = active and style.TextColor or style.TextColor_Disabled, + TextSize = textSize, + Text = self.props.Name, + ZIndex = self.props.ZIndex or 1, + }), + }) + end) +end + +return RoundTextButton diff --git a/BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Components/RoundTextButton.spec.lua b/BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Components/RoundTextButton.spec.lua new file mode 100644 index 0000000000..cdd2496bb8 --- /dev/null +++ b/BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Components/RoundTextButton.spec.lua @@ -0,0 +1,35 @@ +return function() + local Library = script.Parent.Parent + local Roact = require(Library.Parent.Parent.Roact) + + local MockWrapper = require(Library.MockWrapper) + + local RoundTextButton = require(script.Parent.RoundTextButton) + + local function createTestRoundTextButton() + return Roact.createElement(MockWrapper, {}, { + RoundTextButton = Roact.createElement(RoundTextButton, { + Active = true, + Style = {}, + Name = "Name", + }), + }) + end + + it("should create and destroy without errors", function() + local element = createTestRoundTextButton() + local instance = Roact.mount(element) + Roact.unmount(instance) + end) + + it("should render correctly", function () + local container = Instance.new("Folder") + local instance = Roact.mount(createTestRoundTextButton(), container) + local button = container:FindFirstChildOfClass("ImageButton") + + expect(button.Border).to.be.ok() + expect(button.Text).to.be.ok() + + Roact.unmount(instance) + end) +end \ No newline at end of file diff --git a/BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Components/SearchBar.lua b/BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Components/SearchBar.lua new file mode 100644 index 0000000000..5f2673e5ce --- /dev/null +++ b/BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Components/SearchBar.lua @@ -0,0 +1,483 @@ +--[[ + Search Bar Component + + Implements a search bar component with a text box that dynamically moves as you type, and searches after a delay. + + Props: + UDim2 Size: size of the searchBar + number LayoutOrder = 0 : optional layout order for UI layouts + number TextSearchDelay : optional delay when text changes before requesting search, in ms + string DefaultText : default text to show in the empty search bar. + bool Enabled : searchbar is enabled or not + bool Rounded : searchbar has rounded corners + bool EnableFocus : if the searchbar borders becomes dark when it is selected + + callback OnSearchRequested(string searchTerm) : callback for when the user presses the enter key + or clicks the search button or types if search is live +]] +local FFlagAssetManagerLuaCleanup1 = settings():GetFFlag("AssetManagerLuaCleanup1") +local FFlagUXImprovementAddSearchBar = settings():GetFFlag("UXImprovementAddSearchBar") + +local Library = script.Parent.Parent +local Roact = require(Library.Parent.Parent.Roact) +local Theming = require(Library.Theming) +local withTheme = Theming.withTheme + +local LayoutOrderIterator = require(Library.Utils.LayoutOrderIterator) + +local TextService = game:GetService("TextService") + +local TEXT_SEARCH_DELAY = 500 + +local SearchBar = Roact.PureComponent:extend("SearchBar") + +local RoundFrame = require(Library.Components.RoundFrame) + +local function stripSearchTerm(searchTerm) + return searchTerm and searchTerm:gsub("\n", " ") or "" +end + +function SearchBar:init() + self.state = { + text = "", + + isFocused = false, + isContainerHovered = false, + isClearButtonHovered = false, + } + + self.textBoxRef = Roact.createRef() + + self.requestSearch = function() + if self.props.Enabled then + self.props.OnSearchRequested(self.state.text) + end + end + + self.onContainerHovered = function() + if self.props.Enabled then + self:setState({ + isContainerHovered = true, + }) + end + end + + self.onContainerHoverEnded = function() + if self.props.Enabled then + self:setState({ + isContainerHovered = false, + }) + end + end + + self.onTextChanged = function(rbx) + if self.props.Enabled then + local text = stripSearchTerm(rbx.Text) + local textBox = self.textBoxRef.current + if FFlagUXImprovementAddSearchBar then + if self.state.text ~= text and textBox ~= nil then + self:setState({ + text = text, + }) + + local textSearchDelay = self.props.TextSearchDelay or TEXT_SEARCH_DELAY + delay(textSearchDelay / 1000, function() + self.requestSearch() + end) + + local textBound = TextService:GetTextSize(text, textBox.TextSize, textBox.Font, Vector2.new(math.huge, math.huge)) + if textBound.x > textBox.AbsoluteSize.x then + textBox.TextXAlignment = Enum.TextXAlignment.Right + else + textBox.TextXAlignment = Enum.TextXAlignment.Left + end + end + else + if self.state.text ~= text then + self:setState({ + text = text, + }) + + local textSearchDelay = self.props.TextSearchDelay or TEXT_SEARCH_DELAY + delay(textSearchDelay / 1000, function() + self.requestSearch() + end) + + local textBound = TextService:GetTextSize(text, textBox.TextSize, textBox.Font, Vector2.new(math.huge, math.huge)) + if textBound.x > textBox.AbsoluteSize.x then + textBox.TextXAlignment = Enum.TextXAlignment.Right + else + textBox.TextXAlignment = Enum.TextXAlignment.Left + end + end + end + end + end + + self.onTextBoxFocused = function(rbx) + if self.props.Enabled then + self:setState({ + isFocused = true, + }) + end + end + + self.onTextBoxFocusLost = function(rbx, enterPressed, inputObject) + if self.props.Enabled then + self:setState({ + isFocused = false, + isContainerHovered = false, + }) + end + end + + self.onClearButtonHovered = function() + if self.props.Enabled then + self:setState({ + isClearButtonHovered = true, + }) + end + end + + self.onClearButtonHoverEnded = function() + if self.props.Enabled then + self:setState({ + isClearButtonHovered = false, + }) + end + end + + self.onClearButtonClicked = function() + if self.props.Enabled then + local textBox = self.textBoxRef.current + self:setState({ + isFocused = true, + isClearButtonHovered = false, + }) + + textBox.Text = "" + self.props.OnSearchRequested("") + textBox:CaptureFocus() + textBox.TextXAlignment = Enum.TextXAlignment.Left + end + end +end + +function SearchBar:render() + return withTheme(function(theme) + local props = self.props + local state = self.state + + local size = props.Size + local layoutOrder = props.LayoutOrder or 0 + local defaultText = props.DefaultText + local enabled = props.Enabled + local focusDisabled = props.FocusDisabled + + local onSearchRequested = props.OnSearchRequested + + local rounded = props.Rounded + + assert(size ~= nil, "Searchbar requires a size.") + assert(onSearchRequested ~= nil and type(onSearchRequested) == "function", + "Searchbar requires a OnSearchRequested function.") + + local text = state.text + + local isFocused = (FFlagUXImprovementAddSearchBar and not focusDisabled and state.isFocused) or (not FFlagUXImprovementAddSearchBar and state.isFocused) + local isContainerHovered = state.isContainerHovered + local isClearButtonHovered = state.isClearButtonHovered + + local showClearButton = #text > 0 + + --[[ + By default, TextBoxes let you keep typing infinitely and it will just go out of the bounds + (unless you set properties like ClipDescendants, TextWrapped) + Elsewhere, text boxes shift their contents to the left as you're typing past the bounds + So what you're typing is on the screen + + This is implemented here by: + - Set ClipsDescendants = true on the container + - Get the width of the container, subtracting any padding and the width of the button on the right + - Get the width of the text being rendered (this is calculated in the Roact.Change.Text event) + - If the text is shorter than the parent, then: + - Anchor the text label to the left side of the parent + - Set its width = container width + - Else + - Anchor the text label to the right side of the parent + - Sets its width = text width (with AnchorPoint = (1, 0), this grows to the left) + ]] + local searchBarTheme = theme.searchBar + + local buttonSize = searchBarTheme.buttons.size + + local textBoxOffset = #text > 0 and -buttonSize * 2 or -buttonSize + + local borderColor + if isFocused then + borderColor = searchBarTheme.border.selected.color + elseif isContainerHovered then + borderColor = searchBarTheme.border.hovered.color + else + borderColor = searchBarTheme.border.color + end + + local clearButtonImage = isClearButtonHovered and searchBarTheme.images.clear.hovered.image or searchBarTheme.images.clear.image + + local layoutIndex = LayoutOrderIterator.new() + + local Contents = Roact.createElement("Frame", { + Size = size, + BackgroundColor3 = searchBarTheme.backgroundColor, + BorderColor3 = borderColor, + BorderSizePixel = 1, + LayoutOrder = layoutOrder, + + [Roact.Event.MouseEnter] = self.onContainerHovered, + [Roact.Event.MouseLeave] = self.onContainerHoverEnded, + }, { + Background = Roact.createElement("Frame", { + Size = UDim2.new(1, 0, 1, 0), + BackgroundTransparency = 1, + BorderSizePixel = 0, + }, { + SearchBarLayout = Roact.createElement("UIListLayout", { + SortOrder = Enum.SortOrder.LayoutOrder, + Padding = UDim.new(0, 0), + FillDirection = Enum.FillDirection.Horizontal, + }), + + SearchImageFrame = Roact.createElement("Frame", { + BackgroundTransparency = 1, + Size = UDim2.new(0, buttonSize, 0, buttonSize), + LayoutOrder = layoutIndex:getNextOrder(), + } , { + SearchImage = Roact.createElement("ImageLabel", { + AnchorPoint = Vector2.new(0.5, 0.5), + Position = UDim2.new(0.5, 0, 0.5, 0), + Size = UDim2.new(0, searchBarTheme.buttons.iconSize, + 0, searchBarTheme.buttons.iconSize), + BackgroundTransparency = 1, + Image = searchBarTheme.images.search.image, + ImageColor3 = searchBarTheme.buttons.search.color, + }), + }), + + TextBox = Roact.createElement("TextBox", { + Size = UDim2.new(1, textBoxOffset, 0, buttonSize), + LayoutOrder = layoutIndex:getNextOrder(), + + BackgroundTransparency = 1, + ClipsDescendants = true, + + ClearTextOnFocus = false, + Font = searchBarTheme.font, + TextSize = searchBarTheme.textSize, + TextXAlignment = Enum.TextXAlignment.Left, + TextColor3 = searchBarTheme.text.color, + Text = text, + TextEditable = enabled, + + PlaceholderText = defaultText, + PlaceholderColor3 = searchBarTheme.text.placeholder.color, + + -- Get a reference to the text box so that clicking on the container can call :CaptureFocus() + [Roact.Ref] = self.textBoxRef, + + [Roact.Change.Text] = self.onTextChanged, + [Roact.Event.Focused] = self.onTextBoxFocused, + [Roact.Event.FocusLost] = self.onTextBoxFocusLost, + }), + + ClearButton = showClearButton and Roact.createElement("ImageButton", { + Size = UDim2.new(0, buttonSize, 0, buttonSize), + LayoutOrder = layoutIndex:getNextOrder(), + + BackgroundTransparency = 1, + + [Roact.Event.MouseEnter] = self.onClearButtonHovered, + [Roact.Event.MouseLeave] = self.onClearButtonHoverEnded, + [Roact.Event.MouseButton1Down] = self.onClearButtonClicked, + }, { + ClearImage = Roact.createElement("ImageLabel", { + AnchorPoint = Vector2.new(0.5, 0.5), + Position = UDim2.new(0.5, 0, 0.5, 0), + Size = UDim2.new(0, searchBarTheme.buttons.iconSize, + 0, searchBarTheme.buttons.iconSize), + BackgroundTransparency = 1, + Image = clearButtonImage, + ImageColor3 = searchBarTheme.buttons.clear.color, + }), + }), + }), + }) + + if FFlagAssetManagerLuaCleanup1 then + Contents = Roact.createElement("Frame", { + Size = size, + BackgroundColor3 = searchBarTheme.backgroundColor, + BorderColor3 = borderColor, + BorderSizePixel = 1, + LayoutOrder = layoutOrder, + + [Roact.Event.MouseEnter] = self.onContainerHovered, + [Roact.Event.MouseLeave] = self.onContainerHoverEnded, + }, { + SearchBarLayout = Roact.createElement("UIListLayout", { + SortOrder = Enum.SortOrder.LayoutOrder, + Padding = UDim.new(0, 0), + FillDirection = Enum.FillDirection.Horizontal, + }), + + SearchImageFrame = Roact.createElement("Frame", { + BackgroundTransparency = 1, + Size = UDim2.new(0, buttonSize, 0, buttonSize), + LayoutOrder = layoutIndex:getNextOrder(), + } , { + SearchImage = Roact.createElement("ImageLabel", { + AnchorPoint = Vector2.new(0.5, 0.5), + Position = UDim2.new(0.5, 0, 0.5, 0), + Size = UDim2.new(0, searchBarTheme.buttons.iconSize, + 0, searchBarTheme.buttons.iconSize), + BackgroundTransparency = 1, + Image = searchBarTheme.images.search.image, + ImageColor3 = searchBarTheme.buttons.search.color, + }), + }), + + TextBox = Roact.createElement("TextBox", { + Size = UDim2.new(1, textBoxOffset, 0, buttonSize), + LayoutOrder = layoutIndex:getNextOrder(), + + BackgroundTransparency = 1, + ClipsDescendants = true, + + ClearTextOnFocus = false, + Font = searchBarTheme.font, + TextSize = searchBarTheme.textSize, + TextXAlignment = Enum.TextXAlignment.Left, + TextColor3 = searchBarTheme.text.color, + Text = text, + TextEditable = enabled, + + PlaceholderText = defaultText, + PlaceholderColor3 = searchBarTheme.text.placeholder.color, + + -- Get a reference to the text box so that clicking on the container can call :CaptureFocus() + [Roact.Ref] = self.textBoxRef, + + [Roact.Change.Text] = self.onTextChanged, + [Roact.Event.Focused] = self.onTextBoxFocused, + [Roact.Event.FocusLost] = self.onTextBoxFocusLost, + }), + + ClearButton = showClearButton and Roact.createElement("ImageButton", { + Size = UDim2.new(0, buttonSize, 0, buttonSize), + LayoutOrder = layoutIndex:getNextOrder(), + + BackgroundTransparency = 1, + + [Roact.Event.MouseEnter] = self.onClearButtonHovered, + [Roact.Event.MouseLeave] = self.onClearButtonHoverEnded, + [Roact.Event.MouseButton1Down] = self.onClearButtonClicked, + }, { + ClearImage = Roact.createElement("ImageLabel", { + AnchorPoint = Vector2.new(0.5, 0.5), + Position = UDim2.new(0.5, 0, 0.5, 0), + Size = UDim2.new(0, searchBarTheme.buttons.iconSize, + 0, searchBarTheme.buttons.iconSize), + BackgroundTransparency = 1, + Image = clearButtonImage, + ImageColor3 = searchBarTheme.buttons.clear.color, + }), + }), + }) + end + + if FFlagUXImprovementAddSearchBar and FFlagAssetManagerLuaCleanup1 then + Contents = Roact.createElement(rounded and RoundFrame or "Frame", { + Size = size, + BackgroundColor3 = searchBarTheme.backgroundColor, + BorderColor3 = borderColor, + BorderSizePixel = 1, + LayoutOrder = layoutOrder, + + [Roact.Event.MouseEnter] = self.onContainerHovered, + [Roact.Event.MouseLeave] = self.onContainerHoverEnded, + }, { + SearchBarLayout = Roact.createElement("UIListLayout", { + SortOrder = Enum.SortOrder.LayoutOrder, + Padding = UDim.new(0, 0), + FillDirection = Enum.FillDirection.Horizontal, + }), + + SearchImageFrame = Roact.createElement("Frame", { + BackgroundTransparency = 1, + Size = UDim2.new(0, buttonSize, 1, 0), + LayoutOrder = layoutIndex:getNextOrder(), + } , { + SearchImage = Roact.createElement("ImageLabel", { + AnchorPoint = Vector2.new(0.5, 0.5), + Position = UDim2.new(0.5, 0, 0.5, 0), + Size = UDim2.new(0, searchBarTheme.buttons.iconSize, + 0, searchBarTheme.buttons.iconSize), + BackgroundTransparency = 1, + Image = searchBarTheme.images.search.image, + ImageColor3 = searchBarTheme.buttons.search.color, + }), + }), + + TextBox = Roact.createElement("TextBox", { + Size = UDim2.new(1, textBoxOffset, 1, 0), + LayoutOrder = layoutIndex:getNextOrder(), + + BackgroundTransparency = 1, + ClipsDescendants = true, + + ClearTextOnFocus = false, + Font = searchBarTheme.font, + TextSize = searchBarTheme.textSize, + TextXAlignment = Enum.TextXAlignment.Left, + TextColor3 = searchBarTheme.text.color, + Text = text, + TextEditable = enabled, + + PlaceholderText = defaultText, + PlaceholderColor3 = searchBarTheme.text.placeholder.color, + + -- Get a reference to the text box so that clicking on the container can call :CaptureFocus() + [Roact.Ref] = self.textBoxRef, + + [Roact.Change.Text] = self.onTextChanged, + [Roact.Event.Focused] = self.onTextBoxFocused, + [Roact.Event.FocusLost] = self.onTextBoxFocusLost, + }), + + ClearButton = showClearButton and Roact.createElement("ImageButton", { + Size = UDim2.new(0, buttonSize, 1, 0), + LayoutOrder = layoutIndex:getNextOrder(), + + BackgroundTransparency = 1, + + [Roact.Event.MouseEnter] = self.onClearButtonHovered, + [Roact.Event.MouseLeave] = self.onClearButtonHoverEnded, + [Roact.Event.MouseButton1Down] = self.onClearButtonClicked, + }, { + ClearImage = Roact.createElement("ImageLabel", { + AnchorPoint = Vector2.new(0.5, 0.5), + Position = UDim2.new(0.5, 0, 0.5, 0), + Size = UDim2.new(0, searchBarTheme.buttons.iconSize, + 0, searchBarTheme.buttons.iconSize), + BackgroundTransparency = 1, + Image = clearButtonImage, + ImageColor3 = searchBarTheme.buttons.clear.color, + }), + }), + }) + end + + return Contents + end) +end + +return SearchBar diff --git a/BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Components/SearchBar.spec.lua b/BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Components/SearchBar.spec.lua new file mode 100644 index 0000000000..eb00ad0348 --- /dev/null +++ b/BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Components/SearchBar.spec.lua @@ -0,0 +1,59 @@ +local FFlagAssetManagerLuaCleanup1 = settings():GetFFlag("AssetManagerLuaCleanup1") + +return function() + local Library = script.Parent.Parent + local Roact = require(Library.Parent.Parent.Roact) + + local MockWrapper = require(Library.MockWrapper) + + local SearchBar = require(script.Parent.SearchBar) + + it("should create and destroy without errors", function() + local element = Roact.createElement(MockWrapper, {}, { + SearchBar = Roact.createElement(SearchBar, { + Size = UDim2.new(0, 100, 0, 20), + OnSearchRequested = function() end, + }), + }) + local instance = Roact.mount(element) + Roact.unmount(instance) + end) + + describe("the textbox", function() + it("should move as text is typed", function() + local width = 200 + local element = Roact.createElement(MockWrapper, {}, { + SearchBar = Roact.createElement(SearchBar, { + Size = UDim2.new(0, width, 0, 20), + Enabled = true, + OnSearchRequested = function() end, + }), + }) + local container = Instance.new("Folder") + local instance = Roact.mount(element, container, "SearchBar") + local searchBar = container.SearchBar + local textBox + if FFlagAssetManagerLuaCleanup1 then + textBox =searchBar.TextBox + else + textBox = searchBar.Background.TextBox + end + + local str = ("abcdefghijklmnopqrstuvwxyz"):rep(2) + + textBox.Text = str:sub(1, 1) + local previousWidth = textBox.AbsoluteSize.x + + for i = 1, #str, 1 do + local text = str:sub(1, i) + textBox.Text = text + + local width = textBox.AbsoluteSize.x + expect(width >= previousWidth).to.equal(true) + previousWidth = width + end + + Roact.unmount(instance) + end) + end) +end diff --git a/BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Components/Separator.lua b/BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Components/Separator.lua new file mode 100644 index 0000000000..f0ecae653d --- /dev/null +++ b/BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Components/Separator.lua @@ -0,0 +1,51 @@ +--[[ + A simple border to separate elements. + + Props: + Enum DominantAxis = Specifies whether the separator fills the + space horizontally or vertically. Width will make the separator + fill the horizontal space, and Height will make the separator + fill the vertical space. + Weight = The thickness of the separator line. + Padding = The padding in pixels to subtract from either side of + the separator's dominant axis. + + Position = The position of the center of the separator. + LayoutOrder = The order in which the separator appears in a UILayout. + ZIndex = The render order of the separator. +]] + +local Library = script.Parent.Parent +local Roact = require(Library.Parent.Parent.Roact) +local Theming = require(Library.Theming) +local withTheme = Theming.withTheme + +local function Separator(props) + local dominantAxis = props.DominantAxis or Enum.DominantAxis.Width + local position = props.Position + local weight = props.Weight or 1 + local padding = props.Padding or 0 + local zIndex = props.ZIndex + local layoutOrder = props.LayoutOrder + + local size + if dominantAxis == Enum.DominantAxis.Width then + size = UDim2.new(1, -padding * 2, 0, weight) + else + size = UDim2.new(0, weight, 1, -padding * 2) + end + + return withTheme(function(theme) + return Roact.createElement("Frame", { + Size = size, + Position = position, + AnchorPoint = Vector2.new(0.5, 0.5), + BackgroundColor3 = theme.separator.lineColor, + BorderSizePixel = 0, + ZIndex = zIndex, + LayoutOrder = layoutOrder, + }) + end) +end + +return Separator \ No newline at end of file diff --git a/BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Components/Separator.spec.lua b/BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Components/Separator.spec.lua new file mode 100644 index 0000000000..300dda2c74 --- /dev/null +++ b/BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Components/Separator.spec.lua @@ -0,0 +1,30 @@ +return function() + local Library = script.Parent.Parent + local Roact = require(Library.Parent.Parent.Roact) + + local MockWrapper = require(Library.MockWrapper) + + local Separator = require(script.Parent.Separator) + + local function createTestSeparator(props) + return Roact.createElement(MockWrapper, {}, { + Separator = Roact.createElement(Separator, props) + }) + end + + it("should create and destroy without errors", function() + local element = createTestSeparator() + local instance = Roact.mount(element) + Roact.unmount(instance) + end) + + it("should render correctly", function () + local container = Instance.new("Folder") + local instance = Roact.mount(createTestSeparator(), container) + local separator = container:FindFirstChildOfClass("Frame") + + expect(separator).to.be.ok() + + Roact.unmount(instance) + end) +end \ No newline at end of file diff --git a/BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Components/StyledDialog.lua b/BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Components/StyledDialog.lua new file mode 100644 index 0000000000..200abb3ce9 --- /dev/null +++ b/BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Components/StyledDialog.lua @@ -0,0 +1,105 @@ +--[[ + An implementation of BaseDialog that adds UILibrary Buttons to the bottom. + To use the component, the consumer supplies an array of buttons, optionally + defining a Style for each button if it should display differently. + + Props: + array Buttons = An array of items used to render the buttons for this dialog. + { + {Key = "Cancel", Text = "SomeLocalizedTextForCancel"}, + {Key = "Save", Text = "SomeLocalizedTextForSave", Style = "Primary"}, + } + function OnButtonClicked(key) = A callback for when the user clicked + a button in the dialog. Accepts the Key of the button that was clicked. + function OnClose = A callback for when the user closed the dialog by + clicking the X in the corner of the window. + + Vector2 Size = The starting size of the dialog. + Vector2 MinSize = The minimum size of the dialog, if it is resizable. + bool Resizable = Whether the dialog can be resized. + int BorderPadding = The padding to add around the edges of the dialog. + int ButtonPadding = The padding to add between buttons. + int ButtonHeight = The height of the buttons in the dialog, in pixels. + int ButtonWidth = The width of each button in the dialog, in pixels. + string Title = The title to display at the top of the window. +]] + +local Library = script.Parent.Parent +local Roact = require(Library.Parent.Parent.Roact) + +local Theming = require(Library.Theming) +local withTheme = Theming.withTheme + +local BaseDialog = require(Library.Components.BaseDialog) +local Button = require(Library.Components.Button) + +local StyledDialog = Roact.PureComponent:extend("StyledDialog") + +function StyledDialog:init() + self.enabledChanged = function(enabled) + if not enabled and self.props.OnClose then + self.props.OnClose() + end + end + + self.buttonClicked = function(button) + if self.props.OnButtonClicked then + self.props.OnButtonClicked(button.Key) + end + end +end + +function StyledDialog:render() + return withTheme(function(theme) + local props = self.props + local title = props.Title + local size = props.Size + local minSize = props.MinSize + local resizable = props.Resizable + local borderPadding = props.BorderPadding + local textSize = props.TextSize + + local buttons = props.Buttons + local buttonPadding = props.ButtonPadding + local buttonHeight = props.ButtonHeight + local buttonWidth = props.ButtonWidth + + return Roact.createElement(BaseDialog, { + Title = title, + Size = size, + MinSize = minSize, + Resizable = resizable, + Buttons = buttons, + ButtonHeight = buttonHeight, + BorderPadding = borderPadding, + ButtonPadding = buttonPadding, + + RenderButton = function(button, index, activated) + return Roact.createElement(Button, { + Size = UDim2.new(0, buttonWidth, 0, buttonHeight), + LayoutOrder = index, + Style = button.Style, + + OnClick = activated, + RenderContents = function(buttonTheme) + return { + Text = Roact.createElement("TextLabel", { + Size = UDim2.new(1, 0, 1, 0), + BackgroundTransparency = 1, + Font = buttonTheme.font, + Text = button.Text, + TextSize = textSize, + TextColor3 = buttonTheme.textColor, + }) + } + end, + }) + end, + + OnButtonClicked = self.buttonClicked, + OnClose = props.OnClose, + }, self.props[Roact.Children]) + end) +end + +return StyledDialog diff --git a/BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Components/StyledDialog.spec.lua b/BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Components/StyledDialog.spec.lua new file mode 100644 index 0000000000..4a4289e0ac --- /dev/null +++ b/BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Components/StyledDialog.spec.lua @@ -0,0 +1,94 @@ +return function() + local Library = script.Parent.Parent + local Roact = require(Library.Parent.Parent.Roact) + + local MockWrapper = require(Library.MockWrapper) + + local StyledDialog = require(script.Parent.StyledDialog) + + local function createTestStyledDialog(props, children, container) + return Roact.createElement(MockWrapper, { + Container = container, + }, { + StyledDialog = Roact.createElement(StyledDialog, props, children) + }) + end + + it("should create and destroy without errors", function() + local element = createTestStyledDialog({ + Buttons = {}, + }) + local instance = Roact.mount(element) + Roact.unmount(instance) + end) + + it("should render correctly", function() + local container = Instance.new("Folder") + + local element = createTestStyledDialog({ + Buttons = {}, + }, {}, container) + + local instance = Roact.mount(element, container) + + local gui = container:FindFirstChildOfClass("BillboardGui") + expect(gui).to.be.ok() + expect(gui.FocusProvider).to.be.ok() + expect(gui.FocusProvider.Padding).to.be.ok() + expect(gui.FocusProvider.Content).to.be.ok() + expect(gui.FocusProvider.Buttons).to.be.ok() + + Roact.unmount(instance) + end) + + it("should require a Buttons table", function() + local element = createTestStyledDialog() + expect(function() + Roact.mount(element) + end).to.throw() + + element = createTestStyledDialog({ + Buttons = true, + }) + expect(function() + Roact.mount(element) + end).to.throw() + end) + + it("should create a Button for each button", function() + local container = Instance.new("Folder") + + local element = createTestStyledDialog({ + Buttons = { + {Key = "Test", Text = "TestText"}, + {Key = "Test2", Text = "TestText2"}, + }, + }, {}, container) + + local instance = Roact.mount(element, container) + + local gui = container:FindFirstChildOfClass("BillboardGui") + expect(gui.FocusProvider.Buttons).to.be.ok() + expect(gui.FocusProvider.Buttons[1]).to.be.ok() + expect(gui.FocusProvider.Buttons[2]).to.be.ok() + + Roact.unmount(instance) + end) + + it("should render its children", function() + local container = Instance.new("Folder") + + local element = createTestStyledDialog({ + Buttons = {}, + }, { + Frame = Roact.createElement("Frame"), + }, container) + + local instance = Roact.mount(element, container) + + local gui = container:FindFirstChildOfClass("BillboardGui") + expect(gui.FocusProvider.Content.Frame).to.be.ok() + + Roact.unmount(instance) + end) +end \ No newline at end of file diff --git a/BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Components/StyledDropdown.lua b/BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Components/StyledDropdown.lua new file mode 100644 index 0000000000..56830dda86 --- /dev/null +++ b/BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Components/StyledDropdown.lua @@ -0,0 +1,281 @@ +--[[ + A dropdown menu styled to match the Roblox Studio start page. + Consists of a button used to open the dropdown as well as the menu itself. + Note that the logic for opening and closing the menu is contained within this component, + but the consumer is responsible for showing the current value in the button. + + Required Props: + UDim2 Size = The size of the button that opens the dropdown. + UDim2 Position = The position of the button that opens the dropdown. + int TextSize = The size of the text in the dropdown and button. + int ItemHeight = The height of each entry in the dropdown, in pixels. + string ButtonText = The text to display in the button that opens the dropdown. + Usually should be set to the currently selected dropdown entry. + array Items = An ordered array of each item that should appear in the dropdown. + The array is formatted like this: + { + {Key = "Item1", Text = "SomeLocalizedTextForItem1"}, + {Key = "Item2", Text = "SomeLocalizedTextForItem2"}, + {Key = "Item3", Text = "SomeLocalizedTextForItem3"}, + } + Key is how the item will be referenced in code. Text is what will appear to the user. + function OnItemClicked(item) = A callback when the user selects an item in the dropdown. + Returns the item as it was defined in the Items array. + + Optional Props: + int MaxItems = The maximum number of entries that can display at a time. + If this is less than the number of entries in the dropdown, a scrollbar will appear. + bool ShowRibbon = Whether to show a colored ribbon next to the currently + hovered dropdown entry. Usually should be enabled for Light theme only. + int TextPadding = The amount of padding, in pixels, around the text elements. + int IconSize = The size of the arrow icon in the button. + int IconPadding = The distance from the right side of the arrow icon to the button edge. + int ScrollBarPadding = The padding which appears on either side of the scrollbar. + int ScrollBarThickness = The horizontal width of the scrollbar. +]] +local FFlagStudioFixUILibDropdownStyle = game:GetFastFlag("StudioFixUILibDropdownStyle") +local FFlagStudioFixUILibDropdownText = game:GetFastFlag("StudioFixUILibDropdownText") + +-- Defaults +local TEXT_PADDING = 8 +local ICON_SIZE = 12 +local ICON_PADDING = 4 + +local RIBBON_WIDTH = 5 +local VERTICAL_OFFSET = 2 + +local TextService = game:GetService("TextService") + +local Library = script.Parent.Parent +local Roact = require(Library.Parent.Parent.Roact) + +local Theming = require(Library.Theming) +local withTheme = Theming.withTheme + +local DropdownMenu = require(Library.Components.DropdownMenu) +local RoundFrame = require(Library.Components.RoundFrame) + +local StyledDropdown = Roact.PureComponent:extend("StyledDropdown") + +function StyledDropdown:init() + self.state = { + showDropdown = false, + isButtonHovered = false, + hoveredKey = nil, + } + self.buttonRef = Roact.createRef() + + self.onItemClicked = function(key) + self.props.OnItemClicked(key) + self.hideDropdown() + end + + self.showDropdown = function() + self:setState({ + showDropdown = true, + }) + end + + self.hideDropdown = function() + self:setState({ + showDropdown = false, + }) + end + + self.onKeyMouseEnter = function(key) + self:setState({ + hoveredKey = key, + }) + end + + self.onKeyMouseLeave = function(key) + if self.state.hoveredKey == key then + self:setState({ + hoveredKey = Roact.None, + }) + end + end + + self.onMouseEnter = function() + self:setState({ + isButtonHovered = true, + }) + end + + self.onMouseLeave = function() + self:setState({ + isButtonHovered = false, + }) + end +end + +function StyledDropdown:createLabel(key, displayText, textSize, textPadding, font, textColor) + return Roact.createElement("TextLabel", { + Size = UDim2.new(1, 0, 1, 0), + Font = font, + TextSize = textSize, + Text = displayText, + TextXAlignment = Enum.TextXAlignment.Left, + TextColor3 = textColor, + BackgroundTransparency = 1, + + [Roact.Event.MouseEnter] = function() + self.onKeyMouseEnter(key) + end, + [Roact.Event.MouseLeave] = function() + self.onKeyMouseLeave(key) + end, + }, { + Padding = Roact.createElement("UIPadding", { + PaddingLeft = UDim.new(0, textPadding), + }), + }) +end + +function StyledDropdown:render() + return withTheme(function(theme) + local props = self.props + local state = self.state + local dropdownTheme = theme.styledDropdown + local listTheme = dropdownTheme.listTheme or dropdownTheme + local showDropdown = state.showDropdown + local buttonRef = self.buttonRef and self.buttonRef.current + local buttonExtents + if buttonRef then + local buttonMin = buttonRef.AbsolutePosition + local buttonSize = buttonRef.AbsoluteSize + local buttonMax = buttonMin + buttonSize + buttonExtents = Rect.new(buttonMin.X, buttonMin.Y, buttonMax.X, buttonMax.Y) + end + local listWidth = props.ListWidth or 0 + local items = props.Items or {} + local size = props.Size + local position = props.Position + local textSize = props.TextSize + local itemHeight = props.ItemHeight + local maxItems = props.MaxItems + local showRibbon = props.ShowRibbon + + local textPadding = props.TextPadding or TEXT_PADDING + local iconSize = props.IconSize or ICON_SIZE + local iconPadding = props.IconPadding or ICON_PADDING + local scrollBarPadding = props.ScrollBarPadding + local scrollBarThickness = props.ScrollBarThickness + + local hoveredKey = state.hoveredKey + local selectedItem = props.SelectedItem + local isButtonHovered = state.isButtonHovered + local buttonText = props.ButtonText + + local maxWidth = 0 + local maxHeight = maxItems and (maxItems * itemHeight) or nil + local LayoutOrder = props.LayoutOrder or 0 + + for _, data in ipairs(items) do + local textBound = TextService:GetTextSize(data.Text, + textSize, dropdownTheme.font, Vector2.new(9000, 100)) + + local itemWidth = textBound.X + textPadding * 2 + maxWidth = math.max(maxWidth, itemWidth) + end + + if FFlagStudioFixUILibDropdownStyle then + maxWidth = math.max(maxWidth, listWidth) + end + + local buttonTheme = (showDropdown or isButtonHovered) and dropdownTheme.selected + or dropdownTheme + + return Roact.createElement("ImageButton", { + Size = size, + Position = position, + BackgroundTransparency = 1, + ImageTransparency = 1, + + [Roact.Ref] = self.buttonRef, + + [Roact.Event.Activated] = self.showDropdown, + [Roact.Event.MouseEnter] = self.onMouseEnter, + [Roact.Event.MouseLeave] = self.onMouseLeave, + + LayoutOrder = LayoutOrder, + }, { + RoundFrame = Roact.createElement(RoundFrame, { + Size = UDim2.new(1, 0, 1, 0), + BackgroundColor3 = buttonTheme.backgroundColor, + BorderColor3 = buttonTheme.borderColor, + }), + + ArrowIcon = Roact.createElement("ImageLabel", { + Size = UDim2.new(0, iconSize, 0, iconSize), + Position = UDim2.new(1, -iconPadding, 0.5, 0), + AnchorPoint = Vector2.new(1, 0.5), + BackgroundTransparency = 1, + ImageColor3 = buttonTheme.textColor, + Image = dropdownTheme.arrowImage, + }), + + TextLabel = Roact.createElement("TextLabel", { + Size = UDim2.new(1, FFlagStudioFixUILibDropdownText and -iconSize or 0, 1, 0), + BackgroundTransparency = 1, + Font = dropdownTheme.font, + TextColor3 = buttonTheme.textColor, + TextSize = textSize, + Text = buttonText, + TextXAlignment = Enum.TextXAlignment.Left, + TextTruncate = FFlagStudioFixUILibDropdownText and Enum.TextTruncate.AtEnd or nil, + }, { + Padding = Roact.createElement("UIPadding", { + PaddingLeft = UDim.new(0, textPadding), + }), + }), + + Dropdown = showDropdown and buttonRef and Roact.createElement(DropdownMenu, { + OnItemClicked = self.onItemClicked, + OnFocusLost = self.hideDropdown, + SourceExtents = buttonExtents, + Offset = Vector2.new(0, VERTICAL_OFFSET), + MaxHeight = maxHeight, + ShowBorder = true, + ScrollBarPadding = scrollBarPadding, + ScrollBarThickness = scrollBarThickness, + ListWidth = FFlagStudioFixUILibDropdownStyle and maxWidth or listWidth, + Items = items, + RenderItem = function(item, index, activated) + local key = item.Key + local selected = key == selectedItem + local displayText = item.Text + local isHovered = hoveredKey == key + local textColor = (selected or isHovered) and dropdownTheme.hovered.textColor + or dropdownTheme.textColor + local itemColor = listTheme.backgroundColor + if selected then + itemColor = listTheme.selected.backgroundColor + elseif isHovered then + itemColor = listTheme.hovered.backgroundColor + end + + return Roact.createElement("ImageButton", { + Size = UDim2.new(0, FFlagStudioFixUILibDropdownStyle and maxWidth or math.max(listWidth, maxWidth), 0, itemHeight), + BackgroundColor3 = itemColor, + BorderSizePixel = 0, + LayoutOrder = index, + AutoButtonColor = false, + [Roact.Event.Activated] = activated, + }, { + Ribbon = isHovered and showRibbon and Roact.createElement("Frame", { + Size = UDim2.new(0, RIBBON_WIDTH, 1, 0), + BackgroundColor3 = listTheme.selected.backgroundColor, + BorderSizePixel = 0, + }), + + Label = self:createLabel(key, displayText, textSize, + textPadding, dropdownTheme.font, textColor), + }) + end, + }) + }) + end) +end + +return StyledDropdown diff --git a/BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Components/StyledDropdown.spec.lua b/BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Components/StyledDropdown.spec.lua new file mode 100644 index 0000000000..2e01d21f4d --- /dev/null +++ b/BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Components/StyledDropdown.spec.lua @@ -0,0 +1,34 @@ +return function() + local Library = script.Parent.Parent + local Roact = require(Library.Parent.Parent.Roact) + + local MockWrapper = require(Library.MockWrapper) + + local StyledDropdown = require(script.Parent.StyledDropdown) + + local function createTestStyledDropdown(props, children) + return Roact.createElement(MockWrapper, {}, { + StyledDropdown = Roact.createElement(StyledDropdown, props, children) + }) + end + + it("should create and destroy without errors", function() + local element = createTestStyledDropdown() + local instance = Roact.mount(element) + Roact.unmount(instance) + end) + + it("should render correctly", function() + local container = Instance.new("Folder") + local instance = Roact.mount(createTestStyledDropdown(), container) + local button = container:FindFirstChildOfClass("ImageButton") + + expect(button).to.be.ok() + expect(button.RoundFrame).to.be.ok() + expect(button.ArrowIcon).to.be.ok() + expect(button.TextLabel).to.be.ok() + expect(button.TextLabel.Padding).to.be.ok() + + Roact.unmount(instance) + end) +end \ No newline at end of file diff --git a/BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Components/StyledScrollingFrame.lua b/BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Components/StyledScrollingFrame.lua new file mode 100644 index 0000000000..7e54ef796e --- /dev/null +++ b/BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Components/StyledScrollingFrame.lua @@ -0,0 +1,98 @@ +--[[ + A scrolling frame with a colored background, providing a consistent look + with the Studio native start page. + + Props: + UDim2 Position = The position of the scrolling frame. + UDim2 Size = The size of the scrolling frame. + UDim2 CanvasSize = The size of the scrolling frame's canvas. + + int LayoutOrder = The order this component will display in a UILayout. + int ZIndex = The draw index of the frame. + + bool ScrollingEnabled = Whether scrolling in this frame will change the CanvasPosition. + int ScrollBarPadding = The padding which appears on either side of the scrollbar. + int ScrollBarThickness = The horizontal width of the scrollbar. + + function OnScroll(Vector2 CanvasPosition) = A callback for when the CanvasPosition changes. +]] + +local DEFAULT_SCROLLBAR_THICKNESS = 8 +local DEFAULT_SCROLLBAR_PADDING = 2 + +local Library = script.Parent.Parent +local Roact = require(Library.Parent.Parent.Roact) + +local Theming = require(Library.Theming) +local withTheme = Theming.withTheme + +local StyledScrollingFrame = Roact.PureComponent:extend("StyledScrollingFrame") + +function StyledScrollingFrame:init() + self.onScroll = function(rbx) + if self.props.OnScroll then + self.props.OnScroll(rbx.CanvasPosition) + end + end +end + +function StyledScrollingFrame:render() + return withTheme(function(theme) + local props = self.props + local scrollTheme = theme.scrollingFrame + + local position = props.Position + local size = props.Size + local canvasSize = props.CanvasSize + local layoutOrder = props.LayoutOrder + local zindex = props.ZIndex + local scrollingEnabled = props.ScrollingEnabled + local padding = props.ScrollBarPadding or DEFAULT_SCROLLBAR_PADDING + local scrollBarThickness = props.ScrollBarThickness or DEFAULT_SCROLLBAR_THICKNESS + + local backgroundThickness = scrollBarThickness + (padding * 2) + + local ref = props[Roact.Ref] + local children = props[Roact.Children] + + return Roact.createElement("Frame", { + Position = position, + Size = size, + LayoutOrder = layoutOrder, + ZIndex = zindex, + BackgroundTransparency = 1, + }, { + ScrollBarBackground = Roact.createElement("Frame", { + Position = UDim2.new(1, 0, 0, 0), + Size = UDim2.new(0, backgroundThickness, 1, 0), + AnchorPoint = Vector2.new(1, 0), + BorderSizePixel = 0, + BackgroundColor3 = scrollTheme.backgroundColor, + ZIndex = 2, + }), + + ScrollingFrame = Roact.createElement("ScrollingFrame", { + Size = UDim2.new(1, -padding, 1, 0), + CanvasSize = canvasSize, + BackgroundTransparency = 1, + BorderSizePixel = 0, + ScrollBarThickness = scrollBarThickness, + ZIndex = 2, + + TopImage = scrollTheme.topImage, + MidImage = scrollTheme.midImage, + BottomImage = scrollTheme.bottomImage, + + ScrollBarImageColor3 = scrollTheme.scrollbarColor, + + ScrollingEnabled = scrollingEnabled, + ScrollingDirection = Enum.ScrollingDirection.Y, + + [Roact.Change.CanvasPosition] = self.onScroll, + [Roact.Ref] = ref, + }, children), + }) + end) +end + +return StyledScrollingFrame diff --git a/BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Components/StyledScrollingFrame.spec.lua b/BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Components/StyledScrollingFrame.spec.lua new file mode 100644 index 0000000000..6ecb742b68 --- /dev/null +++ b/BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Components/StyledScrollingFrame.spec.lua @@ -0,0 +1,62 @@ +return function() + local Library = script.Parent.Parent + local Roact = require(Library.Parent.Parent.Roact) + + local MockWrapper = require(Library.MockWrapper) + + local StyledScrollingFrame = require(script.Parent.StyledScrollingFrame) + + local function createTestScrollingFrame(props, children) + return Roact.createElement(MockWrapper, {}, { + ScrollingFrame = Roact.createElement(StyledScrollingFrame, props, children) + }) + end + + it("should create and destroy without errors", function() + local element = createTestScrollingFrame() + local instance = Roact.mount(element) + Roact.unmount(instance) + end) + + it("should render correctly", function() + local container = Instance.new("Folder") + local instance = Roact.mount(createTestScrollingFrame(), container) + local frame = container:FindFirstChildOfClass("Frame") + + expect(frame).to.be.ok() + expect(frame.ScrollBarBackground).to.be.ok() + expect(frame.ScrollingFrame).to.be.ok() + + Roact.unmount(instance) + end) + + it("should render its children in the ScrollingFrame", function() + local container = Instance.new("Folder") + local instance = Roact.mount(createTestScrollingFrame({}, { + ChildFrame = Roact.createElement("Frame"), + }), container) + local frame = container:FindFirstChildOfClass("Frame") + + expect(frame).to.be.ok() + expect(frame.ScrollBarBackground).to.be.ok() + expect(frame.ScrollingFrame).to.be.ok() + expect(frame.ScrollingFrame.ChildFrame).to.be.ok() + + Roact.unmount(instance) + end) + + it("should add padding to both sides of the ScrollBar", function() + local container = Instance.new("Folder") + local instance = Roact.mount(createTestScrollingFrame({ + ScrollBarPadding = 2, + ScrollBarThickness = 8, + }), container) + local frame = container:FindFirstChildOfClass("Frame") + + expect(frame).to.be.ok() + expect(frame.ScrollBarBackground).to.be.ok() + expect(frame.ScrollBarBackground.Size.X.Offset).to.equal(12) + + Roact.unmount(instance) + end) +end \ No newline at end of file diff --git a/BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Components/StyledTooltip.lua b/BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Components/StyledTooltip.lua new file mode 100644 index 0000000000..91bffdcb3e --- /dev/null +++ b/BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Components/StyledTooltip.lua @@ -0,0 +1,196 @@ +--[[ + An element which can be added to a component to give that component a tooltip. + When the user hovers the mouse over the component, the tooltip will appear + after a short delay. + + Required Props: + table Elements = The table containing roact elements to display in the tooltip. + Vector2 TooltipExtents = vector containing tooltip size + bool Enabled = Whether the tooltip will display on hover. + + Optional Props: + float ShowDelay = The time in seconds before the tooltip appears + after the user stops moving the mouse over the element. Defaults to 0.5. + int Priority = The display order of this element, compared to other focused + elements or elements that show on top. +]] + +local SHOW_DELAY_DEFAULT = 0.5 + +local RunService = game:GetService("RunService") + +local Library = script.Parent.Parent +local Roact = require(Library.Parent.Parent.Roact) + +local Theming = require(Library.Theming) +local withTheme = Theming.withTheme + +local Focus = require(Library.Focus) +local ShowOnTop = Focus.ShowOnTop +local withFocus = Focus.withFocus + +local DropShadow = require(Library.Components.DropShadow) + +local Tooltip = Roact.PureComponent:extend("Tooltip") + +function Tooltip:init(props) + self.state = { + showToolTip = false, + } + + self.isElementHovered = false + self.isTooltipHovered = false + self.mousePos = nil + + self.connectHover = function() + self.hoverConnection = RunService.Heartbeat:Connect(function() + if self.isElementHovered or self.isTooltipHovered then + if tick() >= self.targetTime then + self.disconnectHover() + self:setState({ + showToolTip = true, + }) + end + end + end) + end + + self.disconnectHover = function() + if self.hoverConnection then + self.hoverConnection:Disconnect() + end + end + + self.elementMouseEnter = function(rbx, xpos, ypos) + local showDelay = props.ShowDelay or SHOW_DELAY_DEFAULT + self.isElementHovered = true + self.targetTime = tick() + showDelay + if not self.mousePos then + self.mousePos = Vector2.new(xpos, ypos) + end + self.connectHover() + end + + self.elementMouseMoved = function(rbx, xpos, ypos) + local showDelay = props.ShowDelay or SHOW_DELAY_DEFAULT + self.targetTime = tick() + showDelay + end + + self.elementMouseLeave = function() + self.isElementHovered = false + local hovered = self.isElementHovered or self.isTooltipHovered + self:setState({ + showToolTip = hovered, + }) + if not hovered then + self.targetTime = 0 + self.mousePos = nil + self.disconnectHover() + end + end + + self.tooltipMouseEnter = function(rbx, xpos, ypos) + self.isTooltipHovered = true + end + + self.tooltipMouseMoved = function(rbx, xpos, ypos) + local showDelay = props.ShowDelay or SHOW_DELAY_DEFAULT + self.targetTime = tick() + showDelay + end + + self.tooltipMouseLeave = function() + self.isTooltipHovered = false + local hovered = self.isElementHovered or self.isTooltipHovered + self:setState({ + showToolTip = hovered, + }) + if not hovered then + self.targetTime = 0 + self.mousePos = nil + self.disconnectHover() + end + end +end + +function Tooltip:willUnmount() + self.disconnectHover() +end + +function Tooltip:render() + return withTheme(function(theme) + return withFocus(function(pluginGui) + local props = self.props + local state = self.state + + local styledTooltipTheme = theme.styledTooltip + + local enabled = props.Enabled + local priority = props.Priority or 0 + + local mousePos = self.mousePos + + local elements = props.Elements + local tooltipWidth = props.TooltipExtents and props.TooltipExtents.X + local tooltipHeight = props.TooltipExtents and props.TooltipExtents.Y + + local content = {} + + if state.showToolTip and mousePos and enabled and pluginGui then + local targetX = mousePos.X + local targetY = mousePos.Y + + local targetWidth = pluginGui.AbsoluteSize.X + local targetHeight = pluginGui.AbsoluteSize.Y + + + if targetX + tooltipWidth >= targetWidth then + targetX = targetWidth - tooltipWidth + end + + if targetY + tooltipHeight >= targetHeight then + targetY = targetHeight - tooltipHeight + end + + content.TooltipContainer = Roact.createElement(ShowOnTop, { + Priority = priority, + }, { + Tooltip = Roact.createElement("Frame", { + Position = UDim2.new(0, targetX, 0, targetY), + Size = UDim2.new(0, tooltipWidth, 0, tooltipHeight), + BackgroundTransparency = 1, + BorderSizePixel = 0, + + [Roact.Event.MouseEnter] = self.tooltipMouseEnter, + [Roact.Event.MouseMoved] = self.tooltipMouseMoved, + [Roact.Event.MouseLeave] = self.tooltipMouseLeave, + }, { + DropShadow = Roact.createElement(DropShadow, { + Transparency = styledTooltipTheme.shadowTransparency, + Color = styledTooltipTheme.shadowColor, + Offset = styledTooltipTheme.shadowOffset, + ZIndex = 1, + }), + + ContentFrame = Roact.createElement("Frame", { + Size = UDim2.new(1, 0, 1, 0), + BackgroundColor3 = styledTooltipTheme.backgroundColor, + BorderSizePixel = 0, + ZIndex = 2, + }, elements), + }) + }) + end + + return Roact.createElement("Frame",{ + Size = UDim2.new(1, 0, 1, 0), + BackgroundTransparency = 1, + + [Roact.Event.MouseEnter] = self.elementMouseEnter, + [Roact.Event.MouseMoved] = self.elementMouseMoved, + [Roact.Event.MouseLeave] = self.elementMouseLeave, + }, content) + end) + end) +end + +return Tooltip diff --git a/BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Components/StyledTooltip.spec.lua b/BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Components/StyledTooltip.spec.lua new file mode 100644 index 0000000000..c2291b26f9 --- /dev/null +++ b/BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Components/StyledTooltip.spec.lua @@ -0,0 +1,30 @@ +return function() + local Library = script.Parent.Parent + local Roact = require(Library.Parent.Parent.Roact) + + local MockWrapper = require(Library.MockWrapper) + + local StyledTooltip = require(script.Parent.StyledTooltip) + + local function createTestTooltip(props) + return Roact.createElement(MockWrapper, {}, { + Tooltip = Roact.createElement(StyledTooltip, props) + }) + end + + it("should create and destroy without errors", function() + local element = createTestTooltip() + local instance = Roact.mount(element) + Roact.unmount(instance) + end) + + it("should render correctly", function () + local container = Instance.new("Folder") + local instance = Roact.mount(createTestTooltip(), container) + local frame = container:FindFirstChildOfClass("Frame") + + expect(frame).to.be.ok() + + Roact.unmount(instance) + end) +end \ No newline at end of file diff --git a/BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Components/TextEntry.lua b/BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Components/TextEntry.lua new file mode 100644 index 0000000000..04040995a1 --- /dev/null +++ b/BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Components/TextEntry.lua @@ -0,0 +1,137 @@ +--[[ + A text entry that is only one line. + Used in a RoundTextBox when Multiline is false. + + Props: + string Text = The text to display + string PlaceholderText = text to display when box is empty/in default state + bool Visible = Whether to display this component + function SetText(text) = Callback to tell parent that text has changed + function FocusChanged(focus) = Callback to tell parent that this component has focus +]] + +local Library = script.Parent.Parent +local Roact = require(Library.Parent.Parent.Roact) + +local Theming = require(Library.Theming) +local withTheme = Theming.withTheme + +local TextEntry = Roact.PureComponent:extend("TextEntry") +local FFlagAllowTextEntryToTakeSizeAndPositionProp = game:DefineFastFlag("AllowTextEntryToTakeSizeAndPositionProp", false) +local FFlagGameSettingsFixNameWhitespace = game:DefineFastFlag("GameSettingsFixNameWhitespace", false) +local FFlagFixTextChangedFromEmptyForTextEntry = game:DefineFastFlag("FixTextChangedFromEmptyForTextEntry", false) + +function TextEntry:init() + self.textBoxRef = Roact.createRef() + self.onTextChanged = function(rbx) + if rbx.TextFits then + rbx.TextXAlignment = Enum.TextXAlignment.Left + else + rbx.TextXAlignment = Enum.TextXAlignment.Right + end + if FFlagFixTextChangedFromEmptyForTextEntry then + if FFlagGameSettingsFixNameWhitespace then + local processed = string.gsub(rbx.Text, "[\n\r]", " ") + self.props.SetText(processed) + else + self.props.SetText(rbx.Text) + end + else + if rbx.Text ~= self.props.Text then + if FFlagGameSettingsFixNameWhitespace then + local processed = string.gsub(rbx.Text, "[\n\r]", " ") + self.props.SetText(processed) + else + self.props.SetText(rbx.Text) + end + end + end + end + + self.mouseEnter = function() + self.props.HoverChanged(true) + end + self.mouseLeave = function() + self.props.HoverChanged(false) + end +end + +function TextEntry:render() + return withTheme(function(theme) + local textSize = self.props.TextSize + local font = self.props.Font + + local textEntryTheme = theme.textEntry + + local size + local position + local textTransparency + local enabled + if FFlagAllowTextEntryToTakeSizeAndPositionProp then + size = self.props.Size and self.props.Size or UDim2.new(1, 0, 1, 0) + position = self.props.Position and self.props.Position or nil + enabled = (self.props.Enabled == nil) and true or self.props.Enabled + textTransparency = enabled and textEntryTheme.textTransparency.enabled or textEntryTheme.textTransparency.disabled + else + size = UDim2.new(1, 0, 1, 0) + position = nil + enabled = nil + textTransparency = nil + end + + return Roact.createElement("Frame", { + Size = size, + BackgroundTransparency = 1, + ClipsDescendants = true, + }, { + Text = Roact.createElement("TextBox", { + Visible = self.props.Visible, + + Size = UDim2.new(1, 0, 1, 0), + Position = position, + BackgroundTransparency = 1, + BorderSizePixel = 0, + + PlaceholderText = self.props.PlaceholderText, + PlaceholderColor3 = self.props.TextColor3, + ClearTextOnFocus = false, + Font = font, + TextSize = textSize, + TextColor3 = self.props.TextColor3, + Text = self.props.Text, + TextXAlignment = Enum.TextXAlignment.Left, + TextTransparency = textTransparency, + TextEditable = enabled, + + [Roact.Ref] = self.textBoxRef, + + [Roact.Event.MouseEnter] = self.mouseEnter, + [Roact.Event.MouseLeave] = self.mouseLeave, + + [Roact.Event.Focused] = function() + self.props.FocusChanged(true) + end, + + [Roact.Event.FocusLost] = function() + -- workaround because we do not disconnect events before we start unmounting host components. + -- see https://github.com/Roblox/roact/issues/235 for more info + if not self.textBoxRef.current then return end + + local textBox = self.textBoxRef.current + textBox.TextXAlignment = Enum.TextXAlignment.Left + self.props.FocusChanged(false) + end, + + [Roact.Change.Text] = function(rbx) + -- workaround because we do not disconnect events before we start unmounting host components. + -- see https://github.com/Roblox/roact/issues/235 for more info + if not self.textBoxRef.current then return end + + self.onTextChanged(rbx) + end + }), + }) + end) +end + +return TextEntry diff --git a/BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Components/TextEntry.spec.lua b/BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Components/TextEntry.spec.lua new file mode 100644 index 0000000000..254be99b19 --- /dev/null +++ b/BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Components/TextEntry.spec.lua @@ -0,0 +1,44 @@ +return function() + local Library = script.Parent.Parent + local Roact = require(Library.Parent.Parent.Roact) + + local MockWrapper = require(Library.MockWrapper) + + local TextEntry = require(script.Parent.TextEntry) + + local function createTestTextEntry(text, visible) + return Roact.createElement(MockWrapper, {}, { + TextEntry = Roact.createElement(TextEntry, { + Text = text, + Visible = visible, + TextSize = 22, + }), + }) + end + + it("should create and destroy without errors", function() + local element = createTestTextEntry() + local instance = Roact.mount(element) + Roact.unmount(instance) + end) + + it("should render correctly", function () + local container = Instance.new("Folder") + local instance = Roact.mount(createTestTextEntry("", true), container) + local frame = container:FindFirstChildOfClass("Frame") + + expect(frame.Text).to.be.ok() + + Roact.unmount(instance) + end) + + it("should hide its text when not visible", function () + local container = Instance.new("Folder") + local instance = Roact.mount(createTestTextEntry("", false), container) + local frame = container:FindFirstChildOfClass("Frame") + + expect(frame.Text.Visible).to.equal(false) + + Roact.unmount(instance) + end) +end \ No newline at end of file diff --git a/BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Components/Timeline/Keyframe.lua b/BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Components/Timeline/Keyframe.lua new file mode 100644 index 0000000000..fe02fb2ad6 --- /dev/null +++ b/BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Components/Timeline/Keyframe.lua @@ -0,0 +1,74 @@ +-- TODO: Delete file when FFlagRemoveUILibraryTimeline is retired +--[[ + A single keyframe which can be displayed on a media timeline. + + Props: + int Width = The size in pixels of the keyframe item. Determines both width and height. + UDim2 Position = The position of the keyframe. + int ZIndex = The display order of the keyframe. + int BorderSizePixel = The size of the keyframe's border highlight. + string Style = A style key for coloring this keyframe. Indexed into the keyframe theme. + + bool Selected = Whether this keyframe is currently selected. Changes the appearance. + + function OnActivated = A callback for when the user clicks on this keyframe. + function OnRightClick = A callback for when the user right-clicks on this keyframe. + function OnInputBegan = A callback for when the user starts interacting with the keyframe. + function OnInputEnded = A callback for when the user stops interacting with the keyframe. +]] + +local Library = script.Parent.Parent.Parent +local Roact = require(Library.Parent.Parent.Roact) +local Theming = require(Library.Theming) +local withTheme = Theming.withTheme + +local DEFAULT_WIDTH = 10 +local DEFAULT_BORDER_SIZE = 2 + +local Keyframe = Roact.PureComponent:extend("Keyframe") + +function Keyframe:render() + return withTheme(function(theme) + local props = self.props + + local style = props.Style + local selected = props.Selected + + local themeBase = style and theme.keyframe[style] or theme.keyframe.Default + local keyframeTheme = selected and themeBase.selected or themeBase + + local position = props.Position + local borderSize = props.BorderSizePixel or DEFAULT_BORDER_SIZE + local width = props.Width or DEFAULT_WIDTH + local zindex = props.ZIndex + + local onActivated = props.OnActivated + local onRightClick = props.OnRightClick + local onInputBegan = props.OnInputBegan + local onInputEnded = props.OnInputEnded + + return Roact.createElement("ImageButton", { + Size = UDim2.new(0, width, 0, width), + AnchorPoint = Vector2.new(0.5, 0.5), + Position = position, + Rotation = 45, + ZIndex = zindex, + + ImageTransparency = 1, + BackgroundTransparency = 0, + AutoButtonColor = false, + + BorderSizePixel = borderSize, + BorderColor3 = keyframeTheme.borderColor, + BackgroundColor3 = keyframeTheme.backgroundColor, + + [Roact.Event.Activated] = onActivated, + [Roact.Event.MouseButton2Click] = onRightClick, + + [Roact.Event.InputBegan] = onInputBegan, + [Roact.Event.InputEnded] = onInputEnded, + }, props[Roact.Children]) + end) +end + +return Keyframe diff --git a/BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Components/Timeline/Keyframe.spec.lua b/BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Components/Timeline/Keyframe.spec.lua new file mode 100644 index 0000000000..57826636bf --- /dev/null +++ b/BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Components/Timeline/Keyframe.spec.lua @@ -0,0 +1,31 @@ +-- TODO: Delete file when FFlagRemoveUILibraryTimeline is retired +return function() + local Library = script.Parent.Parent.Parent + local Roact = require(Library.Parent.Parent.Roact) + + local MockWrapper = require(Library.MockWrapper) + + local Keyframe = require(script.Parent.Keyframe) + + local function createTestKeyframe(enabled, selected) + return Roact.createElement(MockWrapper, {}, { + keyframe = Roact.createElement(Keyframe), + }) + end + + it("should create and destroy without errors", function() + local element = createTestKeyframe() + local instance = Roact.mount(element) + Roact.unmount(instance) + end) + + it("should render correctly", function () + local container = Instance.new("Folder") + local instance = Roact.mount(createTestKeyframe(), container) + local frame = container:FindFirstChildOfClass("ImageButton") + + expect(frame).to.be.ok() + + Roact.unmount(instance) + end) +end \ No newline at end of file diff --git a/BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Components/Timeline/Scrubber.lua b/BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Components/Timeline/Scrubber.lua new file mode 100644 index 0000000000..8ef1706dad --- /dev/null +++ b/BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Components/Timeline/Scrubber.lua @@ -0,0 +1,66 @@ +-- TODO: Delete file when FFlagRemoveUILibraryTimeline is retired +--[[ + Generic implementation of a Scrubber for a Timeline + + Properties: + UDim2 Position = position of the scrubber head + UDim2 HeadSize = size of the scrubber head + float Height = length of the scrubber line + bool ShowHead = whether or not the scrubber head is visible + Vector2 AnchorPoint = anchor point for the Scrubber component + int ZIndex = display order of the scrubber component + int thickness = pixel width of the scrubber line +]] + +local Library = script.Parent.Parent.Parent +local Roact = require(Library.Parent.Parent.Roact) +local Theming = require(Library.Theming) +local withTheme = Theming.withTheme + +local Scrubber = Roact.PureComponent:extend("Scrubber") + +function Scrubber:render() + return withTheme(function(theme) + local props = self.props + + local position = props.Position + local headSize = props.HeadSize + local height = props.Height + local showHead = props.ShowHead + local anchorPoint = props.AnchorPoint + local zIndex = props.ZIndex + local thickness = props.Thickness + + local children = props[Roact.Children] + if not children then + children = {} + end + if showHead then + table.insert(children, Roact.createElement("ImageLabel", { + Image = theme.scrubber.image, + ImageColor3 = theme.scrubber.backgroundColor, + BackgroundTransparency = 1, + Size = UDim2.new(1, 0, 1, 0), + })) + end + + table.insert(children, Roact.createElement("Frame", { + Position = UDim2.new(0.5, 0, 0, 0), + Size = UDim2.new(0, thickness, 0, height), + BackgroundColor3 = theme.scrubber.backgroundColor, + AnchorPoint = Vector2.new(0.5, 0), + BorderSizePixel = 0, + })) + + return Roact.createElement("Frame", { + BackgroundTransparency = 1, + Position = position, + Size = headSize, + ZIndex = zIndex, + AnchorPoint = anchorPoint, + [Roact.Event.InputBegan] = self.onDragBegan, + }, children) + end) +end + +return Scrubber \ No newline at end of file diff --git a/BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Components/Timeline/Scrubber.spec.lua b/BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Components/Timeline/Scrubber.spec.lua new file mode 100644 index 0000000000..4efff8bc32 --- /dev/null +++ b/BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Components/Timeline/Scrubber.spec.lua @@ -0,0 +1,54 @@ +-- TODO: Delete file when FFlagRemoveUILibraryTimeline is retired +return function() + local Library = script.Parent.Parent.Parent + local Roact = require(Library.Parent.Parent.Roact) + + local MockWrapper = require(Library.MockWrapper) + + local Scrubber = require(script.Parent.Scrubber) + + local function createTestScrubber(showHead) + return Roact.createElement(MockWrapper, {}, { + Scrubber = Roact.createElement(Scrubber, { + Height = 1000, + HeadSize = UDim2.new(0, 48, 0, 48), + ShowHead = showHead, + AnchorPoint = Vector2.new(0.5, 0), + Thickness = 1, + }) + }) + end + + it("should create and destroy without errors", function() + local element = createTestScrubber(true) + local instance = Roact.mount(element) + Roact.unmount(instance) + end) + + + describe("should render correctly", function() + it("should render with head correctly", function() + local container = Instance.new("Folder") + local instance = Roact.mount(createTestScrubber(true), container) + local frame = container:FindFirstChildOfClass("Frame") + + expect(frame).to.be.ok() + expect(frame["1"]).to.be.ok() + expect(frame["2"]).to.be.ok() + + Roact.unmount(instance) + end) + + it("should render without head correctly", function() + local container = Instance.new("Folder") + local instance = Roact.mount(createTestScrubber(false), container) + local frame = container:FindFirstChildOfClass("Frame") + + expect(frame).to.be.ok() + expect(#frame:GetChildren()).to.be.equal(1) + expect(frame["1"]).to.be.ok() + + Roact.unmount(instance) + end) + end) +end \ No newline at end of file diff --git a/BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Components/TitledFrame.lua b/BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Components/TitledFrame.lua new file mode 100644 index 0000000000..02a8431675 --- /dev/null +++ b/BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Components/TitledFrame.lua @@ -0,0 +1,57 @@ +--[[ + A frame with a title offset to the left side. + Used as a distinct vertical entry on a SettingsPage. + + Props: + string Title = The text to display in this TitledFrame's left-hand title. + int MaxHeight = The maximum height of this TitledFrame in pixels. Defaults to 100. + int LayoutOrder = The order which this TitledFrame will sort to in a UIListLayout. + int TextSize = The size of text +]] + +local Library = script.Parent.Parent +local Roact = require(Library.Parent.Parent.Roact) +local Theming = require(Library.Theming) +local withTheme = Theming.withTheme + +local CENTER_GUTTER = 180 + +local function TitledFrame(props) + return withTheme(function(theme) + local textSize = props.TextSize + local centerGutter = props.CenterGutter or CENTER_GUTTER + + return Roact.createElement("Frame", { + BackgroundTransparency = 1, + BorderSizePixel = 0, + ZIndex = props.ZIndex or 1, + Size = UDim2.new(1, 0, 0, props.MaxHeight or 100), + LayoutOrder = props.LayoutOrder or 1, + }, { + Title = Roact.createElement("TextLabel", { + BackgroundTransparency = 1, + BorderSizePixel = 0, + Size = UDim2.new(0, centerGutter, 1, 0), + + TextColor3 = theme.titledFrame.text, + Font = theme.titledFrame.font, + TextSize = textSize, + TextXAlignment = Enum.TextXAlignment.Left, + TextYAlignment = Enum.TextYAlignment.Top, + Text = props.Title, + TextWrapped = true, + }), + + Content = Roact.createElement("Frame", { + BackgroundTransparency = 1, + BorderSizePixel = 0, + + Size = UDim2.new(1, -centerGutter, 1, 0), + AnchorPoint = Vector2.new(1, 0), + Position = UDim2.new(1, 0, 0, 0), + }, props[Roact.Children]), + }) + end) +end + +return TitledFrame \ No newline at end of file diff --git a/BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Components/TitledFrame.spec.lua b/BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Components/TitledFrame.spec.lua new file mode 100644 index 0000000000..048a470fbc --- /dev/null +++ b/BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Components/TitledFrame.spec.lua @@ -0,0 +1,34 @@ +return function() + local Library = script.Parent.Parent + local Roact = require(Library.Parent.Parent.Roact) + + local MockWrapper = require(Library.MockWrapper) + + local TitledFrame = require(script.Parent.TitledFrame) + + + local function createTestTitledFrame() + return Roact.createElement(MockWrapper, {}, { + TitledFrame = Roact.createElement(TitledFrame, { + Title = "Title", + }), + }) + end + + it("should create and destroy without errors", function() + local element = createTestTitledFrame() + local instance = Roact.mount(element) + Roact.unmount(instance) + end) + + it("should render correctly", function () + local container = Instance.new("Folder") + local instance = Roact.mount(createTestTitledFrame(), container) + local titledFrame = container:FindFirstChildOfClass("Frame") + + expect(titledFrame.Title).to.be.ok() + expect(titledFrame.Content).to.be.ok() + + Roact.unmount(instance) + end) +end \ No newline at end of file diff --git a/BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Components/ToggleButton.lua b/BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Components/ToggleButton.lua new file mode 100644 index 0000000000..69ac76ab8a --- /dev/null +++ b/BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Components/ToggleButton.lua @@ -0,0 +1,61 @@ +--[[ + A toggle button with on and off state. + + Necessary props: + Position = explicit position, if not placed in UIListLayout + + bool Enabled = Whether or not this button can be clicked. + bool IsOn = whether the button should be on or off + + function onToggle = The function that will be called when this button is clicked to turn on and off + + Optional pros: + int LayoutOrder = The order this ToggleButton will sort to when placed in a UIListLayout +]] + +local Library = script.Parent.Parent +local Roact = require(Library.Parent.Parent.Roact) + +local Theming = require(Library.Theming) +local withTheme = Theming.withTheme + +local ToggleButton = Roact.PureComponent:extend("ToggleButton") + +function ToggleButton:init(props) + self.onToggle = function() + self.props.onToggle(not self.props.IsOn) + end +end + +function ToggleButton:render() + return withTheme(function(theme) + local props = self.props + + local toogleButtonTheme = theme.toggleButton + + local backgroundImage + if props.Enabled then + if props.IsOn then + backgroundImage = toogleButtonTheme.onImage + else + backgroundImage = toogleButtonTheme.offImage + end + else + backgroundImage = toogleButtonTheme.disabledImage + end + + return Roact.createElement("ImageButton", { + BackgroundTransparency = 1, -- Necessary to make the rounded background + Image = backgroundImage, + + Position = props.Position, + Size = props.Size or UDim2.new(0, toogleButtonTheme.defaultWidth, 0, toogleButtonTheme.defaultHeight), + + LayoutOrder = props.LayoutOrder or 1, + + [Roact.Event.Activated] = self.onToggle, + }) + end) +end + +return ToggleButton diff --git a/BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Components/ToggleButton.spec.lua b/BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Components/ToggleButton.spec.lua new file mode 100644 index 0000000000..56fd52742a --- /dev/null +++ b/BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Components/ToggleButton.spec.lua @@ -0,0 +1,30 @@ +return function() + local Library = script.Parent.Parent + local Roact = require(Library.Parent.Parent.Roact) + + local MockWrapper = require(Library.MockWrapper) + + local ToggleButton = require(script.Parent.ToggleButton) + + local function createTestToggleButton() + return Roact.createElement(MockWrapper, {}, { + ToggleButton = Roact.createElement(ToggleButton, { + Size = UDim2.new(0, 20, 0, 20), + Enabled = true, + IsOn = true, + + OnClickedOn = function() + end, + + OnClickedOff = function() + end, + }), + }) + end + + it("should create and destroy without errors", function() + local element = createTestToggleButton() + local instance = Roact.mount(element) + Roact.unmount(instance) + end) +end \ No newline at end of file diff --git a/BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Components/Tooltip.lua b/BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Components/Tooltip.lua new file mode 100644 index 0000000000..dd7f6e7fa3 --- /dev/null +++ b/BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Components/Tooltip.lua @@ -0,0 +1,193 @@ +--[[ + An element which can be added to a component to give that component a tooltip. + When the user hovers the mouse over the component, the tooltip will appear + after a short delay. + + Props: + string Text = The text to display in the tooltip. + float ShowDelay = The time in seconds before the tooltip appears + after the user stops moving the mouse over the element. Defaults to 0.5. + bool Enabled = Whether the tooltip will display on hover. + int Priority = The display order of this element, compared to other focused + elements or elements that show on top. +]] + +local PADDING = 3 +local SHADOW_OFFSET = Vector2.new(3, 3) +local OFFSET = Vector2.new(10, 5) +local SHOW_DELAY_DEFAULT = 0.5 + +local RunService = game:GetService("RunService") +local TextService = game:GetService("TextService") + +local Library = script.Parent.Parent +local Roact = require(Library.Parent.Parent.Roact) + +local Theming = require(Library.Theming) +local withTheme = Theming.withTheme + +local Focus = require(Library.Focus) +local ShowOnTop = Focus.ShowOnTop +local withFocus = Focus.withFocus + +local DropShadow = require(Library.Components.DropShadow) + +local Tooltip = Roact.PureComponent:extend("Tooltip") + +function Tooltip:init(props) + local showDelay = props.ShowDelay or SHOW_DELAY_DEFAULT + + self.state = { + showToolTip = false, + } + + self.isHovered = false + self.mousePos = nil + + self.connectHover = function() + self.hoverConnection = RunService.Heartbeat:Connect(function() + if self.isHovered then + if tick() >= self.targetTime then + self.disconnectHover() + self:setState({ + showToolTip = true, + }) + end + end + end) + end + + self.disconnectHover = function() + if self.hoverConnection then + self.hoverConnection:Disconnect() + end + end + + self.mouseEnter = function(rbx, xpos, ypos) + self.isHovered = true + self.targetTime = tick() + showDelay + self.mousePos = Vector2.new(xpos, ypos) + self.connectHover() + end + + self.mouseMoved = function(rbx, xpos, ypos) + self.mousePos = Vector2.new(xpos, ypos) + self.targetTime = tick() + showDelay + end + + self.mouseLeave = function() + self.isHovered = false + self.targetTime = 0 + self.mousePos = nil + self.disconnectHover() + self:setState({ + showToolTip = false, + }) + end +end + +function Tooltip:willUnmount() + self.disconnectHover() +end + +function Tooltip:render() + return withTheme(function(theme) + return withFocus(function(pluginGui) + local props = self.props + local state = self.state + + local tooltipTheme = theme.tooltip + local textSize = tooltipTheme.textSize + + local text = props.Text + local enabled = props.Enabled + local priority = props.Priority or 0 + + local mousePos = self.mousePos + + local content = {} + + if state.showToolTip and mousePos and enabled and pluginGui then + local targetX = mousePos.X + OFFSET.X + local targetY = mousePos.Y + OFFSET.Y + + local targetWidth = pluginGui.AbsoluteSize.X + local targetHeight = pluginGui.AbsoluteSize.Y + + local textBound = TextService:GetTextSize(text, + textSize, tooltipTheme.font, Vector2.new(100, 9000)) + + local tooltipTargetWidth = textBound.X + 2 * PADDING + local tooltipTargetHeight = textBound.Y + 2 * PADDING + + if targetX + tooltipTargetWidth >= targetWidth then + targetX = targetWidth - tooltipTargetWidth + end + + if targetY + tooltipTargetHeight >= targetHeight then + targetY = targetHeight - tooltipTargetHeight + end + + content.TooltipContainer = Roact.createElement(ShowOnTop, { + Priority = priority, + }, { + Tooltip = Roact.createElement("Frame", { + Position = UDim2.new(0, targetX, 0, targetY), + Size = UDim2.new(0, tooltipTargetWidth, 0, tooltipTargetHeight), + BackgroundTransparency = 1, + ZIndex = 10, + }, { + DropShadow = Roact.createElement(DropShadow, { + Transparency = tooltipTheme.shadowTransparency, + Color = tooltipTheme.shadowColor, + Offset = SHADOW_OFFSET, + ZIndex = 1, + }), + + ContentFrame = Roact.createElement("Frame", { + Size = UDim2.new(1, 0, 1, 0), + ZIndex = 2, + + BackgroundColor3 = tooltipTheme.backgroundColor, + BorderColor3 = tooltipTheme.borderColor, + }, { + UIPadding = Roact.createElement("UIPadding", { + PaddingBottom = UDim.new(0, PADDING), + PaddingLeft = UDim.new(0, PADDING), + PaddingRight = UDim.new(0, PADDING), + PaddingTop = UDim.new(0, PADDING), + }), + + Label = Roact.createElement("TextLabel", { + Size = UDim2.new(1, 0, 1, 0), + BackgroundTransparency = 1, + BorderSizePixel = 0, + Text = text, + + TextColor3 = tooltipTheme.textColor, + + Font = tooltipTheme.font, + TextSize = textSize, + TextXAlignment = Enum.TextXAlignment.Left, + TextYAlignment = Enum.TextYAlignment.Top, + TextWrapped = true, + ZIndex = 3, + }), + }) + }) + }) + end + + return Roact.createElement("Frame",{ + Size = UDim2.new(1, 0, 1, 0), + BackgroundTransparency = 1, + + [Roact.Event.MouseEnter] = self.mouseEnter, + [Roact.Event.MouseMoved] = self.mouseMoved, + [Roact.Event.MouseLeave] = self.mouseLeave, + }, content) + end) + end) +end + +return Tooltip diff --git a/BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Components/Tooltip.spec.lua b/BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Components/Tooltip.spec.lua new file mode 100644 index 0000000000..28c7d6b015 --- /dev/null +++ b/BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Components/Tooltip.spec.lua @@ -0,0 +1,30 @@ +return function() + local Library = script.Parent.Parent + local Roact = require(Library.Parent.Parent.Roact) + + local MockWrapper = require(Library.MockWrapper) + + local Tooltip = require(script.Parent.Tooltip) + + local function createTestTooltip(props) + return Roact.createElement(MockWrapper, {}, { + Tooltip = Roact.createElement(Tooltip, props) + }) + end + + it("should create and destroy without errors", function() + local element = createTestTooltip() + local instance = Roact.mount(element) + Roact.unmount(instance) + end) + + it("should render correctly", function () + local container = Instance.new("Folder") + local instance = Roact.mount(createTestTooltip(), container) + local frame = container:FindFirstChildOfClass("Frame") + + expect(frame).to.be.ok() + + Roact.unmount(instance) + end) +end \ No newline at end of file diff --git a/BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Components/TreeView.lua b/BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Components/TreeView.lua new file mode 100644 index 0000000000..14751f8704 --- /dev/null +++ b/BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Components/TreeView.lua @@ -0,0 +1,524 @@ +--[[ + Displays a heirarchical data set. + + For these notes, assume that : + - DataNode = the type of the data you have passed into the TreeView + + Props: + (required) + dataTree : (DataNode) the first element in some heirarchical data set + getChildren : (function>(DataNode)) gets a list of the node's children + renderElement : (function(table)) renders the current element given a table of props + + (optional) + sortChildren : (function(DataNode, DataNode)) a comparator function passed to table.sort() + onSelectionChanged : (function(list)) a callback for observing when items are selected + expandAll: (bool) a check to determine if the entire tree should be initially expanded. + expandRoot: (bool) a check to determine if the root of the tree should be initially expanded. + createFlatList: (bool) a check to determine if a flat list or node/child structure should be used. + + Elements rendered by the TreeView are given the following props : + -- data information + element : (DataNode) + parent : (DataNode) + + -- styling information + rowIndex : (int) the current row of the element + indent : (int) the current depth of the element + canExpand : (bool) true if the element contains children + isExpanded : (bool) true if the element is currently showing its children + isSelected : (bool) true if the element has been selected, + + -- function callbacks + toggleExpanded : (function()) a function that tells the treeview to expand or collapse this row + toggleSelected : (function(bool)) a function that tells the treeview to select this row +]] +local FFlagStudioFixTreeViewForSquish = settings():GetFFlag("StudioFixTreeViewForSquish") +-- Related Ticket https://jira.rbx.com/browse/CLISTUDIO-21831 +local FFlagStudioFixTreeViewForFlatList = settings():GetFFlag("StudioFixTreeViewForFlatList") +local FFlagFixTreeViewFlatListDefault = game:DefineFastFlag("FixTreeViewFlatListDefault", false) + +local Library = script.Parent.Parent + +local Roact = require(Library.Parent.Parent.Roact) +local Theming = require(Library.Theming) +local withTheme = Theming.withTheme + +local TreeView = Roact.PureComponent:extend("TreeView") + +function TreeView:init() + assert(self.props.dataTree ~= nil, "TreeView expected a dataTree, but none was provided") + assert(type(self.props.getChildren) == "function", "TreeView expected getChildren() to be defined") + if self.props.sortChildren then + assert(type(self.props.sortChildren) == "function", "TreeView expected sortChildren()") + end + + local dataTreeRoot = self.props.dataTree + local getChildrenFunc = self.props.getChildren + local sortChildrenFunc = self.props.sortChildren + local expandAll = self.props.expandAll + local expandRoot = self.props.expandRoot + + self.layoutRef = Roact.createRef() + self.contentRef = Roact.createRef() + self.scrollbarResizeSignalToken = nil + + self.previousNodesArray = {} + self.nodesArray = {} + + self.state = { + expandedItems = {}, + selectedItems = {}, + } + + --[[ + resizeScrollContent : a callback for when the content in the treeview resizes. Ensures that all content can be seen + ]] + self.resizeScrollContent = function() + if not FFlagStudioFixTreeViewForSquish then + return + end + + -- keep the canvas size equal to the size of the content in it + local absoluteContentSize = self.layoutRef.current.AbsoluteContentSize + self.contentRef.current.CanvasSize = UDim2.new(0, absoluteContentSize.X, 0, absoluteContentSize.Y) + end + + --[[ + onTreeUpdated: fires a callback (if provided) for whenever the tree is created/updated. Provides the nodes of the tree + in a list format. + ]] + self.onTreeUpdated = function() + local function treeChanged() + if #self.previousNodesArray ~= #self.nodesArray then + return true + end + + for index, node in ipairs(self.previousNodesArray) do + if self.nodesArray[index] ~= node then + return true + end + end + + return false + end + if self.props.onTreeUpdated and treeChanged() then + self.props.onTreeUpdated(self.nodesArray) + end + end + + --[[ + toggleStateValue : a helper function for changing state values. + + Since Roact only does partial merges of keys, this function is to help ensure that keys get removed + when they are assigned nil. + + Args : + tableName : (string) a table key defined in self.state + element : (DataNode) + copyOldState : (boolean) when true, preserves the old state. When false, removes old state entirely + ]] + self.toggleStateValue = function(tableName, element, copyOldState) + assert(tableName ~= nil, "Expected a table name, but found none") + assert(self.state[tableName] ~= nil, string.format("%s does not exist in the state table", tostring(tableName))) + assert(element ~= nil, string.format("Expected an element to add to %s but found none", tableName)) + assert(type(copyOldState) == "boolean", "Expected copyOldState to be a boolean") + + local newValue = true + if self.state[tableName][element] then + newValue = nil + end + + -- copy the old table and update with the new value + local newState = { + [element] = newValue + } + if copyOldState then + for k, v in pairs(self.state[tableName]) do + if k ~= element then + newState[k] = v + end + end + end + + self:setState({ + [tableName] = newState + }) + + return newState + end + + self.toggleExpandedElement = function(element) + return function() + if element == nil then + return + end + + self.toggleStateValue("expandedItems", element, true) + end + end + + self.toggleSelectedElement = function(element) + -- shouldSelectAlso : (bool) if true, does not clear the previous selection + return function(shouldSelectAlso) + if shouldSelectAlso == nil then + shouldSelectAlso = false + end + assert(type(shouldSelectAlso) == "boolean", "Expected shouldSelectAlso to be a boolean") + + local onSelectionChanged = self.props.onSelectionChanged + local newState = self.toggleStateValue("selectedItems", element, shouldSelectAlso) + + if onSelectionChanged then + -- return a list of selected elements + local selectedData = {} + for k, isSelected in pairs(newState) do + if isSelected then + table.insert(selectedData, k) + end + end + onSelectionChanged(selectedData) + end + end + end + + --[[ + createNode : provides a bunch of props to the consumer's renderElement callback + + Args : + element : (DataNode) + parent : (DataNode) + rowIndex : (int) the current row of the element + indent : (int) the current depth of the element + ]] + -- Remove with FFlagStudioFixTreeViewForSquish + self.DEPRECATED_createNode = function(element, parent, rowIndex, indent) + local expandedItems = self.state.expandedItems + local selectedItems = self.state.selectedItems + local renderElement = self.props.renderElement + local getChildren = self.props.getChildren + + local canExpand = next(getChildren(element)) ~= nil + + local props = { + -- data information + element = element, + parent = parent, + + -- styling information + rowIndex = rowIndex, + indent = indent, + canExpand = canExpand, + isExpanded = expandedItems[element] == true, + isSelected = selectedItems[element] == true, + + -- function callbacks + toggleExpanded = self.toggleExpandedElement(canExpand and element or nil), + toggleSelected = self.toggleSelectedElement(element), + } + + return renderElement(props) + end + + self.createNode = function(element, rowIndex, indent, children) + local expandedItems = self.state.expandedItems + local selectedItems = self.state.selectedItems + local renderElement = self.props.renderElement + local getChildren = self.props.getChildren + + local canExpand = next(getChildren(element)) ~= nil + + local props = { + -- data information + element = element, + + -- styling information + rowIndex = rowIndex, + indent = indent, + canExpand = canExpand, + isExpanded = expandedItems[element] == true, + isSelected = selectedItems[element] == true, + + -- function callbacks + toggleExpanded = self.toggleExpandedElement(canExpand and element or nil), + toggleSelected = self.toggleSelectedElement(element), + + children = children, + } + + return renderElement(props) + end + + --[[ + traverseDepthFirst : visits all of the children in a tree structure, assuming they have been expanded + + Args: + parent : (DataNode) the element to search inside for children + depth : (int) a counter for indenting purposes + handlers : (table) all of the callbacks to properly traverse the tree + - onNodeVisited : (function(DataNode, int, DataNode)) + - decideShouldContinue : (function(DataNode, int, DataNode)) decides if it should continue + - getChildrenOfElement : (function>(DataNode)) gets a list of children from a parent + - sortChildren : (optional, function(DataNode, DataNode) a comparator function to sort the children + ]] + + -- Remove with FFlagStudioFixTreeViewForSquish + self.DEPRECATED_traverseDepthFirst = function(parent, depth, handlers) + local children = handlers.getChildrenOfElement(parent) + + if handlers.sortChildren then + table.sort(children, handlers.sortChildren) + end + + for _, child in pairs(children) do + -- alert any listeners that we've visited this node + handlers.onNodeVisited(child, depth + 1, parent) + + -- check if there are any children of this node we should traverse + local shouldContinue = handlers.decideShouldContinue(child, depth + 1, parent) + if shouldContinue then + self.DEPRECATED_traverseDepthFirst(child, depth + 1, handlers) + end + end + end + + self.traverseDepthFirst = function(current, depth, handlers) + if not handlers.decideShouldContinue(current) then + return handlers.onNodeVisited(current, depth, {}) + end + + local children = handlers.getChildrenOfElement(current) + + local childComponents = {} + + local createFlatList + if FFlagFixTreeViewFlatListDefault then + if self.props.createFlatList == nil then + createFlatList = true + else + createFlatList = self.props.createFlatList + end + else + createFlatList = FFlagStudioFixTreeViewForFlatList and self.props.createFlatList + end + + if handlers.sortChildren then + table.sort(children, handlers.sortChildren) + end + + if createFlatList then + handlers.onNodeVisited(current, depth, {}) + for _, child in pairs(children) do + self.traverseDepthFirst(child, depth + 1, handlers) + end + else + for _, child in pairs(children) do + local childComponent = self.traverseDepthFirst(child, depth + 1, handlers) + table.insert(childComponents, childComponent) + end + + return handlers.onNodeVisited(current, depth, childComponents) + end + end + --[[ + getVisibleNodes : returns a map of the elements to render into the tree, including the root + ]] + self.getVisibleNodes = function() + self.previousNodesArray = self.nodesArray + self.nodesArray = {} + + local expandedItems = self.state.expandedItems + + local root = self.props.dataTree + local getChildren = self.props.getChildren + local sortChildren = self.props.sortChildren + local createFlatList + if FFlagFixTreeViewFlatListDefault then + if self.props.createFlatList == nil then + createFlatList = true + else + createFlatList = self.props.createFlatList + end + else + createFlatList = FFlagStudioFixTreeViewForFlatList and self.props.createFlatList + end + + local numNodes = 1 + local treeNodes + if not FFlagStudioFixTreeViewForSquish then + treeNodes = { + Root = self.DEPRECATED_createNode(root, nil, 0, 0), + } + else + treeNodes = {} + end + + if expandedItems[root] then + if FFlagStudioFixTreeViewForSquish then + treeNodes.Root = self.traverseDepthFirst(root, 0, { + -- upon visiting a node, add it to the map of elements to display + onNodeVisited = function(child, depth, children) + local node = self.createNode(child, numNodes, depth, children) + + if createFlatList then + if node then + local nodeName = string.format("Node-%d", numNodes) + treeNodes[nodeName] = node + numNodes = numNodes + 1 + end + + if FFlagFixTreeViewFlatListDefault then + table.insert(self.nodesArray, child) + end + else + numNodes = numNodes + 1 + return node + end + end, + + -- when deciding whether to continue traversing the child elements, check if it is expanded + decideShouldContinue = function(child) + return expandedItems[child] == true + end, + + -- allow the consumer to figure out how to get the children of each element + getChildrenOfElement = getChildren, + sortChildren = sortChildren }) + else + self.DEPRECATED_traverseDepthFirst(root, 0, { + -- upon visiting a node, add it to the map of elements to display + onNodeVisited = function(child, depth, parent) + local nodeName = string.format("Node-%d", numNodes) + treeNodes[nodeName] = self.DEPRECATED_createNode(child, parent, numNodes, depth) + + numNodes = numNodes + 1 + table.insert(self.nodesArray, child) + end, + + -- when deciding whether to continue traversing the child elements, check if it is expanded + decideShouldContinue = function(child, depth, parent) + return expandedItems[child] == true + end, + + -- allow the consumer to figure out how to get the children of each element + getChildrenOfElement = getChildren, + sortChildren = sortChildren }) + end + elseif FFlagStudioFixTreeViewForSquish then + treeNodes.Root = self.createNode(root, 0 , 0, {}) + end + + return treeNodes + end + + -- if the tree is marked as expandAll, then show all the nodes by default + if expandAll then + local expandedItems = { + [dataTreeRoot] = true, + } + if FFlagStudioFixTreeViewForSquish then + self.traverseDepthFirst(dataTreeRoot, 0, { + onNodeVisited = function(child) + expandedItems[child] = true + end, + decideShouldContinue = function() + return true + end, + getChildrenOfElement = getChildrenFunc, + sortChildren = sortChildrenFunc, + }) + else + self.DEPRECATED_traverseDepthFirst(dataTreeRoot, 0, { + onNodeVisited = function(child) + expandedItems[child] = true + end, + decideShouldContinue = function() + return true + end, + getChildrenOfElement = getChildrenFunc, + sortChildren = sortChildrenFunc, + }) + end + self.state.expandedItems = expandedItems + end + + if expandRoot then + self.state.expandedItems = { + [dataTreeRoot] = true, + } + end +end + +function TreeView:render() + return withTheme(function(theme) + local props = self.props + + local padding = theme.treeView.scrollbar.scrollbarPadding + + local size = FFlagStudioFixTreeViewForSquish and props.Size or UDim2.new(1, -2*padding, 1, -2*padding) + + local layoutOrder = props.LayoutOrder + + local childrenPadding = not FFlagStudioFixTreeViewForSquish and UDim.new(0, theme.treeView.elementPadding) or nil + + local treeViewChildren = { + Layout = Roact.createElement("UIListLayout", { + SortOrder = Enum.SortOrder.LayoutOrder, + FillDirection = Enum.FillDirection.Vertical, + Padding = childrenPadding, + [Roact.Ref] = self.layoutRef, + [Roact.Change.AbsoluteContentSize] = self.resizeScrollContent, + }) + } + + for name, node in pairs(self.getVisibleNodes()) do + -- each of these children will be rendered by the consumer + treeViewChildren[name] = node + end + + return Roact.createElement("ScrollingFrame", { + Position = UDim2.new(0, padding, 0, padding), + Size = size, + BorderSizePixel = 0, + BackgroundTransparency = 1, + ScrollBarThickness = theme.treeView.scrollbar.scrollbarThickness, + ClipsDescendants = true, + LayoutOrder = layoutOrder, + + TopImage = theme.treeView.scrollbar.topImage, + MidImage = theme.treeView.scrollbar.midImage, + BottomImage = theme.treeView.scrollbar.bottomImage, + + ScrollBarImageColor3 = theme.treeView.scrollbar.scrollbarImageColor, + + ElasticBehavior = Enum.ElasticBehavior.Always, + ScrollingDirection = Enum.ScrollingDirection.XY, + + [Roact.Ref] = self.contentRef, + }, treeViewChildren) + end) +end + +function TreeView:didUpdate() + self.onTreeUpdated() +end + +function TreeView:didMount() + if not FFlagStudioFixTreeViewForSquish then + local resizeSignal = self.layoutRef.current:GetPropertyChangedSignal("AbsoluteContentSize") + self.scrollbarResizeSignalToken = resizeSignal:Connect(self.resizeScrollContent) + end + + self.onTreeUpdated() +end + +function TreeView:didUnmount() + self.previousNodesArray = nil + self.nodesArray = nil + if self.scrollbarResizeSignalToken then + self.scrollbarResizeSignalToken:Disconnect() + self.scrollbarResizeSignalToken = nil + end +end + +return TreeView \ No newline at end of file diff --git a/BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Components/TreeView.spec.lua b/BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Components/TreeView.spec.lua new file mode 100644 index 0000000000..482c173dc5 --- /dev/null +++ b/BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Components/TreeView.spec.lua @@ -0,0 +1,355 @@ +local TreeView = require(script.Parent.TreeView) + +local Library = script.Parent.Parent +local Roact = require(Library.Parent.Parent.Roact) +local MockWrapper = require(Library.MockWrapper) + +local FFlagStudioFixTreeViewForSquish = settings():GetFFlag("StudioFixTreeViewForSquish") + +local function mockDataNode(value, parent) + local Node = { + Value = value, + Children = {}, + } + + if parent then + table.insert(parent.Children, Node) + end + + return Node +end + +local function mockDataTree() + local root = mockDataNode("Players") + local node1 = mockDataNode("John Doe", root) + local node2 = mockDataNode("Jane Doe", root) + local node3 = mockDataNode("Builderman", root) + mockDataNode("Sword", node1) + mockDataNode("Shield", node1) + mockDataNode("Gun", node2) + mockDataNode("Hammer", node3) + + return root +end + +local function mockGetChildren(node) + return node.Children +end + +local function mockRenderElement(props) + return Roact.createElement("Frame",{}) +end + +local function mockSortChildren(nodeA, nodeB) + return nodeA.Value > nodeB.Value +end + +return function() + describe("TreeView", function() + it("should create and destroy without errors", function() + local element = Roact.createElement(MockWrapper, {}, { + TreeView = Roact.createElement(TreeView, { + dataTree = mockDataTree(), + getChildren = mockGetChildren, + renderElement = mockRenderElement, + + -- optional props + sortChildren = mockSortChildren, + }), + }) + local container = Instance.new("Frame") + local instance = Roact.mount(element, container) + Roact.unmount(instance) + end) + + it("should error when it is missing important props", function() + expect(function() + local element = Roact.createElement(MockWrapper, {}, { + TreeView = Roact.createElement(TreeView, { + dataTree = mockDataTree(), + getChildren = mockGetChildren, + }), + }) + local instance = Roact.mount(element) + Roact.unmount(instance) + end).to.throw() + + expect(function() + local element = Roact.createElement(MockWrapper, {}, { + TreeView = Roact.createElement(TreeView, { + dataTree = mockDataTree, + renderElement = mockRenderElement, + }), + }) + local instance = Roact.mount(element) + Roact.unmount(instance) + end).to.throw() + + expect(function() + local element = Roact.createElement(MockWrapper, {}, { + TreeView = Roact.createElement(TreeView, { + getChildren = mockGetChildren, + renderElement = mockRenderElement, + }), + }) + local instance = Roact.mount(element) + Roact.unmount(instance) + end).to.throw() + end) + + it("should render correctly", function() + local element = Roact.createElement(MockWrapper, {}, { + TreeView = Roact.createElement(TreeView, { + dataTree = mockDataTree(), + getChildren = mockGetChildren, + renderElement = mockRenderElement, + }), + }) + local container = Instance.new("Folder") + local instance = Roact.mount(element, container) + + local treeView = container:FindFirstChildOfClass("ScrollingFrame") + expect(treeView).to.be.ok() + expect(treeView.Root).to.be.ok() + expect(treeView.Layout).to.be.ok() + + Roact.unmount(instance) + end) + + it("should render the children when you expand the node", function() + local nodesRenderedCount = 0 + local expandChildFunc + local function renderElement(props) + -- if rendering the root node, grab the callback to expand it + if props.element.Value == "Players" then + expandChildFunc = props.toggleExpanded + end + + nodesRenderedCount = nodesRenderedCount + 1 + + -- create an element + return Roact.createElement("TextLabel", { + Text = props.element.Value + }, props.children) + end + + local count = 0 + local function dfCount(root) + local children = root:GetChildren() + count = count + 1 + + if #children == 0 then + return + end + + for _, child in ipairs(children) do + dfCount(child) + end + end + + -- render the tree + local element = Roact.createElement(MockWrapper, {}, { + TreeView = Roact.createElement(TreeView, { + dataTree = mockDataTree(), + getChildren = mockGetChildren, + renderElement = renderElement, + }), + }) + local container = Instance.new("Folder") + local instance = Roact.mount(element, container) + local treeView = container:FindFirstChildOfClass("ScrollingFrame") + + -- Remove with FFlagStudioFixTreeViewForSquish + local renderedChildren + renderedChildren = treeView:GetChildren() + + if FFlagStudioFixTreeViewForSquish then + dfCount(treeView) + expect(count).to.equal(nodesRenderedCount + 2)-- should equal number of nodes + 1 UIListLayout + 1 for RoactTree + else + expect(#renderedChildren).to.equal(nodesRenderedCount + 1)-- should equal number of nodes + 1 UIListLayout + end + expect(nodesRenderedCount).to.equal(1) + + -- expand the root node, it should re-render the root node and its three children + nodesRenderedCount = 0 + expandChildFunc() + + -- it should have rendered the children + treeView = container:FindFirstChildOfClass("ScrollingFrame") + renderedChildren = treeView:GetChildren() + if FFlagStudioFixTreeViewForSquish then + count = 0 + dfCount(treeView) + expect(count).to.equal(nodesRenderedCount + 2)-- should equal number of nodes + 1 UIListLayout + 1 for RoactTree + else + expect(#renderedChildren).to.equal(nodesRenderedCount + 1)-- should equal number of nodes + 1 UIListLayout + end + + local foundChildNodes = 0 + local foundRoot = false + local foundChild1 = false + local foundChild2 = false + local foundChild3 = false + if FFlagStudioFixTreeViewForSquish then + local function dfs(node) + local children = node:GetChildren() + + if node:IsA("TextLabel") then + foundChildNodes = foundChildNodes + 1 + if node.Text == "Players" then + foundRoot = true + elseif node.Text == "John Doe" then + foundChild1 = true + elseif node.Text == "Jane Doe" then + foundChild2 = true + elseif node.Text == "Builderman" then + foundChild3 = true + end + end + + if #children == 0 then + return + end + + for _, child in ipairs(children) do + dfs(child) + end + end + + dfs(treeView) + else + for _, childNode in ipairs(renderedChildren) do + if childNode:IsA("TextLabel") then + foundChildNodes = foundChildNodes + 1 + if childNode.Text == "Players" then + foundRoot = true + elseif childNode.Text == "John Doe" then + foundChild1 = true + elseif childNode.Text == "Jane Doe" then + foundChild2 = true + elseif childNode.Text == "Builderman" then + foundChild3 = true + end + end + end + end + expect(foundChildNodes).to.equal(nodesRenderedCount) + expect(foundRoot).to.equal(true) + expect(foundChild1).to.equal(true) + expect(foundChild2).to.equal(true) + expect(foundChild3).to.equal(true) + + -- clean up + Roact.unmount(instance) + end) + + it("should allow you to select one or multiple elements in the tree", function() + local isRootSelected = false + local selectNodeFunc + + local function renderElement(props) + isRootSelected = props.isSelected + selectNodeFunc = props.toggleSelected + + return Roact.createElement("Frame") + end + + local element = Roact.createElement(MockWrapper, {}, { + TreeView = Roact.createElement(TreeView, { + dataTree = mockDataTree(), + getChildren = mockGetChildren, + renderElement = renderElement, + }), + }) + local container = Instance.new("Folder") + local instance = Roact.mount(element, container) + + -- select the root node + expect(isRootSelected).to.equal(false) + selectNodeFunc() + expect(isRootSelected).to.equal(true) + + Roact.unmount(instance) + end) + + it("should render all of the children immediately if expandAll is set", function() + local nodeCount = 0 + local renderElement = function(props) + nodeCount = nodeCount + 1 + return Roact.createElement("Frame", {}, props.children) + end + + local element = Roact.createElement(MockWrapper, {}, { + TreeView = Roact.createElement(TreeView, { + dataTree = mockDataTree(), + getChildren = mockGetChildren, + renderElement = renderElement, + expandAll = true, + }), + }) + local container = Instance.new("Folder") + local instance = Roact.mount(element, container) + + local treeView = container:FindFirstChildOfClass("ScrollingFrame") + local treeViewChildren = treeView:GetChildren() + + local count = 0 + local function dfCount(root) + local children = root:GetChildren() + count = count + 1 + + if #children == 0 then + return + end + + for _, child in ipairs(children) do + dfCount(child) + end + end + + -- mockDataTree has 8 nodes + expect(nodeCount).to.equal(8) + if FFlagStudioFixTreeViewForSquish then + dfCount(treeView) + expect(count).to.equal(nodeCount + 2) + else + -- there should be 8 nodes + 1 UIListLayout + expect(#treeViewChildren).to.equal(nodeCount + 1) + end + + Roact.unmount(instance) + end) + + itSKIP("should fire update callback", function() + local nodeCount = 0 + local renderElement = function(props) + nodeCount = nodeCount + 1 + return Roact.createElement("Frame", {}) + end + + local numInvoked = 0 + local treeList = nil + local element = Roact.createElement(MockWrapper, {}, { + TreeView = Roact.createElement(TreeView, { + dataTree = mockDataTree(), + getChildren = mockGetChildren, + renderElement = renderElement, + expandAll = true, + onTreeUpdated = function(tree) + treeList = tree + numInvoked = numInvoked + 1 + end, + }), + }) + local container = Instance.new("Folder") + local instance = Roact.mount(element, container) + + expect(treeList).to.be.ok() + expect(#treeList).to.equal(nodeCount) + expect(numInvoked).to.equal(1) + + Roact.unmount(instance) + end) + end) +end diff --git a/BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Components/createFitToContent.lua b/BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Components/createFitToContent.lua new file mode 100644 index 0000000000..014946770f --- /dev/null +++ b/BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Components/createFitToContent.lua @@ -0,0 +1,70 @@ +--[[ + This component generates a list of elements and resizes the container so that it fits the size of the list. + + Call the function createFitToContent and pass in the container, the layout of the elements, and any properties +]] +local FFlagFixFitToContentOnCloseError = game:DefineFastFlag("FixFitToContentOnCloseError", false) + +local Library = script.Parent.Parent +local Roact = require(Library.Parent.Parent.Roact) + +local join = require(Library.join) + +local function createFitToContent(containerComponent, layoutComponent, layoutProps) + local name = ("FitComponent(%s, %s)"):format(containerComponent, layoutComponent) + local FitComponent = Roact.Component:extend(name) + + function FitComponent:init() + self.layoutRef = Roact.createRef() + self.containerRef = Roact.createRef() + + self.layoutProps = join(layoutProps, { + [Roact.Ref] = self.layoutRef, + [Roact.Change.AbsoluteContentSize] = function() + if self.layoutRef.current ~= nil and self.containerRef.current ~= nil then + self:resizeContainer() + end + end, + }) + end + + function FitComponent:render() + assert(self.props.Size == nil, "Size must not be specified!") + + local children = join({ + ["Layout"] = Roact.createElement(layoutComponent, self.layoutProps), + }, self.props[Roact.Children]) + + local props = join(self.props, { + [Roact.Children] = children, + [Roact.Ref] = self.containerRef, + }) + + return Roact.createElement(containerComponent, props) + end + + function FitComponent:didMount() + self:resizeContainer() + end + + function FitComponent:didUpdate() + self:resizeContainer() + end + + function FitComponent:resizeContainer() + if FFlagFixFitToContentOnCloseError then + local layout = self.layoutRef.current + if layout then + local layoutSize = layout.AbsoluteContentSize + self.containerRef.current.Size = UDim2.new(1, 0, 0, layoutSize.Y) + end + else + local layoutSize = self.layoutRef.current.AbsoluteContentSize + self.containerRef.current.Size = UDim2.new(1, 0, 0, layoutSize.Y) + end + end + + return FitComponent +end + +return createFitToContent \ No newline at end of file diff --git a/BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Components/createFitToContent.spec.lua b/BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Components/createFitToContent.spec.lua new file mode 100644 index 0000000000..ed5fa5f239 --- /dev/null +++ b/BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Components/createFitToContent.spec.lua @@ -0,0 +1,44 @@ +return function() + local Library = script.Parent.Parent +local Roact = require(Library.Parent.Parent.Roact) + + local createFitToContent = require(script.Parent.createFitToContent) + + it("should create and destroy without errors", function() + local fitToContent = createFitToContent("Frame", "UIListLayout", {}) + local component = Roact.createElement(fitToContent, {}, {}) + local instance = Roact.mount(component) + + Roact.unmount(instance) + end) + + it("should throw an error if size is specified", function() + local fitToContent = createFitToContent("Frame", "UIListLayout", {}) + local component = Roact.createElement(fitToContent, { + Size = UDim2.new() + }, {}) + + expect(function() + Roact.mount(component) + end).to.throw() + end) + + it("should add a Layout to its children", function() + local container = Instance.new("Folder") + + local fitToContent = createFitToContent("Frame", "UIListLayout", {}) + local component = Roact.createElement(fitToContent, {}, { + Frame1 = Roact.createElement("Frame"), + Frame2 = Roact.createElement("Frame"), + }) + + local instance = Roact.mount(component, container) + local frame = container:FindFirstChildOfClass("Frame") + + expect(frame.Frame1).to.be.ok() + expect(frame.Frame2).to.be.ok() + expect(frame.Layout).to.be.ok() + + Roact.unmount(instance) + end) +end \ No newline at end of file diff --git a/BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Focus.lua b/BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Focus.lua new file mode 100644 index 0000000000..2aa90263cf --- /dev/null +++ b/BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Focus.lua @@ -0,0 +1,181 @@ +--[[ + A utility for elements that need to display on top of all other elements, + and elements that need to capture focus and block input to all other elements. + + Uses Portals to place elements in the main PluginGui. You will need to + pass in the main PluginGui when creating a FocusProvider. + + withFocus(pluginGui) + Gets the top-level PluginGui. Useful for querying its size or state. + For example, a Tooltip queries the size of the pluginGui to avoid + clipping the tooltip off of the right or bottom side of the screen. + + ShowOnTop + A Roact component that wraps its children such that they will be + rendered on top of all other components. + Props: + int Priority = The ZIndex of this component relative to other + focused elements. + + CaptureFocus + A Roact component that wraps its children such that they will be + rendered on top of all other components, and will block input to all + other components. + + Props: + int Priority = The ZIndex of this component relative to other + focused elements. + callback OnFocusLost = A callback for when the user clicks + outside of the focused element. + + KeyboardListener + A Roact component that listens to keyboard events within the PluginGui. + + Props: + callback OnKeyPressed(input, keysHeld) + A callback for when the user presses a key inside the plugin. + The input param is the InputObject for the InputBegan event. The + keysHeld param is a map containing every key that is currently held. + callback OnKeyReleased(input) + A callback for when the user releases a key. +]] + +local FOCUSED_ZINDEX = 100000 +local Library = script.Parent +local Roact = require(Library.Parent.Parent.Roact) +local Symbol = require(Library.Utils.Symbol) +local focusKey = Symbol.named("UILibraryFocus") + +local FocusProvider = Roact.PureComponent:extend("UILibraryFocusProvider") +function FocusProvider:init() + local pluginGui = self.props.pluginGui + assert(pluginGui ~= nil, "No pluginGui was given to this FocusProvider.") + + self._context[focusKey] = pluginGui +end +function FocusProvider:render() + return Roact.oneChild(self.props[Roact.Children]) +end + +-- the consumer should complain if it doesn't have a focus +local FocusConsumer = Roact.PureComponent:extend("UILibraryFocusConsumer") +function FocusConsumer:init() + assert(self._context[focusKey] ~= nil, "No FocusProvider found.") + assert(self.props.focusedRender ~= nil, "Use withFocus, not FocusConsumer.") + self.pluginGui = self._context[focusKey] +end +function FocusConsumer:render() + return self.props.focusedRender(self.pluginGui) +end + +-- withFocus should provide a simple way to make components that use focus +-- callback : function(FocusConsumer) +local function withFocus(callback) + return Roact.createElement(FocusConsumer, { + focusedRender = callback + }) +end + +local CaptureFocus = Roact.PureComponent:extend("UILibraryCaptureFocus") +function CaptureFocus:render() + return withFocus(function(pluginGui) + local priority = self.props.Priority or 0 + return Roact.createElement(Roact.Portal, { + target = pluginGui, + }, { + -- Consume all clicks outside the element to close it when it loses focus + TopLevelDetector = Roact.createElement("ImageButton", { + ZIndex = priority + FOCUSED_ZINDEX, + Size = UDim2.new(1, 0, 1, 0), + BackgroundTransparency = 1, + [Roact.Event.Activated] = self.props.OnFocusLost, + }, { + -- Also block all scrolling events going through + ScrollBlocker = Roact.createElement("ScrollingFrame", { + Size = UDim2.new(1, 0, 1, 0), + -- We need to have ScrollingEnabled = true for this frame for it to block + -- But we don't want it to actually scroll, so its canvas must be same size as the frame + ScrollingEnabled = true, + CanvasSize = UDim2.new(1, 0, 1, 0), + BackgroundTransparency = 1, + ScrollBarThickness = 0, + }, self.props[Roact.Children]), + }), + }) + end) +end + +local ShowOnTop = Roact.PureComponent:extend("UILibraryShowOnTop") +function ShowOnTop:render() + return withFocus(function(pluginGui) + local priority = self.props.Priority or 0 + return Roact.createElement(Roact.Portal, { + target = pluginGui, + }, { + TopLevelFrame = Roact.createElement("Frame", { + ZIndex = priority + FOCUSED_ZINDEX, + Size = UDim2.new(1, 0, 1, 0), + BackgroundTransparency = 1, + }, self.props[Roact.Children]), + }) + end) +end + +local KeyboardListener = Roact.PureComponent:extend("KeyboardListener") +function KeyboardListener:init() + self.keysHeld = {} + assert(self._context[focusKey] ~= nil, "No FocusProvider found.") + self.pluginGui = self._context[focusKey] + + self.onInputBegan = function(input) + if input.UserInputType == Enum.UserInputType.Keyboard then + self.keysHeld[input.KeyCode] = true + self.props.OnKeyPressed(input, self.keysHeld) + end + end + self.onInputEnded = function(input) + if input.UserInputType == Enum.UserInputType.Keyboard then + self.keysHeld[input.KeyCode] = nil + self.props.OnKeyReleased(input) + end + end + if self.pluginGui:IsA("DockWidgetPluginGui") then + self.focusConnection = self.pluginGui.WindowFocusReleased:Connect(function() + for key, _ in pairs(self.keysHeld) do + self.props.OnKeyReleased({ + KeyCode = key, + UserInputType = Enum.UserInputType.Keyboard, + }) + end + self.keysHeld = {} + end) + end +end +function KeyboardListener:render() + return Roact.createElement(ShowOnTop, {}, { + Listener = Roact.createElement("Frame", { + Size = UDim2.new(1, 0, 1, 0), + BackgroundTransparency = 1, + [Roact.Event.InputBegan] = function(_, input) + self.onInputBegan(input) + end, + [Roact.Event.InputEnded] = function(_, input) + self.onInputEnded(input) + end, + }), + }) +end +function KeyboardListener:willUnmount() + if self.focusConnection then + self.focusConnection:Disconnect() + end +end + +return { + Provider = FocusProvider, + Consumer = FocusConsumer, + CaptureFocus = CaptureFocus, + ShowOnTop = ShowOnTop, + KeyboardListener = KeyboardListener, + withFocus = withFocus, +} \ No newline at end of file diff --git a/BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Focus.spec.lua b/BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Focus.spec.lua new file mode 100644 index 0000000000..4be889f2e9 --- /dev/null +++ b/BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Focus.spec.lua @@ -0,0 +1,118 @@ +return function() + local Library = script.Parent + local Roact = require(Library.Parent.Parent.Roact) + + local MockWrapper = require(Library.MockWrapper) + + local Focus = require(script.Parent.Focus) + local ShowOnTop = Focus.ShowOnTop + local CaptureFocus = Focus.CaptureFocus + local KeyboardListener = Focus.KeyboardListener + + describe("ShowOnTop", function() + local function createTestShowOnTop(children, container) + return Roact.createElement(MockWrapper, { + Container = container, + }, { + ShowOnTop = Roact.createElement(ShowOnTop, {}, children) + }) + end + + it("should create and destroy without errors", function() + local element = createTestShowOnTop() + local instance = Roact.mount(element) + Roact.unmount(instance) + end) + + it("should render correctly", function() + local container = Instance.new("Folder") + local element = createTestShowOnTop({}, container) + local instance = Roact.mount(element) + + local gui = container:FindFirstChild("MockGui") + expect(gui).to.be.ok() + expect(gui.TopLevelFrame).to.be.ok() + Roact.unmount(instance) + end) + + it("should render its children", function() + local container = Instance.new("Folder") + local element = createTestShowOnTop({ + ChildFrame = Roact.createElement("Frame"), + }, container) + local instance = Roact.mount(element) + + local gui = container:FindFirstChild("MockGui") + expect(gui.TopLevelFrame.ChildFrame).to.be.ok() + Roact.unmount(instance) + end) + end) + + describe("CaptureFocus", function() + local function createTestCaptureFocus(children, container) + return Roact.createElement(MockWrapper, { + Container = container, + }, { + CaptureFocus = Roact.createElement(CaptureFocus, {}, children) + }) + end + + it("should create and destroy without errors", function() + local element = createTestCaptureFocus() + local instance = Roact.mount(element) + Roact.unmount(instance) + end) + + it("should render correctly", function() + local container = Instance.new("Folder") + local element = createTestCaptureFocus({}, container) + local instance = Roact.mount(element) + + local gui = container:FindFirstChild("MockGui") + expect(gui).to.be.ok() + expect(gui.TopLevelDetector).to.be.ok() + expect(gui.TopLevelDetector.ScrollBlocker).to.be.ok() + Roact.unmount(instance) + end) + + it("should render its children", function() + local container = Instance.new("Folder") + local element = createTestCaptureFocus({ + ChildFrame = Roact.createElement("Frame"), + }, container) + local instance = Roact.mount(element) + + local gui = container:FindFirstChild("MockGui") + expect(gui.TopLevelDetector.ScrollBlocker.ChildFrame).to.be.ok() + Roact.unmount(instance) + end) + end) + + describe("KeyboardListener", function() + local function createTestKeyboardListener(container) + return Roact.createElement(MockWrapper, { + Container = container, + }, { + KeyboardListener = Roact.createElement(KeyboardListener) + }) + end + + it("should create and destroy without errors", function() + local element = createTestKeyboardListener() + local instance = Roact.mount(element) + Roact.unmount(instance) + end) + + it("should render correctly", function() + local container = Instance.new("Folder") + local element = createTestKeyboardListener(container) + local instance = Roact.mount(element) + + local gui = container:FindFirstChild("MockGui") + expect(gui).to.be.ok() + expect(gui.TopLevelFrame).to.be.ok() + expect(gui.TopLevelFrame.Listener).to.be.ok() + Roact.unmount(instance) + end) + end) +end \ No newline at end of file diff --git a/BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Localizing.lua b/BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Localizing.lua new file mode 100644 index 0000000000..1c8f65af37 --- /dev/null +++ b/BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Localizing.lua @@ -0,0 +1,98 @@ +--[[ + A utility for elements that require text to be localized and display translated + strings. + + When initializing LocalizationProvider, it expects a Localization object, an example being + src/Studio/Localization.lua where there is two tables for development strings and translated + strings. withLocalization is mainly used to render elements with the localized strings using + the localization object passed into LocalizationProvider +]] + +local Library = script.Parent + +local Roact = require(Library.Parent.Parent.Roact) +local Symbol = require(Library.Utils.Symbol) + + +local localizationKey = Symbol.named("Localization") + +--[[ + Inserted near the top of the Roact tree, this stores the localization object into _context + + Props: + localization : (Localization) an object that can provide localized strings, preferrably a Localization object +]] +local LocalizationProvider = Roact.PureComponent:extend("LocalizationProvider") + +function LocalizationProvider:init() + assert(self.props.localization ~= nil, "LocalizationProvider expects a Localization object") + local localization = self.props.localization + + self._context[localizationKey] = localization +end + +function LocalizationProvider:render() + return Roact.oneChild(self.props[Roact.Children]) +end + + +--[[ + Never explicitly created by the user, it exposes the localization object from context. + This should only ever be created by a call to withLocalization(). + + Props: + localizedRender : (function(Localization localization)) + a callback used to render children while exposing the Localization object stored in _context +]] +local LocalizationConsumer = Roact.PureComponent:extend("LocalizationConsumer") + +function LocalizationConsumer:init() + assert(type(self.props.localizedRender) == "function", "LocalizationConsumer expected to be created with withLocale()") + assert(self._context[localizationKey] ~= nil, "LocalizationConsumer expects a LocalizationProvider in the Roact tree") + + self.localization = self._context[localizationKey] + self.state = { + -- keep a simple string of the table reference so we have something to call setstate on later + localization = tostring(self.localization.translator) + } + + self.lcToken = self.localization.localeChanged:connect(function(newLocale) + -- force trigger a re-render of children + self:setState({ + localization = tostring(newLocale) + }) + end) +end + +function LocalizationConsumer:render() + return self.props.localizedRender(self.localization) +end + +function LocalizationConsumer:willUnmount() + if self.lcToken then + self.lcToken:disconnect() + self.lcToken = nil + end +end + +--[[ + callback : function(Localization localization) + a callback used to render children while exposing the localization stored in _context +]] +local function withLocalization(callback) + assert(type(callback) == "function", "withLocalization expects a function") + return Roact.createElement(LocalizationConsumer, { + localizedRender = callback + }) +end + +local function getLocalization(component) + return component._context[localizationKey] +end + +return { + Provider = LocalizationProvider, + Consumer = LocalizationConsumer, + withLocalization = withLocalization, + getLocalization = getLocalization, +} diff --git a/BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Localizing.spec.lua b/BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Localizing.spec.lua new file mode 100644 index 0000000000..3840e0ef41 --- /dev/null +++ b/BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Localizing.spec.lua @@ -0,0 +1,157 @@ +local Localizing = require(script.Parent.Localizing) + +local Library = script.Parent +local Roact = require(Library.Parent.Parent.Roact) +local Signal = require(Library.Utils.Signal) +local Localization = require(Library.Studio.Localization) + +local LocalizationProvider = Localizing.Provider +local LocalizationConsumer = Localizing.Consumer +local withLocalization = Localizing.withLocalization + +return function() + describe("LocalizationProvider", function() + it("should construct/deconstruct without a problem", function() + local localization = Localization.mock() + + local root = Roact.createElement(LocalizationProvider, { + localization = localization + }) + local handle = Roact.mount(root) + Roact.unmount(handle) + + localization:destroy() + end) + + it("should error if a localization object isn't provided", function() + expect(function() + local root = Roact.createElement(LocalizationProvider, {}) + local instance = Roact.mount(root) + Roact.unmount(instance) + end).to.throw() + end) + end) + + + describe("LocalizationConsumer", function() + it("should construct/deconstruct without a problem when used appropriately", function() + local mockLocalization = Localization.mock() + + local function createTestElement() + return withLocalization(function(localization) + return Roact.createElement("TextLabel", { + Text = localization:getText("Anything", "test") + }) + end) + end + + local root = Roact.createElement(LocalizationProvider, { + localization = mockLocalization + },{ + Roact.createElement(createTestElement, {}) + }) + + local handle = Roact.mount(root) + Roact.unmount(handle) + + mockLocalization:destroy() + end) + + it("should error if constructed without a LocalizationProvider in the Roact tree", function() + local function createTestElement() + return withLocalization(function(localization) + return Roact.createElement("TextLabel", { + Text = localization:getText("Anything", "test") + }) + end) + end + + expect(function() + local root = Roact.createElement(createTestElement) + local instance = Roact.mount(root) + Roact.unmount(instance) + end).to.throw() + end) + + it("should error if you construct it on its own", function() + expect(function() + local root = Roact.createElement(LocalizationConsumer, {}) + local instance = Roact.mount(root) + Roact.unmount(instance) + end).to.throw() + end) + + it("should re-render its contents if the localization changes", function() + local changeSignal = Signal.new() + local localization = Localization.mock(changeSignal) + + -- create a test element and keep track of how many times it renders + local renderCount = 0 + + local TestElement = Roact.PureComponent:extend("TestElement") + function TestElement:render() + renderCount = renderCount + 1 + local text = self.props.text + + return Roact.createElement("TextLabel", { + Text = text + }) + end + + -- create the roact tree + local root = Roact.createElement(LocalizationProvider, { + localization = localization + },{ + Roact.createElement(function() + return withLocalization(function(localizationObject) + return Roact.createElement(TestElement, { + text = localizationObject:getText("Test", "hello_world") + }) + end) + end) + }) + + local instance = Roact.mount(root) + expect(renderCount).to.equal(1) + + -- trigger a locale change + changeSignal:fire() + expect(renderCount).to.equal(2) + + -- clean up + Roact.unmount(instance) + localization:destroy() + end) + end) + + + describe("withLocalization()", function() + it("should error if a render callback isn't provided", function() + expect(function() + withLocalization() + end).to.throw() + end) + + it("should expose the stored localization object", function() + local mockLocalization = Localization.mock() + local foundLocalization = nil + + local function localizedRender(localization) + foundLocalization = localization + return Roact.createElement("TextLabel") + end + + local root = Roact.createElement(LocalizationProvider, { + localization = mockLocalization + },{ + Roact.createElement(function() + return withLocalization(localizedRender) + end) + }) + local instance = Roact.mount(root) + Roact.unmount(instance) + + expect(foundLocalization).to.equal(mockLocalization) + end) + end) +end \ No newline at end of file diff --git a/BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/MockWrapper.lua b/BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/MockWrapper.lua new file mode 100644 index 0000000000..1dc9b74743 --- /dev/null +++ b/BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/MockWrapper.lua @@ -0,0 +1,44 @@ +--[[ + USE IN TESTS ONLY + Provides mocks of all necessary context items for testing. +]] + +local Library = script.Parent +local Roact = require(Library.Parent.Parent.Roact) + +local UILibraryWrapper = require(Library.UILibraryWrapper) + +local createTheme = require(Library.createTheme) +local dummyTheme = createTheme() + +local MockWrapper = Roact.PureComponent:extend("MockWrapper") + +local function createMockPlugin(container) + return { + CreateQWidgetPluginGui = function() + return Instance.new("BillboardGui", container) + end + } +end + +function MockWrapper:init(props) + local container = props.Container + + self.mockGui = Instance.new("ScreenGui", container) + self.mockGui.Name = "MockGui" + self.mockPlugin = createMockPlugin(container) +end + +function MockWrapper:render() + return Roact.createElement(UILibraryWrapper, { + theme = dummyTheme, + focusGui = self.mockGui, + plugin = self.mockPlugin, + }, self.props[Roact.Children]) +end + +function MockWrapper:willUnmount() + self.mockGui:Destroy() +end + +return MockWrapper \ No newline at end of file diff --git a/BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Plugin.lua b/BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Plugin.lua new file mode 100644 index 0000000000..1eb3a1fec5 --- /dev/null +++ b/BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Plugin.lua @@ -0,0 +1,28 @@ +local Library = script.Parent + +local Roact = require(Library.Parent.Parent.Roact) +local Symbol = require(Library.Utils.Symbol) +local pluginKey = Symbol.named("Plugin") + +local PluginProvider = Roact.PureComponent:extend("PluginProvider") +function PluginProvider:init() + local plugin = self.props.plugin + assert(plugin ~= nil, "No plugin was given to this PluginProvider.") + + self._context[pluginKey] = plugin +end +function PluginProvider:render() + return Roact.oneChild(self.props[Roact.Children]) +end + +-- Gets the plugin at the passed in component's context. +local function getPlugin(component) + assert(component._context[pluginKey] ~= nil, "No PluginProvider found.") + local plugin = component._context[pluginKey] + return plugin +end + +return { + Provider = PluginProvider, + getPlugin = getPlugin, +} \ No newline at end of file diff --git a/BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Studio/Analytics.lua b/BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Studio/Analytics.lua new file mode 100644 index 0000000000..e08e32d7dc --- /dev/null +++ b/BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Studio/Analytics.lua @@ -0,0 +1,123 @@ +--[[ + Providable helper for sending analytics events and reporting counters. + Consumers can use this utility as a wrapper for AnalyticsService, + allowing mock Analytics objects to be made for testing without sending + actual analytics calls. + + function Analytics.new(props) + Creates a new Analytics object with the given props. + Props: + string Target = The target namespace to send EventStream events to. + For Studio plugins, this is usually "studio". If no Target is + provided, this defaults to "studio". + string Context = The context of the namespace to send EventStream + events to. This will usually match or reference the name of the plugin. + bool LogEvents = Whether to log Analytics events to the console. + + function Analytics.mock() + Creates a mock Analytics object which will do nothing, but provides a + proxy for every function so that it can be safely used for testing. + + function Analytics:sendEvent(string eventName, table additionalArgs) + Sends an EventStream event. The additionalArgs table can be used to pass + more info along with the event itself. + + function Analytics:reportCounter(string counterName, number num = 1) + Reports number num to the RCity bucket named counterName. Used for + iterating ephemeral counters. +]] + +local RbxAnalyticsService = game:GetService("RbxAnalyticsService") +local HttpService = game:GetService("HttpService") +local StudioService = game:GetService("StudioService") + +local Library = script.Parent.Parent +local join = require(Library.join) + +local Analytics = {} +Analytics.__index = Analytics + +function Analytics.new(props) + assert(type(props) == "table", "Analytics props is expected to be a table.") + assert(props.Context, "Analytics expected a context string.") + + local self = { + senders = props.Senders or RbxAnalyticsService, + logEvents = props.LogEvents, + + target = props.Target or "studio", + context = props.Context, + + placeId = game.PlaceId, + userId = StudioService:GetUserId(), + } + setmetatable(self, Analytics) + + self.sessionId = self.senders:GetSessionId() + self.clientId = self.senders:GetClientId() + + return self +end + +-- EventStream events handler +function Analytics:sendEventDeferred(eventName, additionalArgs) + self:logEvent(eventName, additionalArgs) + local args = join(additionalArgs, { + studioSid = self.sessionId, + clientId = self.clientId, + placeId = self.placeId, + userId = self.userId, + }) + self.senders:SendEventDeferred(self.target, self.context, eventName, args) +end + +-- RCity Ephemeral Counters handler +function Analytics:reportCounter(counterName, num) + self:logCounter(counterName, num) + self.senders:ReportCounter(counterName, num or 1) +end + +function Analytics:reportStats(statName, num) + self:logStats(statName, num) + self.senders:ReportStats(statName, num) +end + +function Analytics:logEvent(eventName, tab) + if self.logEvents then + local readableTable = HttpService:JSONEncode(tab) + print(string.format("Analytics: sendEventDeferred: \"%s\", %s", eventName, readableTable)) + end +end + +function Analytics:logCounter(counterName, value) + if self.logEvents then + print(string.format("Analytics: reportCounter: \"%s\", %s", counterName, value)) + end +end + +function Analytics:logStats(statName, value) + if self.logEvents then + print(string.format("Analytics: reportStats: \"%s\", %s", statName, value)) + end +end + +function Analytics.mock(props) + return Analytics.new(join(props, { + Senders = { + SendEventDeferred = function() + end, + ReportCounter = function() + end, + ReportStats = function() + end, + GetSessionId = function() + return 0 + end, + GetClientId = function() + return 0 + end, + } + })) +end + +return Analytics \ No newline at end of file diff --git a/BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Studio/ContextMenus.lua b/BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Studio/ContextMenus.lua new file mode 100644 index 0000000000..a49b560b3c --- /dev/null +++ b/BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Studio/ContextMenus.lua @@ -0,0 +1,42 @@ +--[[ + A Roact wrapper for the PluginMenu API. + + Props: + table Actions = The set of actions to send to MakePluginMenu. + function OnMenuOpened() = A callback for when the context menu has successfully opened. +]] + +local Library = script.Parent.Parent +local Roact = require(Library.Parent.Parent.Roact) + +local PluginContext = require(Library.Plugin) +local getPlugin = PluginContext.getPlugin +local PluginMenus = require(Library.Studio.PluginMenus) + +local ContextMenu = Roact.PureComponent:extend("ContextMenu") + +function ContextMenu:showMenu() + local props = self.props + local actions = props.Actions + local plugin = getPlugin(self) + + props.OnMenuOpened() + PluginMenus.makePluginMenu(plugin, actions) +end + +function ContextMenu:didMount() + self:showMenu() +end + +function ContextMenu:didUpdate() + self:showMenu() +end + +function ContextMenu:render() + return nil +end + +return { + ContextMenu = ContextMenu, + Separator = PluginMenus.Separator, +} \ No newline at end of file diff --git a/BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Studio/Hyperlink.lua b/BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Studio/Hyperlink.lua new file mode 100644 index 0000000000..f7a64aa58c --- /dev/null +++ b/BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Studio/Hyperlink.lua @@ -0,0 +1,60 @@ +--[[ + A widget that contains a hyperlink. + + Props: + bool Enabled = Whether this widget should be interactable. + string Text = The hyperlink text + int TextSize = The size of the text + int LayoutOrder = The order in which this element is displayed if in a UIListLayout. + function OnClick = what happens when the hyperlink is clicked + Mouse = plugin mouse for changing the mouse icon +]] + +local Library = script.Parent.Parent +local Roact = require(Library.Parent.Parent.Roact) + +local Mouse = require(script.Parent.Internal.Mouse) + +local Theming = require(Library.Theming) +local withTheme = Theming.withTheme + +local Hyperlink = Roact.PureComponent:extend("hyperlink") + +local function calculateTextSize(text, textSize, font) + local hugeFrameSizeNoTextWrapping = Vector2.new(5000, 5000) + local size = game:GetService("TextService"):GetTextSize(text, textSize, font, hugeFrameSizeNoTextWrapping) + return UDim2.new(0, size.X, 0, size.Y) +end + +function Hyperlink:render() + return withTheme(function(theme) + if self.props.Enabled == nil then + self.props.Enabled = true + end + + local textSize = self.props.TextSize or 22 + + return Roact.createElement("TextButton", { + BackgroundTransparency = 1, + Text = self.props.Text, + TextSize = textSize, + Font = Enum.Font.SourceSans, + TextColor3 = theme.hyperlink.textColor, + Size = self.props.Size or calculateTextSize(self.props.Text, textSize, Enum.Font.SourceSans), + Position = self.props.Position, + TextXAlignment = Enum.TextXAlignment.Left, + LayoutOrder = self.props.LayoutOrder, + + [Roact.Event.MouseEnter] = function() if self.props.Enabled then Mouse.onEnter(self.props.Mouse) end end, + [Roact.Event.MouseLeave] = function() if self.props.Enabled then Mouse.onLeave(self.props.Mouse) end end, + + [Roact.Event.Activated] = function() + if self.props.Enabled and nil ~= self.props.OnClick then + self.props.OnClick() + end + end, + }) + end) +end + +return Hyperlink \ No newline at end of file diff --git a/BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Studio/Internal/Mouse.lua b/BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Studio/Internal/Mouse.lua new file mode 100644 index 0000000000..8db89bd316 --- /dev/null +++ b/BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Studio/Internal/Mouse.lua @@ -0,0 +1,19 @@ +--[[ + Mouse helper functions +]] + +local Mouse = {} + +function Mouse.onEnter(pluginMouse, iconName) + if pluginMouse then + pluginMouse.Icon = iconName and "rbxasset://SystemCursors/" .. iconName or "rbxasset://SystemCursors/PointingHand" + end +end + +function Mouse.onLeave(pluginMouse) + if pluginMouse then + pluginMouse.Icon = "rbxasset://SystemCursors/Arrow" + end +end + +return Mouse \ No newline at end of file diff --git a/BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Studio/Localization.lua b/BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Studio/Localization.lua new file mode 100644 index 0000000000..bec44c12bb --- /dev/null +++ b/BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Studio/Localization.lua @@ -0,0 +1,284 @@ +--[[ + Reads data out of the localization table, provides a simple interface for fetching strings + + Props : + stringResourceTable : (CSV localization file) the file with the English strings, used for development + translationResourceTable : (CSV localization file) the file with all of the translated strings + pluginName : (string) the "plugin_name" field used in the localization file's keys + + Optional Props : + namespace : (string) the namespace of all keys in the localization, defaults to "Studio" + overrideGetLocale : (function(void)) a function that returns a localeId + overrideLocaleId : (string) a locale used to ignore the current locale set by Roblox + overrideLocaleChangedSignal : (Signal) a signal that the user has changed to a different language + overrideTranslator : (function<>()) + + - NOTE - + To make the localization resource files backend friendly, the keys should be structured like this: + ... + + For formatted strings, follow this guide online : https://developer.roblox.com/articles/localization-format-strings + + For example, your DevelopmentReferenceTable.csv should look something like this : + Key,Context,Example,Source,en + Studio.MoneyManager.Currency.Robux,the name displayed for Robux,,,R$ + Studio.MoneyManager.Currency.USD,the name displayed for US dollars,,,USD + Studio.MoneyManager.Sell.LimitedsTitle,the page title for selling limited items,,,Sell your limiteds + Studio.MoneyManager.Sell.LimitedValue,shows how much an item is worth,,,{1:translate} is worth {2:fixed} {3:translate} + + And your TranslationReferenceTable.csv should look something like this : (line breaks added for readability) + Key,Context,Example,Source,de,es,es-es,ja,ko + Studio.MoneyManager.Sell.LimitedValue,{1:translate} ist fünf {2:fixed} {3:translate}, + {1:translate} vale {2:fixed} {3:translate}, + {1:translate} vale {2:fixed} {3:translate}, + {1:translate}は{2:fixed}{3:translate}の価値がある, + {1:translate}은{2:fixed}{3:translate}가치가있다. + + (it is okay for keys to be missing in this file. This file can be empty and that's fine) + + + Localization Usage : + local rsTable = script.Parent.DevelopmentReferenceTable + local trsTable = script.Parent.TranslationReferenceTable + local pluginLocalization = Localization.new({ + stringResourceTable = rsTable, + translationResourceTable = trsTable, + pluginName = "MoneyManager" + }) + local example = pluginLocalization:getText("Sell", "LimitedsValue", "Valkyrie Helm", 71850, "R$") +]] + +game:DefineFastFlag("FixStudioLocalizationLocaleId", false) + +-- services +local LocalizationService = game:GetService("LocalizationService") +local StudioService = game:GetService("StudioService") + +-- libraries +local Library = script.Parent.Parent +local Signal = require(Library.Utils.Signal) + +-- constants +local FALLBACK_LOCALE = "en-us" + + +local Localization = {} +Localization.__index = Localization + +function Localization.new(props) + assert(type(props) == "table", "Localization props is expected to be a table.") + assert(props.stringResourceTable ~= nil, "Localization must have a .csv string resource table for English strings") + assert(props.translationResourceTable ~= nil, "Localization must have a .csv string resource table of translations") + assert(type(props.pluginName) == "string", "Please specify the plugin's name") + + local stringResourceTable = props.stringResourceTable + local translationResourceTable = props.translationResourceTable + local overrideGetLocale = props.getLocale + local overrideLocaleId = props.overrideLocaleId + local overrideLocaleChangedSignal = props.overrideLocaleChangedSignal + local keyNamespace = props.namespace + local keyPluginName = props.pluginName + + if keyNamespace == nil then + keyNamespace = "Studio" + end + + local externalLocaleChanged + if overrideLocaleChangedSignal then + externalLocaleChanged = overrideLocaleChangedSignal + elseif game:GetFastFlag("FixStudioLocalizationLocaleId") then + externalLocaleChanged = StudioService:GetPropertyChangedSignal("StudioLocaleId") + else + externalLocaleChanged = LocalizationService:GetPropertyChangedSignal("RobloxLocaleId") + end + + -- a function that gets called when the locale changes, returns the new locale + local function getLocale() + if overrideGetLocale then + return overrideGetLocale() + end + + if overrideLocaleId ~= nil then + return overrideLocaleId + elseif game:GetFastFlag("FixStudioLocalizationLocaleId") then + return StudioService["StudioLocaleId"] + else + return LocalizationService["RobloxLocaleId"] + end + end + + local self = { + -- localeChanged : (Signal) + -- a public facing signal for Localization consumers to observe updates + localeChanged = Signal.new(), + + -- externalLocaleChanged : (Signal) + -- the system signal fired when a user changes their language settings + externalLocaleChanged = externalLocaleChanged, + + -- externalLocaleChangedConnection : (connection token) + -- a subscription token for cleaning up the connection + externalLocaleChangedConnection = nil, + + -- locale : (string) + -- an id for knowing which translation to read from. ex) "en-us" + locale = FALLBACK_LOCALE, + + -- keyNamespace : (string) + -- the first field used to construct a key + keyNamespace = keyNamespace, + + -- keyPluginName : (string) + -- the second field used to construct a key + keyPluginName = keyPluginName, + + -- getLocale : (function()) + -- gets the current locale string + getLocale = getLocale, + + -- stringResourceTable : a CSV file containing all of the English strings + -- this is converted into a proper resource by Rojo + stringResourceTable = stringResourceTable, + + -- translationResourceTable : a CSV file containing all of the translated strings + -- this is converted into a proper resource by Rojo + translationResourceTable = translationResourceTable, + + -- translator & fallbackTranslator : (Translator) + -- objects that handle the string formatting from the current stringResourceTable + translator = nil, + fallbackTranslator = nil + } + setmetatable(self, Localization) + + -- listen to changes to the locale to alert all listeners of the change + self.localeChangedConnection = self.externalLocaleChanged:connect(function() + self:updateLocaleAndTranslator() + self.localeChanged:fire(self.locale) + end) + + -- create the translators for the first time + self:updateLocaleAndTranslator() + + return self +end + +-- scope : (string) the general group of data that the key belongs to +-- key : (string) the id of the string in the resource table +-- ... : (optional, Variant) values used to format a string +function Localization:getText(scope, key, ...) + assert(type(scope) == "string", "Cannot fetch the string without a scope") + assert(type(key) == "string", "Cannot fetch a string without the key") + + local stringKey = string.format("%s.%s.%s.%s", self.keyNamespace, self.keyPluginName, scope, key) + local args = {...} + + local function getTranslation(translator) + if not translator then + return false, nil + end + + local success, result = pcall(function() + return translator:FormatByKey(stringKey, args) + end) + return success, result + end + + -- optimize for one lookup when the locale is English + local success + local translated + if self.locale == FALLBACK_LOCALE then + -- English strings are only written into the development string table, + -- so don't bother looking up the key in the localization table. + success, translated = getTranslation(self.fallbackTranslator) + if success then + return translated + end + + else + -- try to find a translation in our translation file + success, translated = getTranslation(self.translator) + if success then + return translated + end + + -- If no translation exists for this locale id, fall back to default (English) + success, translated = getTranslation(self.fallbackTranslator) + if success then + return translated + end + end + + -- Fall back to the given key if there is no translation for this value + -- Useful for finding misspelled or missing keys + return stringKey +end + +function Localization:destroy() + if self.localeChangedConnection then + self.localeChangedConnection:disconnect() + end +end + +function Localization:updateLocaleAndTranslator() + -- the locale has changed, update the translators + self.locale = self.getLocale() + self.translator = self.translationResourceTable:GetTranslator(self.locale) + self.fallbackTranslator = self.stringResourceTable:GetTranslator(FALLBACK_LOCALE) +end + +-- changeSignal : (Signal, optional) a signal to trigger localization changes +function Localization.mock(localizationChangedSignal) + local changeSignal + if localizationChangedSignal then + changeSignal = localizationChangedSignal + else + changeSignal = Signal.new() + end + + -- any time the localizationChangedSignal fires, this will get the next one + -- this should trigger re-renders for any elements + local currentLocale = 0 + local localeIDs = {"en-us", "es", "es-es", "ko", "ja"} + local function getLocale() + currentLocale = (currentLocale + 1) % 5 + local nextLocale = localeIDs[currentLocale] + return nextLocale + end + + local fakeResourceTable = { + GetTranslator = function(stringResourceTableSelf, localeId) + local translator = { + FormatByKey = function(translatorSelf, key, args) + if not args then + args = {} + elseif type(args) ~= "table" then + error("Args must be a table") + end + + -- return a string like en-us|TEST.MOCK_LOCALIZATION.A.hello_world:[a,b,c,10] + return string.format("%s|%s:[%s]", localeId,key, table.concat(args, ",")) + end, + } + + return translator + end + } + + -- create a fake localization object for tests + return Localization.new({ + -- create a fake resource file that mimics the real thing + stringResourceTable = fakeResourceTable, + translationResourceTable = fakeResourceTable, + + namespace = "TEST", + pluginName = "MOCK_LOCALIZATION", + + -- for tests, don't connect to any system signals to ensure stuff doesn't change mid test + overrideLocaleChangedSignal = changeSignal, + getLocale = getLocale, + }) +end + + +return Localization \ No newline at end of file diff --git a/BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Studio/Localization.spec.lua b/BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Studio/Localization.spec.lua new file mode 100644 index 0000000000..ede3c1b091 --- /dev/null +++ b/BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Studio/Localization.spec.lua @@ -0,0 +1,228 @@ +local Localization = require(script.Parent.Localization) + +local Library = script.Parent.Parent +local Signal = require(Library.Utils.Signal) + +local TestLocalizationChangedSignal = Signal.new() +local TestDevStrings = Library.Studio.TestDevStrings +local TestTranslationStrings = Library.Studio.TestTranslationStrings + + + +return function() + -- since Localization connects to system signals, it's important to clean up after the test + describe("Localization", function() + it("should construct with the correct inputs", function() + local localization = Localization.new({ + stringResourceTable = TestDevStrings, + translationResourceTable = TestTranslationStrings, + pluginName = "UILibrary", + }) + expect(localization).to.be.ok() + + localization:destroy() + end) + + it("should error if it is missing any props", function() + expect(function() + Localization.new() + end).to.throw() + + expect(function() + Localization.new({}) + end).to.throw() + + expect(function() + Localization.new({ stringResourceTable = TestDevStrings }) + end).to.throw() + + expect(function() + Localization.new({ translationResourceTable = TestTranslationStrings }) + end).to.throw() + + expect(function() + Localization.new({ pluginName = "UILibrary" }) + end).to.throw() + end) + + it("should return localized strings when given keys to look up", function() + local localization = Localization.new({ + stringResourceTable = TestDevStrings, + translationResourceTable = TestTranslationStrings, + pluginName = "UILibrary", + overrideLocaleId = "en-us", + overrideLocaleChangedSignal = TestLocalizationChangedSignal + }) + + local greeting = localization:getText("Spec", "greeting_formal") + expect(greeting).to.equal("Hello") + + localization:destroy() + end) + + it("should return a formatted string when args are provided", function() + local localization = Localization.new({ + stringResourceTable = TestDevStrings, + translationResourceTable = TestTranslationStrings, + pluginName = "UILibrary", + overrideLocaleId = "en-us", + overrideLocaleChangedSignal = TestLocalizationChangedSignal + }) + + local greeting = localization:getText("Spec", "greeting_informal", "Builderman", 2) + expect(greeting).to.equal("Sup Builderman, I haven't seen you in 2 days") + + localization:destroy() + end) + + it("should return the English text of a string if a translation is missing in the resourceTable", function() + local localization = Localization.new({ + stringResourceTable = TestDevStrings, + translationResourceTable = TestTranslationStrings, + pluginName = "UILibrary", + overrideLocaleId = "es-es", + overrideLocaleChangedSignal = TestLocalizationChangedSignal + }) + + local normal = localization:getText("Spec", "greeting_formal") + expect(normal).to.equal("Hola") + + local informal = localization:getText("Spec", "greeting_informal", "John Doe", 100) + expect(informal).to.equal("¿Qué pasa John Doe? No te he visto en 100 días") + + local surprise = localization:getText("Spec", "greeting_surprise") + expect(surprise).to.equal("No one expects the Spanish Inquisition!") + + localization:destroy() + end) + + it("should return the key if the string does not exist in the resourceTable at all", function() + local localization = Localization.new({ + stringResourceTable = TestDevStrings, + translationResourceTable = TestTranslationStrings, + pluginName = "UILibrary", + overrideLocaleId = "en-us", + overrideLocaleChangedSignal = TestLocalizationChangedSignal + }) + + local greeting = localization:getText("Spec", "greeting_serious") + expect(greeting).to.equal("Studio.UILibrary.Spec.greeting_serious") + + localization:destroy() + end) + + it("should update its strings if the localization changes", function() + local changeSignal = Signal.new() + local currentLocale = "en-us" + local function getLocale() + return currentLocale + end + + local localization = Localization.new({ + stringResourceTable = TestDevStrings, + translationResourceTable = TestTranslationStrings, + pluginName = "UILibrary", + getLocale = getLocale, + overrideLocaleChangedSignal = changeSignal + }) + + local greeting = localization:getText("Spec", "greeting_formal") + expect(greeting).to.equal("Hello") + + -- trigger a locale change + currentLocale = "es-es" + changeSignal:fire() + + greeting = localization:getText("Spec", "greeting_formal") + expect(greeting).to.equal("Hola") + + + localization:destroy() + end) + + it("should remove the observer to the localization changed signal when it is destroyed", function() + local changeSignal = Signal.new() + local currentLocale = "en-us" + local callCount = 0 + local function getLocale() + callCount = callCount + 1 + return currentLocale + end + + local localization = Localization.new({ + stringResourceTable = TestDevStrings, + translationResourceTable = TestTranslationStrings, + pluginName = "UILibrary", + getLocale = getLocale, + overrideLocaleChangedSignal = changeSignal + }) + + expect(callCount).to.equal(1) + + local greeting = localization:getText("Spec", "greeting_formal") + expect(greeting).to.equal("Hello") + + -- trigger a locale change + currentLocale = "es-es" + changeSignal:fire() + + greeting = localization:getText("Spec", "greeting_formal") + expect(greeting).to.equal("Hola") + expect(callCount).to.equal(2) + + -- destroy the connection and trigger another change + localization:destroy() + currentLocale = "en-us" + changeSignal:fire() + + greeting = localization:getText("Spec", "greeting_formal") + expect(greeting).to.equal("Hola") + expect(callCount).to.equal(2) + end) + + it("should fallback to the base language if it is available when a specific locale isn't supported", function() + local localization = Localization.new({ + stringResourceTable = TestDevStrings, + translationResourceTable = TestTranslationStrings, + pluginName = "UILibrary", + overrideLocaleId = "es-mx", + }) + + local greeting = localization:getText("Spec", "greeting_formal") + expect(greeting).to.equal("Hola") + end) + end) + + + describe("Localization.mock()", function() + it("should always return a string, even without the actual resourceTable", function() + local mock = Localization.mock() + + -- expect it to return someting, if not the real string + -- something like : |...:[list of args] + local strA = mock:getText("Anything", "greeting_formal") + expect(strA).to.equal("en-us|TEST.MOCK_LOCALIZATION.Anything.greeting_formal:[]") + + local strB = mock:getText("Anything", "greeting_informal", "Jane Doe", 1) + expect(strB).to.equal("en-us|TEST.MOCK_LOCALIZATION.Anything.greeting_informal:[Jane Doe,1]") + + mock:destroy() + end) + + it("should allow for an external signal to fake locale changes", function() + local testSignal = Signal.new() + local mockLocalization = Localization.mock(testSignal) + + local callCount = 0 + local mockToken = mockLocalization.localeChanged:connect(function() + callCount = callCount + 1 + end) + + testSignal:fire() + expect(callCount).to.equal(1) + + mockToken:disconnect() + mockLocalization:destroy() + end) + end) +end \ No newline at end of file diff --git a/BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Studio/PartialHyperLink.lua b/BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Studio/PartialHyperLink.lua new file mode 100644 index 0000000000..ef8cf9b89b --- /dev/null +++ b/BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Studio/PartialHyperLink.lua @@ -0,0 +1,39 @@ +local Library = script.Parent.Parent +local Roact = require(Library.Parent.Parent.Roact) + +local StudioWidgetHyperlink = require(script.Parent.Hyperlink) + +local PartialHyperlink = Roact.PureComponent:extend("PartialHyperlink") + +local function calculateTextSize(text, textSize, font) + local hugeFrameSizeNoTextWrapping = Vector2.new(5000, 5000) + return game:GetService('TextService'):GetTextSize(text, textSize, font, hugeFrameSizeNoTextWrapping) +end + + +function PartialHyperlink:render() + local hyperLinkTextSize = calculateTextSize(self.props.HyperLinkText, self.props.Theme.fontStyle.Normal.TextSize, self.props.Theme.fontStyle.Normal.Font) + return Roact.createElement("Frame", { + Size = UDim2.new(1, 0, 0, hyperLinkTextSize.Y), + BackgroundTransparency = 1, + }, { + HyperLink = Roact.createElement(StudioWidgetHyperlink, { + Text = self.props.HyperLinkText, + Size = UDim2.new(0, hyperLinkTextSize.X, 0, hyperLinkTextSize.Y), + Mouse = self.props.Mouse, + OnClick = self.props.OnClick, + }), + TextLabel = Roact.createElement("TextLabel", { + BackgroundTransparency = 1, + Position = UDim2.new(0, hyperLinkTextSize.X, 0, 0), + Size = UDim2.new(1, -hyperLinkTextSize.X, 1, 0), + TextColor3 = self.props.Theme.fontStyle.Normal.TextColor3, + Font = Enum.Font.SourceSans, + TextSize = 22, + TextXAlignment = Enum.TextXAlignment.Left, + Text = self.props.NonHyperLinkText, + }), + }) +end + +return PartialHyperlink \ No newline at end of file diff --git a/BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Studio/PluginMenus.lua b/BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Studio/PluginMenus.lua new file mode 100644 index 0000000000..ee9caf58b9 --- /dev/null +++ b/BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Studio/PluginMenus.lua @@ -0,0 +1,131 @@ +--[[ + Returns a Separator constant and makePluginMenu. makePluginMenu creates a PluginMenu made up of + PluginActions, or SubMenus of PluginActions. + + Parameters: + plugin = The Roblox plugin instance that this menu belongs to + entries = a list of actions to be displayed in the menu. Actions can be a PluginAction instance + created externally, the SEPARATOR constant, or a dictionary containing action information that + will be used to create a new action. Action dictionaries have the following fields: + + Text = the action text displayed in the menu + ItemSelected = a callback that is invoked whenever the action is clicked + [Key] = an optional value provided as an argument to the ItemSelected callback. Can be nil if you + aren't sharing ItemSelected callbacks between actions + [Icon] = an optional icon displayed next to the action in the menu + [Checked] optionally show a check mark next to the action in the menu. Ignored if the action is in + a selection submenu (see below) + [Enabled] optionally control whether an action is selectable or not. Defaults to true + + If you insert one or more actions into the dictionary, the dictionary it was inserted into will + become a submenu. Submenus have the same properties as actions since the submenu is an action, + but ItemSelected will never be invoked. + + There is also a special kind of selection submenu that will automatically display a checkmark next + to a selected action. If you include a CurrentKey field in the submenu, any action whose Key field + equals CurrentKey will have a checkmark displayed. It is the responsibility of the consumer to use + the actions' ItemSelected callback to update the CurrentKey field of the submenu. + + Examples: + + Explorer context menu: + { + -- Ordinary actions + { Text = "Cut", Icon = "rbxasset://textures/cutIcon.png", ItemSelected = function() cutSelection() end }, + { Text = "Copy", Icon = "rbxasset://textures/copyIcon.png", ItemSelected = function() copySelection() end }, + { Text = "Paste Into", Enabled = false, ItemSelected = function() pasteIntoSelection() end }, + ... + PluginMenus.Separator, + ... + -- A submenu action with two inner items (the inner items can also be submenus) + { + Text = "Insert Object", Icon = "insertObjects.png", Enabled = true, + { Text = "Part", Key = partKey, Icon = "part.png", ItemSelected = function(key) insertObject(key) end }, + { Text = "Wedge", Key = wedgeKey, Icon = "wedge.png", ItemSelected = function(key) insertObject(key) end }, + ... + }, + } + + Tools Context Menu + { + { Text = "Collisions Enabled", Checked = true, ItemSelected = function(text) toggleCollisions() end }, + { Text = "Constraints Enabled", Checked = false, ItemSelected = function(text) toggleConstraints() end }, + { + CurrentKey = alwaysKey, Text = "Join Mode", Icon = "rbxasset://textures/joinMode.png", + { Text = "Always", Key = alwaysKey, ItemSelected = function(key) joinModeSelected(key) end }, + { Text = "None", Key = noneKey, ItemSelected = function(key) joinModeSelected(key) end }, + } + } +]] + +-- Importing HttpService only for GenerateGUID +local HttpService = game:GetService("HttpService") + +local Library = script.Parent.Parent +local Symbol = require(Library.Utils.Symbol) + +local SEPARATOR = Symbol.named("(PluginMenuSeparator)") + +local function newId() + return HttpService:GenerateGUID() +end + +local function connectAction(connections, action, entry, item) + table.insert(connections, action.Triggered:Connect(function() + for _, connection in ipairs(connections) do + connection:Disconnect() + end + entry.ItemSelected(item) + end)) +end + +local function createPluginMenu(plugin, entries, subMenus, connections) + local menu = plugin:CreatePluginMenu(newId(), entries.Text, entries.Icon) + + for _, entry in ipairs(entries) do + if entry == SEPARATOR then + menu:AddSeparator() + elseif typeof(entry) == "Instance" and entry:IsA("PluginAction") then + menu:AddAction(entry) + elseif typeof(entry) == "table" then + if #entry > 0 then + local subMenu = createPluginMenu(plugin, entry, subMenus, connections) + table.insert(subMenus, subMenu) + menu:AddMenu(subMenu) + else + local action = menu:AddNewAction(newId(), entry.Text, entry.Icon) + action.Enabled = (entry.Enabled == nil) and true or entry.Enabled + + if entries.CurrentKey then + action.Checked = entries.CurrentKey == entry.Key + else + action.Checked = entry.Checked + end + + connectAction(connections, action, entry, entry.Key) + end + elseif entry then -- Ignore false/nil for when plugins do {xyz, fflag and abc, ...} + error("Unsupported action "..tostring(entry)) + end + end + + return menu +end + +local function makePluginMenu(plugin, entries) + local subMenus = {} + local connections = {} + + local menu = createPluginMenu(plugin, entries, subMenus, connections) + + menu:ShowAsync() + for _, subMenu in ipairs(subMenus) do + subMenu:Destroy() + end + menu:Destroy() +end + +return { + makePluginMenu = makePluginMenu, + Separator = SEPARATOR, +} \ No newline at end of file diff --git a/BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Studio/StudioStyle.lua b/BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Studio/StudioStyle.lua new file mode 100644 index 0000000000..b7af169df8 --- /dev/null +++ b/BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Studio/StudioStyle.lua @@ -0,0 +1,54 @@ +--[[ + Matches the table structure of UILibrary's Style table. Provides a default mapping for colors + +]] +local StudioStyle = {} +StudioStyle.__index = StudioStyle + +-- c : StudioStyleGuideColor +-- m : StudioStyleGuideModifier +function StudioStyle.new(getColor, c, m) + local FStringMainFont = game:GetFastString("StudioBuiltinPluginDefaultFont") + + return { + font = Enum.Font[FStringMainFont], + + backgroundColor = getColor(c.MainBackground), + liveBackgroundColor = Color3.new(), + textColor = getColor(c.MainText), + subTextColor = getColor(c.SubText), + dimmerTextColor = getColor(c.DimmedText), + + itemColor = getColor(c.Item), + borderColor = getColor(c.Border), + + hoveredItemColor = getColor(c.Item, m.Hover), + hoveredTextColor = getColor(c.MainText, m.Hover), + + primaryItemColor = getColor(c.DialogMainButton), + primaryBorderColor = getColor(c.DialogMainButton), + primaryTextColor = getColor(c.DialogMainButtonText), + + primaryHoveredItemColor = getColor(c.DialogMainButton, m.Hover), + primaryHoveredBorderColor = getColor(c.DialogMainButton, m.Hover), + primaryHoveredTextColor = getColor(c.DialogMainButtonText, m.Hover), + + selectionColor = getColor(c.Item, m.Selected), + selectionBorderColor = getColor(c.Border, m.Selected), + selectedTextColor = getColor(c.MainText, m.Selected), + + shadowColor = getColor(c.Shadow), + shadowTransparency = getColor(c.Shadow, m.Hover), + + separationLineColor = getColor(c.Separator), + + disabledColor = getColor(c.MainText, m.Disabled), + errorColor = getColor(c.ErrorText), + + hoverColor = getColor(c.MainBackground, m.Hover), + + hyperlinkTextColor = getColor(c.LinkText), + } +end + +return StudioStyle \ No newline at end of file diff --git a/BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Studio/StudioStyle.spec.lua b/BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Studio/StudioStyle.spec.lua new file mode 100644 index 0000000000..c79266d200 --- /dev/null +++ b/BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Studio/StudioStyle.spec.lua @@ -0,0 +1,35 @@ +local StudioStyle = require(script.Parent.StudioStyle) + +local Library = script.Parent.Parent +local Style = require(Library.StyleDefaults) +local StudioTheme = require(Library.Studio.StudioTheme) + +return function() + it("should define all of the keys in the Style.Defaults object", function() + local numKeysFound = 0 + local numKeysExpected = 0 + + local defaultStyle = Style.Defaults + local studioTheme = StudioTheme.newDummyTheme(function() return {} end) + local mockStudioTheme = studioTheme.getTheme() + local studioStyle = StudioStyle.new(mockStudioTheme.GetColor, + Enum.StudioStyleGuideColor, + Enum.StudioStyleGuideModifier) + + -- every key in the default style should be accounted for + for colorKey, _ in pairs(defaultStyle) do + numKeysExpected = numKeysExpected + 1 + expect(studioStyle[colorKey]).to.be.ok() + end + + -- there should not be extra keys defined + for _, _ in pairs(studioStyle) do + numKeysFound = numKeysFound + 1 + end + + expect(numKeysFound).to.equal(numKeysExpected) + + -- clean up + studioTheme:destroy() + end) +end \ No newline at end of file diff --git a/BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Studio/StudioTheme.lua b/BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Studio/StudioTheme.lua new file mode 100644 index 0000000000..159fc6ce35 --- /dev/null +++ b/BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Studio/StudioTheme.lua @@ -0,0 +1,133 @@ +--[[ + Wraps theme colors and update logic for Roblox Studio. + Plugins are responsible for making a wrapper around this StudioTheme that + defines a createValues function. This function maps Studio theme enums + to named table entries which can be used in the plugin's theme. + + Example usage (a Theme.lua module in your plugin): + + local StudioTheme = require(Plugin.UILibrary.Studio.StudioTheme) + local Theme = {} + + function Theme.createValues(getColor, c, m) + return { + backgroundColor = getColor(c.MainBackground), + } + end + + function Theme.new() + return StudioTheme.new(Theme.createValues) + end + + return Theme +]] + +game:DefineFastFlag("FixMockStudioTheme", false) + +local Library = script.Parent.Parent +local join = require(Library.join) +local Signal = require(Library.Utils.Signal) + +local StudioTheme = {} +StudioTheme.__index = StudioTheme + +function StudioTheme.new(createValues, overrideSignal) + local self = { + getTheme = function() + return settings().Studio.Theme + end, + + createValues = function(...) + return createValues(...) + end, + + valuesChanged = Signal.new(), + values = {}, + themeChangedConnection = nil, + } + + setmetatable(self, StudioTheme) + + if overrideSignal then + self.themeChangedConnection = overrideSignal:Connect(function() + self:recalculateTheme() + end) + else + self.themeChangedConnection = settings().Studio.ThemeChanged:Connect(function() + self:recalculateTheme() + end) + end + + self:recalculateTheme() + + return self +end + +function StudioTheme:connect(...) + return self.valuesChanged:connect(...) +end + +function StudioTheme:destroy() + if self.themeChangedConnection then + self.themeChangedConnection:Disconnect() + end +end + +function StudioTheme:update(changedValues) + self.values = join(self.values, changedValues) + + if self.valuesChanged then + self.valuesChanged:fire(self.values) + end +end + +function StudioTheme:recalculateTheme() + local theme = self.getTheme() + + -- Shorthands for getting a color + local c = Enum.StudioStyleGuideColor + local m = Enum.StudioStyleGuideModifier + + local function getColor(...) + return theme:GetColor(...) + end + + local newValues = self.createValues(getColor, c, m) + + self:update(newValues) +end + +function StudioTheme.newDummyTheme(createValues) + local self = { + getTheme = function() + return { + GetColor = function() + return Color3.new() + end, + } + end, + + createValues = function(...) + return createValues(...) + end, + + valuesChanged = Signal.new(), + values = {}, + } + + setmetatable(self, StudioTheme) + + if game:GetFastFlag("FixMockStudioTheme") then + local newValues = self.createValues(function() + return self.getTheme():GetColor() + end, {}, {}) + + self:update(newValues) + else + self:recalculateTheme() + end + + return self +end + +return StudioTheme diff --git a/BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Studio/StudioTheme.spec.lua b/BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Studio/StudioTheme.spec.lua new file mode 100644 index 0000000000..100b16a65f --- /dev/null +++ b/BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Studio/StudioTheme.spec.lua @@ -0,0 +1,102 @@ +return function() + local Library = script.Parent.Parent + local StudioTheme = require(Library.Studio.StudioTheme) + + local function createValues() + return {} + end + + describe("StudioTheme.new", function() + it("should return a new StudioTheme", function() + local theme = StudioTheme.new(createValues) + expect(theme).to.be.ok() + expect(theme.values).to.be.ok() + + theme:destroy() + end) + + it("should have a getTheme function that gets the Studio theme", function() + local theme = StudioTheme.new(createValues) + expect(theme.getTheme).to.be.ok() + expect(theme.getTheme()).to.equal(settings().Studio.Theme) + + theme:destroy() + end) + + it("should listen for Studio theme changes", function() + local event = Instance.new("BindableEvent") + local theme = StudioTheme.new(createValues, event.Event) + expect(theme.themeChangedConnection).to.be.ok() + + local called = false + theme:connect(function() + called = true + end) + event:Fire() + expect(called).to.equal(true) + + event:Destroy() + theme:destroy() + end) + end) + + describe("StudioTheme.newDummyTheme", function() + it("should return a new fake StudioTheme", function() + local theme = StudioTheme.newDummyTheme(createValues) + expect(theme).to.be.ok() + expect(theme.values).to.be.ok() + + theme:destroy() + end) + + it("should have a getTheme function that returns a constant color", function() + local theme = StudioTheme.newDummyTheme(createValues) + expect(theme.getTheme).to.be.ok() + expect(theme.getTheme()).never.to.equal(settings().Studio.Theme) + expect(theme.getTheme().GetColor).to.be.ok() + expect(theme.getTheme().GetColor()).to.equal(Color3.new()) + + theme:destroy() + end) + + it("should not listen for theme changes", function() + local theme = StudioTheme.newDummyTheme(createValues) + expect(theme.themeChangedConnection).never.to.be.ok() + + theme:destroy() + end) + end) + + describe("StudioTheme.recalculateTheme", function() + it("should call the createValues function", function() + local called = false + + local theme = StudioTheme.new(function() + called = true + return {} + end) + + expect(called).to.equal(true) + called = false + theme:recalculateTheme() + expect(called).to.equal(true) + + theme:destroy() + end) + + it("should update the theme values", function() + local called = false + + local theme = StudioTheme.new(function() + return called and {newColor = Color3.new()} or {} + end) + + expect(theme.values.newColor).never.to.be.ok() + called = true + theme:recalculateTheme() + expect(theme.values.newColor).to.be.ok() + + theme:destroy() + end) + end) +end \ No newline at end of file diff --git a/BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/StyleDefaults.lua b/BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/StyleDefaults.lua new file mode 100644 index 0000000000..537f6b3c69 --- /dev/null +++ b/BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/StyleDefaults.lua @@ -0,0 +1,71 @@ +--[[ + Provides style utility for the UILibrary. + Default values for style can be defined here. +]] + +local Style = {} + +--Defines default entries for the Style table in createTheme. +--Default entries are only to provide compatibility, not actual theme info. +Style.Defaults = { + font = Enum.Font.SourceSans, + + backgroundColor = Color3.new(), + liveBackgroundColor = Color3.new(), + textColor = Color3.new(), + subTextColor = Color3.new(), + dimmerTextColor = Color3.new(), + + itemColor = Color3.new(), + borderColor = Color3.new(), + + hoveredItemColor = Color3.new(), + hoveredTextColor = Color3.new(), + + primaryItemColor = Color3.new(), + primaryBorderColor = Color3.new(), + primaryTextColor = Color3.new(), + + primaryHoveredItemColor = Color3.new(), + primaryHoveredBorderColor = Color3.new(), + primaryHoveredTextColor = Color3.new(), + + selectionColor = Color3.new(), + selectionBorderColor = Color3.new(), + selectedTextColor = Color3.new(), + + shadowColor = Color3.new(), + shadowTransparency = Color3.new(), + + separationLineColor = Color3.new(), + + disabledColor = Color3.new(), + errorColor = Color3.new(), + + hoverColor = Color3.new(), + + hyperlinkTextColor = Color3.new(), +} + +-- A function that checks to see if there are any missing or +-- extraneous keys in the given style. If a value is available +-- for all necessary entries, then the UILibrary will be able to run. +Style.isValid = function(style) + local requiredStyle = Style.Defaults + + for key, _ in pairs(requiredStyle) do + if not style[key] then + return false + end + end + + for key, _ in pairs(style) do + if not requiredStyle[key] then + return false + end + end + + return true +end + +return Style \ No newline at end of file diff --git a/BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Theming.lua b/BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Theming.lua new file mode 100644 index 0000000000..4bda76d40d --- /dev/null +++ b/BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Theming.lua @@ -0,0 +1,63 @@ +local Library = script.Parent + +local Roact = require(Library.Parent.Parent.Roact) +local Signal = require(Library.Utils.Signal) +local Symbol = require(Library.Utils.Symbol) +local themeKey = Symbol.named("UILibraryTheme") + +local ThemeProvider = Roact.PureComponent:extend("UILibraryThemeProvider") +function ThemeProvider:init() + local theme = self.props.theme + assert(theme ~= nil, "No theme was given to this ThemeProvider.") + self.themeChanged = Signal.new() + + self._context[themeKey] = { + values = theme, + themeChanged = self.themeChanged, + } +end +function ThemeProvider:render() + self._context[themeKey].values = self.props.theme + self.themeChanged:fire() + return Roact.oneChild(self.props[Roact.Children]) +end + +-- the consumer should complain if it doesn't have a theme +local ThemeConsumer = Roact.PureComponent:extend("UILibraryThemeConsumer") +function ThemeConsumer:init() + assert(self._context[themeKey] ~= nil, "No ThemeProvider found.") + local theme = self._context[themeKey] + + self.state = { + themeValues = theme.values, + } + + self.themeConnection = theme.themeChanged:connect(function() + self:setState({ + themeValues = theme.values, + }) + end) +end +function ThemeConsumer:render() + local themeValues = self.state.themeValues + return self.props.themedRender(themeValues) +end +function ThemeConsumer:willUnmount() + if self.themeConnection then + self.themeConnection:disconnect() + end +end + +-- withTheme should provide a simple way to style elements +-- callback : function(theme) +local function withTheme(callback) + return Roact.createElement(ThemeConsumer, { + themedRender = callback + }) +end + +return { + Provider = ThemeProvider, + Consumer = ThemeConsumer, + withTheme = withTheme, +} \ No newline at end of file diff --git a/BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/UILibraryWrapper.lua b/BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/UILibraryWrapper.lua new file mode 100644 index 0000000000..51b7868561 --- /dev/null +++ b/BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/UILibraryWrapper.lua @@ -0,0 +1,69 @@ +--[[ + A component that wraps all external elements needed for the UILibrary. + Entries in the wrapper are optional, but if you do not provide an + element that is needed by the components you are using, you will get + an error upon trying to mount those components. + + Props: + Theme theme = A theme object to be used by a ThemeProvider. + PluginGui focusGui = The top-level gui to be used by a FocusProvider. + Plugin plugin = A Plugin object which can be used to construct guis. +]] + +local Library = script.Parent +local Roact = require(Library.Parent.Parent.Roact) + +local Theming = require(Library.Theming) +local ThemeProvider = Theming.Provider + +local Focus = require(Library.Focus) +local FocusProvider = Focus.Provider + +local Plugin = require(Library.Plugin) +local PluginProvider = Plugin.Provider + +local Camera = require(Library.Camera) +local CameraProvider = Camera.Provider + +local UILibraryWrapper = Roact.PureComponent:extend("UILibraryWrapper") + +function UILibraryWrapper:addProvider(root, provider, props) + return Roact.createElement(provider, props, {root}) +end + +function UILibraryWrapper:render() + local props = self.props + local children = props[Roact.Children] + local root = Roact.oneChild(children) + + -- ThemeProvider + local theme = props.theme + if theme then + root = self:addProvider(root, ThemeProvider, { + theme = theme, + }) + end + + -- FocusProvider + local focusGui = props.focusGui + if focusGui then + root = self:addProvider(root, FocusProvider, { + pluginGui = focusGui, + }) + end + + -- PluginProvider + local plugin = props.plugin + if plugin then + root = self:addProvider(root, PluginProvider, { + plugin = plugin, + }) + end + + -- CameraProvider + root = self:addProvider(root, CameraProvider, nil) + + return root +end + +return UILibraryWrapper \ No newline at end of file diff --git a/BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/UILibraryWrapper.spec.lua b/BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/UILibraryWrapper.spec.lua new file mode 100644 index 0000000000..089f6bf3ee --- /dev/null +++ b/BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/UILibraryWrapper.spec.lua @@ -0,0 +1,86 @@ +return function() + local Library = script.Parent + local Roact = require(Library.Parent.Parent.Roact) + + local workspace = game:GetService("Workspace") + + local UILibraryWrapper = require(script.Parent.UILibraryWrapper) + + local function createTestWrapper(props, children) + return Roact.createElement(UILibraryWrapper, props, children) + end + + it("should create and destroy without errors", function() + local element = createTestWrapper() + local instance = Roact.mount(element) + Roact.unmount(instance) + end) + + it("should render its children if nothing is provided", function () + local container = Instance.new("Folder") + local instance = Roact.mount(createTestWrapper({}, { + Frame = Roact.createElement("Frame") + }), container) + + local frame = container:FindFirstChildOfClass("Frame") + + expect(frame).to.be.ok() + + Roact.unmount(instance) + end) + + it("should render its children if items are provided", function () + local container = Instance.new("Folder") + local instance = Roact.mount(createTestWrapper({ + theme = {}, + focusGui = {}, + plugin = {}, + }, { + Frame = Roact.createElement("Frame") + }), container) + + local frame = container:FindFirstChildOfClass("Frame") + + expect(frame).to.be.ok() + + Roact.unmount(instance) + end) + + describe("addProvider", function() + it("should place the new provider above the root", function () + local container = Instance.new("Folder") + local root = Roact.createElement("Frame") + + local result = UILibraryWrapper:addProvider(root, "Frame") + local instance = Roact.mount(result, container) + local frame = container:FindFirstChildOfClass("Frame") + + expect(frame).to.be.ok() + expect(frame[1]).to.be.ok() + + Roact.unmount(instance) + end) + + it("should not modify the tree below the root", function () + local container = Instance.new("Folder") + local root = Roact.createElement("Frame", {}, { + ChildFrame = Roact.createElement("Frame", {}, { + DescendantFrame = Roact.createElement("Frame"), + }), + OtherChild = Roact.createElement("Frame"), + }) + + local result = UILibraryWrapper:addProvider(root, "Frame") + local instance = Roact.mount(result, container) + local frame = container:FindFirstChildOfClass("Frame") + + expect(frame).to.be.ok() + expect(frame[1]).to.be.ok() + expect(frame[1].ChildFrame).to.be.ok() + expect(frame[1].ChildFrame.DescendantFrame).to.be.ok() + expect(frame[1].OtherChild).to.be.ok() + + Roact.unmount(instance) + end) + end) +end \ No newline at end of file diff --git a/BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Utils/AssetType.lua b/BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Utils/AssetType.lua new file mode 100644 index 0000000000..ad59cfcd6a --- /dev/null +++ b/BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Utils/AssetType.lua @@ -0,0 +1,126 @@ +local FFlagFixGetAssetTypeErrorHandling = game:DefineFastFlag("FixGetAssetTypeErrorHandling", false) +local FFlagStudioUILibFixAssetTypeMap = game:DefineFastFlag("StudioUILibFixAssetTypeMap", false) +local FFlagStudioFixMeshPartPreview = game:DefineFastFlag("StudioFixMeshPartPreview", false) +local FFlagEnableToolboxVideos = game:GetFastFlag("EnableToolboxVideos") + +local AssetType = {} + +AssetType.TYPES = { + ModelType = 1, -- MeshPart, Mesh, Model + ImageType = 2, + SoundType = 3, -- Sound comes with the model or mesh. + ScriptType = 4, -- Server, local, module + PluginType = 5, + OtherType = 6, + LoadingType = 7, + VideoType = 8, +} + +-- For check if we show preview button or not. +AssetType.AssetTypesPreviewEnabled = { + [Enum.AssetType.Mesh.Value] = true, + [Enum.AssetType.MeshPart.Value] = true, + [Enum.AssetType.Model.Value] = true, + [Enum.AssetType.Decal.Value] = true, + [Enum.AssetType.Image.Value] = true, + [Enum.AssetType.Audio.Value] = true, + [Enum.AssetType.Lua.Value] = true, + [Enum.AssetType.Plugin.Value] = true, + [Enum.AssetType.Video.Value] = FFlagEnableToolboxVideos or nil, +} + +local classTypeMap = { + BasePart = AssetType.TYPES.ModelType, + Model = AssetType.TYPES.ModelType, + BackpackItem = AssetType.TYPES.ModelType, + Accoutrement = AssetType.TYPES.ModelType, + + Decal = AssetType.TYPES.ImageType, + ImageLabel = AssetType.TYPES.ImageType, + ImageButton = AssetType.TYPES.ImageType, + Texture =AssetType.TYPES.ImageType, + Sky = AssetType.TYPES.ImageType, + + Sound = AssetType.TYPES.SoundType, + VideoFrame = AssetType.TYPES.VideoType, + + BaseScript = AssetType.TYPES.ScriptType, +} + +if FFlagStudioUILibFixAssetTypeMap then + classTypeMap.Part = AssetType.TYPES.ModelType +end + +if FFlagStudioFixMeshPartPreview then + classTypeMap.MeshPart = AssetType.TYPES.ModelType +end + +-- For AssetPreview, we divide assets into four categories. +-- For any parts or meshes, we will need to do a model preview. +-- For images, we show only an image. +-- For sound, we will need to show something and provide play control. (Will +-- probably improve this in the future) +-- For BaseScript, show only names while for all other types show assetName and type +function AssetType:getAssetType(assetInstance) + local notInstance + if FFlagFixGetAssetTypeErrorHandling then + notInstance = not assetInstance or typeof(assetInstance) ~= "Instance" + else + notInstance = not assetInstance + end + + if notInstance then + return self.TYPES.LoadingType + end + local className = assetInstance.className + local type = classTypeMap[className] + + if not type then + return self.TYPES.OtherType + end + + return type +end + +function AssetType:isModel(currentType) + return currentType == self.TYPES.ModelType +end + +function AssetType:isImage(currentType) + return currentType == self.TYPES.ImageType +end + +function AssetType:isAudio(currentType) + return currentType == self.TYPES.SoundType +end + +function AssetType:isScript(currentType) + return currentType == self.TYPES.ScriptType +end + +function AssetType:isPlugin(currentType) + return currentType == self.TYPES.PluginType +end + +function AssetType:markAsPlugin() + return self.TYPES.PluginType +end + +function AssetType:isOtherType(currentType) + return currentType == self.TYPES.OtherType +end + +function AssetType:isLoading(currentType) + return currentType == self.TYPES.LoadingType +end + +function AssetType:isVideo(currentType) + return currentType == self.TYPES.VideoType +end + +function AssetType:isPreviewAvailable(typeId) + assert(typeId ~= nil, "AssetPreviewType can't be nil") + return AssetType.AssetTypesPreviewEnabled[typeId] ~= nil +end + +return AssetType \ No newline at end of file diff --git a/BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Utils/AssetType.spec.lua b/BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Utils/AssetType.spec.lua new file mode 100644 index 0000000000..f4bbeee4ce --- /dev/null +++ b/BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Utils/AssetType.spec.lua @@ -0,0 +1,24 @@ +return function() + local AssetType = require(script.Parent.AssetType) + + describe("isPreviewAvailable()", function() + it("should make sure the assetPreviewType is nil.", function() + expect(function() + AssetType.isPreviewAvailable(nil) + end).to.throw() + end) + + it("should show preview for sound.", function() + local typeId = Enum.AssetType.Audio.Value + local result = AssetType:isPreviewAvailable(typeId) + expect(result).to.equal(true) + end) + + it("should not show preview for LeftArm.", function() + local typeId = Enum.AssetType.LeftArm.Value + local result = AssetType:isPreviewAvailable(typeId) + expect(result).to.equal(false) + end) + end) + +end diff --git a/BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Utils/GetClassIcon.lua b/BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Utils/GetClassIcon.lua new file mode 100644 index 0000000000..4d3cf1a3e7 --- /dev/null +++ b/BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Utils/GetClassIcon.lua @@ -0,0 +1,31 @@ +local FFlagStudioFixGetClassIcon = settings():GetFFlag("StudioFixGetClassIcon") +local FFlagStudioMinorFixesForAssetPreview = settings():GetFFlag("StudioMinorFixesForAssetPreview") + +local StudioService = game:GetService("StudioService") + +local function GetClassIcon(instance) + if FFlagStudioFixGetClassIcon then + local className = instance.ClassName + if instance.IsA then + if instance:IsA("JointInstance") and className == "ManualWeld" or className == "ManualGlue" then + return StudioService:GetClassIcon("JointInstance") + end + end + return StudioService:GetClassIcon(className) + else + if FFlagStudioMinorFixesForAssetPreview then + if typeof(instance) ~= "Instance" then + return StudioService:GetClassIcon("Model") + end + end + + local className = instance.ClassName + if instance:IsA("JointInstance") and className == "ManualWeld" or className == "ManualGlue" then + return StudioService:GetClassIcon("JointInstance") + else + return StudioService:GetClassIcon(className) + end + end +end + +return GetClassIcon diff --git a/BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Utils/GetClassIcon.spec.lua b/BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Utils/GetClassIcon.spec.lua new file mode 100644 index 0000000000..6d1800ed04 --- /dev/null +++ b/BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Utils/GetClassIcon.spec.lua @@ -0,0 +1,43 @@ +-- Icon location is determined by an offset within a sequential list of icons in a single image. +local ICON_NOTFOUND = Vector2.new(0,0) +local ICON_JOINTINSTANCE = Vector2.new(544,0) +local ICON_SCRIPT = Vector2.new(96,0) + +return function() + local GetClassIcon = require(script.Parent.GetClassIcon) + + describe("getClassIcon", function() + it("should correctly return 'JointInstance' classIcon for ManualWelds", function() + local manualWeld = Instance.new("ManualWeld") + local classIconTable = GetClassIcon(manualWeld) + + expect(classIconTable.ImageRectOffset).to.equal(ICON_JOINTINSTANCE) + expect(classIconTable.ImageRectOffset).never.to.equal(ICON_NOTFOUND) + end) + + it("should correctly return 'JointInstance' classIcon for ManualGlues", function() + local manualGlue = Instance.new("ManualGlue") + local classIconTable = GetClassIcon(manualGlue) + + expect(classIconTable.ImageRectOffset).to.equal(ICON_JOINTINSTANCE) + expect(classIconTable.ImageRectOffset).never.to.equal(ICON_NOTFOUND) + end) + + it("should return the Script Class Icon for scripts", function() + local script = Instance.new("Script") + local classIconTable = GetClassIcon(script) + + expect(classIconTable.ImageRectOffset).to.equal(ICON_SCRIPT) + expect(classIconTable.ImageRectOffset).never.to.equal(ICON_NOTFOUND) + end) + + it("should support non-instance objects that have a ClassName member", function() + local notAnInstance = { + ClassName = "Folder" + } + + local classIconTable = GetClassIcon(notAnInstance) + expect(classIconTable).to.be.ok() + end) + end) +end \ No newline at end of file diff --git a/BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Utils/GetTextSize.lua b/BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Utils/GetTextSize.lua new file mode 100644 index 0000000000..37984ffb20 --- /dev/null +++ b/BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Utils/GetTextSize.lua @@ -0,0 +1,20 @@ +local TextService = game:GetService("TextService") + +local FStringMainFont = game:DefineFastString("StudioBuiltinPluginDefaultFont", "Gotham") + +local FONT_SIZE_MEDIUM = 16 +local FONT = Enum.Font.Gotham +pcall(function() + FONT = Enum.Font[FStringMainFont] +end) + +local function GetTextSize(text, fontSize, font, frameSize) + + fontSize = fontSize or FONT_SIZE_MEDIUM + font = font or FONT + frameSize = frameSize or Vector2.new(math.huge, math.huge) + + return TextService:GetTextSize(text, fontSize, font, frameSize) +end + +return GetTextSize \ No newline at end of file diff --git a/BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Utils/Immutable.lua b/BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Utils/Immutable.lua new file mode 100644 index 0000000000..a73d2035b1 --- /dev/null +++ b/BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Utils/Immutable.lua @@ -0,0 +1,141 @@ +--[[ + Provides functions for manipulating immutable data structures. +]] + +local Immutable = {} + +--[[ + Merges dictionary-like tables together. +]] +function Immutable.JoinDictionaries(...) + local result = {} + + for i = 1, select("#", ...) do + local dictionary = select(i, ...) + for key, value in pairs(dictionary) do + result[key] = value + end + end + + return result +end + +--[[ + Joins any number of lists together into a new list +]] +function Immutable.JoinLists(...) + local new = {} + + for listKey = 1, select("#", ...) do + local list = select(listKey, ...) + local len = #new + + for itemKey = 1, #list do + new[len + itemKey] = list[itemKey] + end + end + + return new +end + +--[[ + Creates a new copy of the dictionary and sets a value inside it. +]] +function Immutable.Set(dictionary, key, value) + local new = {} + + for key, value in pairs(dictionary) do + new[key] = value + end + + new[key] = value + + return new +end + +--[[ + Creates a new copy of the list with the given elements appended to it. +]] +function Immutable.Append(list, ...) + local new = {} + local len = #list + + for key = 1, len do + new[key] = list[key] + end + + for i = 1, select("#", ...) do + new[len + i] = select(i, ...) + end + + return new +end + +--[[ + Remove elements from a dictionary +]] +function Immutable.RemoveFromDictionary(dictionary, ...) + local result = {} + + for key, value in pairs(dictionary) do + local found = false + for listKey = 1, select("#", ...) do + if key == select(listKey, ...) then + found = true + break + end + end + if not found then + result[key] = value + end + end + + return result +end + +--[[ + Remove the given key from the list. +]] +function Immutable.RemoveFromList(list, removeIndex) + local new = {} + + for i = 1, #list do + if i ~= removeIndex then + table.insert(new, list[i]) + end + end + + return new +end + +--[[ + Remove the range from the list starting from the index. +]] +function Immutable.RemoveRangeFromList(list, index, count) + local new = {} + + for i = 1, #list do + if i < index or i >= index + count then + table.insert(new, list[i]) + end + end + + return new +end + +--[[ + Creates a new list that has no occurrences of the given value. +]] +function Immutable.RemoveValueFromList(list, removeValue) + local new = {} + + for i = 1, #list do + if list[i] ~= removeValue then + table.insert(new, list[i]) + end + end + + return new +end + +return Immutable diff --git a/BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Utils/Immutable.spec.lua b/BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Utils/Immutable.spec.lua new file mode 100644 index 0000000000..12ad39a52a --- /dev/null +++ b/BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Utils/Immutable.spec.lua @@ -0,0 +1,284 @@ +return function() + local Immutable = require(script.Parent.Immutable) + + describe("JoinDictionaries", function() + it("should preserve immutability", function() + local a = {} + local b = {} + + local c = Immutable.JoinDictionaries(a, b) + + expect(c).never.to.equal(a) + expect(c).never.to.equal(b) + end) + + it("should treat list-like values like dictionary values", function() + local a = { + [1] = 1, + [2] = 2, + [3] = 3 + } + + local b = { + [1] = 11, + [2] = 22 + } + + local c = Immutable.JoinDictionaries(a, b) + + expect(c[1]).to.equal(b[1]) + expect(c[2]).to.equal(b[2]) + expect(c[3]).to.equal(a[3]) + end) + + it("should merge dictionary values correctly", function() + local a = { + hello = "world", + foo = "bar" + } + + local b = { + foo = "baz", + tux = "penguin" + } + + local c = Immutable.JoinDictionaries(a, b) + + expect(c.hello).to.equal(a.hello) + expect(c.foo).to.equal(b.foo) + expect(c.tux).to.equal(b.tux) + end) + + it("should merge multiple dictionaries", function() + local a = { + foo = "yes" + } + + local b = { + bar = "yup" + } + + local c = { + baz = "sure" + } + + local d = Immutable.JoinDictionaries(a, b, c) + + expect(d.foo).to.equal(a.foo) + expect(d.bar).to.equal(b.bar) + expect(d.baz).to.equal(c.baz) + end) + end) + + describe("JoinLists", function() + it("should preserve immutability", function() + local a = {} + local b = {} + + local c = Immutable.JoinLists(a, b) + + expect(c).never.to.equal(a) + expect(c).never.to.equal(b) + end) + + it("should treat list-like values correctly", function() + local a = {1, 2, 3} + local b = {4, 5, 6} + + local c = Immutable.JoinLists(a, b) + + expect(#c).to.equal(6) + + for i = 1, #c do + expect(c[i]).to.equal(i) + end + end) + + it("should merge multiple lists", function() + local a = {1, 2} + local b = {3, 4} + local c = {5, 6} + + local d = Immutable.JoinLists(a, b, c) + + expect(#d).to.equal(6) + + for i = 1, #d do + expect(d[i]).to.equal(i) + end + end) + end) + + describe("Set", function() + it("should preserve immutability", function() + local a = {} + + local b = Immutable.Set(a, "foo", "bar") + + expect(b).never.to.equal(a) + end) + + it("should treat numeric keys normally", function() + local a = {1, 2, 3} + + local b = Immutable.Set(a, 2, 4) + + expect(b[1]).to.equal(1) + expect(b[2]).to.equal(4) + expect(b[3]).to.equal(3) + end) + + it("should overwrite dictionary-like keys", function() + local a = { + foo = "bar", + baz = "qux" + } + + local b = Immutable.Set(a, "foo", "hello there") + + expect(b.foo).to.equal("hello there") + expect(b.baz).to.equal(a.baz) + end) + end) + + describe("Append", function() + it("should preserve immutability", function() + local a = {} + + local b = Immutable.Append(a, "another happy landing") + + expect(b).never.to.equal(a) + end) + + it("should append values", function() + local a = {1, 2, 3} + local b = Immutable.Append(a, 4, 5) + + expect(#b).to.equal(5) + + for i = 1, #b do + expect(b[i]).to.equal(i) + end + end) + end) + + describe("RemoveFromDictionary", function() + it("should preserve immutability", function() + local a = { foo = "bar" } + + local b = Immutable.RemoveFromDictionary(a, "foo") + + expect(b).to.never.equal(a) + end) + + it("should remove fields from the dictionary", function() + local a = { + foo = "bar", + baz = "qux", + boof = "garply", + } + + local b = Immutable.RemoveFromDictionary(a, "foo", "boof") + + expect(b.foo).to.never.be.ok() + expect(b.baz).to.equal("qux") + expect(b.boof).to.never.be.ok() + end) + end) + + describe("RemoveFromList", function() + it("should preserve immutability", function() + local a = {1, 2, 3} + local b = Immutable.RemoveFromList(a, 2) + + expect(b).never.to.equal(a) + end) + + it("should remove elements from the list", function() + local a = {1, 2, 3} + local b = Immutable.RemoveFromList(a, 2) + + expect(b[1]).to.equal(1) + expect(b[2]).to.equal(3) + expect(b[3]).never.to.be.ok() + end) + end) + + describe("RemoveRangeFromList", function() + it("should preserve immutability", function() + local a = {1, 2, 3} + local b = Immutable.RemoveRangeFromList(a, 2, 1) + + expect(b).never.to.equal(a) + end) + + it("should remove single elements from the middle of the list", function() + local a = {1, 2, 3} + local b = Immutable.RemoveRangeFromList(a, 2, 1) + + expect(b[1]).to.equal(1) + expect(b[2]).to.equal(3) + expect(b[3]).never.to.be.ok() + end) + + it("should remove multiple elements from the front of the list", function() + local a = {1, 2, 3, 4, 5, 6} + local b = Immutable.RemoveRangeFromList(a, 1, 4) + + expect(b[1]).to.equal(5) + expect(b[2]).to.equal(6) + expect(b[3]).never.to.be.ok() + end) + + it("should remove multiple elements properly from middle of the list", function() + local a = {1, 2, 3, 4, 5, 6} + local b = Immutable.RemoveRangeFromList(a, 2, 4) + + expect(b[1]).to.equal(1) + expect(b[2]).to.equal(6) + expect(b[3]).never.to.be.ok() + end) + + it("should remove multiple elements properly from the end of the list", function() + local a = {1, 2, 3, 4, 5, 6, 7} + local b = Immutable.RemoveRangeFromList(a, 4, 4) + + expect(b[1]).to.equal(1) + expect(b[2]).to.equal(2) + expect(b[3]).to.equal(3) + expect(b[4]).never.to.be.ok() + end) + + it("should not remove any elements when count is 0 or less", function() + local a = {1, 2, 3} + local b = Immutable.RemoveRangeFromList(a, 2, 0) + + expect(b[1]).to.equal(1) + expect(b[2]).to.equal(2) + expect(b[3]).to.equal(3) + + local c = Immutable.RemoveRangeFromList(a, 2, -1) + expect(c[1]).to.equal(1) + expect(c[2]).to.equal(2) + expect(c[3]).to.equal(3) + end) + end) + + describe("RemoveValueFromList", function() + it("should preserve immutability", function() + local a = {1, 1, 1} + local b = Immutable.RemoveValueFromList(a, 1) + + expect(b).never.to.equal(a) + end) + + it("should remove all elements from the list", function() + local a = {1, 2, 2, 3} + local b = Immutable.RemoveValueFromList(a, 2) + + expect(b[1]).to.equal(1) + expect(b[2]).to.equal(3) + expect(b[3]).never.to.be.ok() + end) + end) +end \ No newline at end of file diff --git a/BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Utils/InsertToolEvent.lua b/BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Utils/InsertToolEvent.lua new file mode 100644 index 0000000000..3e399ab0d0 --- /dev/null +++ b/BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Utils/InsertToolEvent.lua @@ -0,0 +1,51 @@ +local InsertToolEvent = {} +InsertToolEvent.__index = InsertToolEvent + +InsertToolEvent.INSERT_TO_WORKSPACE = 0 +InsertToolEvent.INSERT_TO_STARTER_PACK = 1 +InsertToolEvent.INSERT_CANCELLED = 2 + +function InsertToolEvent.new(onPromptCallback) + local self = { + _onPromptCallback = onPromptCallback, + _bindable = Instance.new("BindableEvent"), + _waiting = false, + } + setmetatable(self, InsertToolEvent) + return self +end + +function InsertToolEvent:isWaiting() + return self._waiting +end + +function InsertToolEvent:destroy() + self:cancel() + self._bindable:Destroy() +end + +function InsertToolEvent:insertToWorkspace() + self._bindable:Fire(InsertToolEvent.INSERT_TO_WORKSPACE) +end + +function InsertToolEvent:insertToStarterPack() + self._bindable:Fire(InsertToolEvent.INSERT_TO_STARTER_PACK) +end + +function InsertToolEvent:cancel() + self._bindable:Fire(InsertToolEvent.INSERT_CANCELLED) +end + +function InsertToolEvent:promptAndWait() + if self._waiting then + return InsertToolEvent.INSERT_CANCELLED + end + + self._waiting = true + self._onPromptCallback() + local result = self._bindable.Event:Wait() + self._waiting = false + return result +end + +return InsertToolEvent diff --git a/BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Utils/LayoutOrderIterator.lua b/BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Utils/LayoutOrderIterator.lua new file mode 100644 index 0000000000..3f5186b038 --- /dev/null +++ b/BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Utils/LayoutOrderIterator.lua @@ -0,0 +1,37 @@ +--[[ + LayoutOrderIterator + Dynamically generates the "LayoutOrder = ..." so LayoutOrder for all elements + does not need to be adjusted when adding or removing elements. + + e.g. + local orderIterator = LayoutOrderIterator.new() + ... + Part1 = Roact.createElement(..., { + ... + LayoutOrder = orderIterator:getNextOrder(), + ... + }), + Part2 = Roact.createElement(..., { + ... + LayoutOrder = orderIterator:getNextOrder(), + ... + }), +]] + +local LayoutOrderIterator = {} +LayoutOrderIterator.__index = LayoutOrderIterator + +function LayoutOrderIterator.new() + local self = setmetatable({}, LayoutOrderIterator) + + self.order = 0 + + return self +end + +function LayoutOrderIterator:getNextOrder() + self.order = self.order + 1 + return self.order +end + +return LayoutOrderIterator \ No newline at end of file diff --git a/BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Utils/LayoutOrderIterator.spec.lua b/BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Utils/LayoutOrderIterator.spec.lua new file mode 100644 index 0000000000..1619ac28a9 --- /dev/null +++ b/BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Utils/LayoutOrderIterator.spec.lua @@ -0,0 +1,30 @@ +return function() + local LayoutOrderIterator = require(script.Parent.LayoutOrderIterator) + + describe("new", function() + it("should construct from nothing", function() + local orderIterator = LayoutOrderIterator.new() + + expect(orderIterator).to.be.ok() + end) + end) + + describe("getNextOrder", function() + it("should correctly generate the next order", function() + local orderIterator = LayoutOrderIterator.new() + + expect(orderIterator:getNextOrder()).to.be.equal(1) + expect(orderIterator:getNextOrder()).to.be.equal(2) + expect(orderIterator:getNextOrder()).to.be.equal(3) + + end) + + it("should correctly generate the next order when more than one iterators are created", function() + local orderIterator1 = LayoutOrderIterator.new() + local orderIterator2 = LayoutOrderIterator.new() + + expect(orderIterator1:getNextOrder()).to.be.equal(1) + expect(orderIterator2:getNextOrder()).to.be.equal(1) + end) + end) +end \ No newline at end of file diff --git a/BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Utils/MathUtils.lua b/BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Utils/MathUtils.lua new file mode 100644 index 0000000000..dbafba5b63 --- /dev/null +++ b/BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Utils/MathUtils.lua @@ -0,0 +1,15 @@ +local MathUtils = {} + +MathUtils.NEAR_ZERO = 0.0001 + +function MathUtils:fuzzyEq(numOne, numTwo, epsilon) + epsilon = epsilon or MathUtils.NEAR_ZERO + return math.abs(numOne - numTwo) < epsilon +end + +function MathUtils:round(num, numDecimalPlaces) + local mult = 10^(numDecimalPlaces or 0) + return math.floor(num * mult + 0.5) / mult +end + +return MathUtils \ No newline at end of file diff --git a/BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Utils/MathUtils.spec.lua b/BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Utils/MathUtils.spec.lua new file mode 100644 index 0000000000..4a2e6195f4 --- /dev/null +++ b/BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Utils/MathUtils.spec.lua @@ -0,0 +1,40 @@ +return function() + local Utils = script.Parent + local mathUtils = require(Utils.MathUtils) + + describe("Round", function() + it("should round to specified place", function() + local num = 101.39901 + expect(mathUtils:round(num, 0)).to.be.equal(101) + expect(mathUtils:round(num, 1)).to.be.equal(101.4) + expect(mathUtils:round(num, 2)).to.be.equal(101.40) + expect(mathUtils:round(num, 3)).to.be.equal(101.399) + expect(mathUtils:round(num, 4)).to.be.equal(101.3990) + end) + + it("round should work for negative numbers", function() + local num = -0.99095 + expect(mathUtils:round(num, 0)).to.be.equal(-1) + expect(mathUtils:round(num, 1)).to.be.equal(-1) + expect(mathUtils:round(num, 2)).to.be.equal(-0.99) + expect(mathUtils:round(num, 3)).to.be.equal(-0.991) + expect(mathUtils:round(num, 4)).to.be.equal(-0.9909) + end) + end) + + describe("Fuzzy Equals", function() + it("fuzzyEq should work with no epsilon provided", function() + expect(mathUtils:fuzzyEq(2.00009, 2)).to.be.equal(true) + expect(mathUtils:fuzzyEq(2, 2)).to.be.equal(true) + expect(mathUtils:fuzzyEq(2, 3)).to.be.equal(false) + expect(mathUtils:fuzzyEq(mathUtils.NEAR_ZERO, 0)).to.be.equal(false) + end) + + it("fuzzyEq should work with supplied epsilon value", function() + expect(mathUtils:fuzzyEq(2.0000009, 2, 0.000001)).to.be.equal(true) + expect(mathUtils:fuzzyEq(2, 2, 0.1)).to.be.equal(true) + expect(mathUtils:fuzzyEq(2, 3, 0.01)).to.be.equal(false) + expect(mathUtils:fuzzyEq(0.00000001, 0, 0.00000001)).to.be.equal(false) + end) + end) +end \ No newline at end of file diff --git a/BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Utils/Signal.lua b/BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Utils/Signal.lua new file mode 100644 index 0000000000..cde7956269 --- /dev/null +++ b/BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Utils/Signal.lua @@ -0,0 +1,63 @@ +--[[ + A limited, simple implementation of a Signal. + + Handlers are fired in order, and (dis)connections are properly handled when + executing an event. + + Signal uses Immutable to avoid invalidating the 'Fire' loop iteration. +]] + +local Immutable = require(script.Parent.Immutable) + +local Signal = {} + +Signal.__index = Signal + +function Signal.new() + local self = { + _listeners = {} + } + + setmetatable(self, Signal) + + return self +end + +function Signal:connect(callback) + local listener = { + callback = callback, + isConnected = true, + } + self._listeners = Immutable.Append(self._listeners, listener) + + local function disconnect() + listener.isConnected = false + self._listeners = Immutable.RemoveValueFromList(self._listeners, listener) + end + + return { + Disconnect = function() + disconnect() + end, + disconnect = disconnect, + } +end + +function Signal:fire(...) + for _, listener in ipairs(self._listeners) do + if listener.isConnected then + listener.callback(...) + end + end +end + +function Signal:Connect(...) + return self:connect(...) +end + +function Signal:Fire(...) + self:fire(...) +end + + +return Signal diff --git a/BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Utils/Signal.spec.lua b/BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Utils/Signal.spec.lua new file mode 100644 index 0000000000..f00f9477b0 --- /dev/null +++ b/BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Utils/Signal.spec.lua @@ -0,0 +1,114 @@ +return function() + local Signal = require(script.Parent.Signal) + + it("should construct from nothing", function() + local signal = Signal.new() + + expect(signal).to.be.ok() + end) + + it("should fire connected callbacks", function() + local callCount = 0 + local value1 = "Hello World" + local value2 = 7 + + local callback = function(arg1, arg2) + expect(arg1).to.equal(value1) + expect(arg2).to.equal(value2) + callCount = callCount + 1 + end + + local signal = Signal.new() + + local connection = signal:connect(callback) + signal:fire(value1, value2) + + expect(callCount).to.equal(1) + + connection:disconnect() + signal:fire(value1, value2) + + expect(callCount).to.equal(1) + end) + + it("should disconnect handlers", function() + local callback = function() + error("Callback was called after disconnect!") + end + + local signal = Signal.new() + + local connection = signal:connect(callback) + connection:disconnect() + + signal:fire() + end) + + it("should fire handlers in order", function() + local signal = Signal.new() + local x = 0 + local y = 0 + + local callback1 = function() + expect(x).to.equal(0) + expect(y).to.equal(0) + x = x + 1 + end + + local callback2 = function() + expect(x).to.equal(1) + expect(y).to.equal(0) + y = y + 1 + end + + signal:connect(callback1) + signal:connect(callback2) + signal:fire() + + expect(x).to.equal(1) + expect(y).to.equal(1) + end) + + it("should continue firing despite mid-event disconnection", function() + local signal = Signal.new() + local countA = 0 + local countB = 0 + + local connectionA + connectionA = signal:connect(function() + connectionA:disconnect() + countA = countA + 1 + end) + + signal:connect(function() + countB = countB + 1 + end) + + signal:fire() + + expect(countA).to.equal(1) + expect(countB).to.equal(1) + end) + + it("should skip listeners that were disconnected during event evaluation", function() + local signal = Signal.new() + local countA = 0 + local countB = 0 + + local connectionB + + signal:connect(function() + countA = countA + 1 + connectionB:disconnect() + end) + + connectionB = signal:connect(function() + countB = countB + 1 + end) + + signal:fire() + + expect(countA).to.equal(1) + expect(countB).to.equal(0) + end) +end \ No newline at end of file diff --git a/BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Utils/Spritesheet.lua b/BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Utils/Spritesheet.lua new file mode 100644 index 0000000000..0b9d15893d --- /dev/null +++ b/BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Utils/Spritesheet.lua @@ -0,0 +1,86 @@ +--[[ + Parses spritesheets into a list of sprites that you can then join with Roact ImageLabel/Button props + Sprites are returned in the order they appear in the spritesheet in the form of: + { + Image = asset, + ImageRectSize = SpriteSize, + ImageRectOffset = positionOfSpriteInSheet, + } + + Required arguments: + string asset - AssetId or path to the spritesheet + table props - Table of properties, similar to creating a Roact element + + Required props: + number/Vector2 SpriteSize - how large each sprite is (must be the same for all sprites) + a number SpriteSize is converted to a uniform Vector2 + number NumSprites - how many sprites there are in the spritesheet + + Optional props: + number SpritesheetWidth - how wide the entire spritesheet is. Defaults to 1024 (max image size) + You do not need to change this unless you break your sprites onto a new + line before X=1024 when there is still enough room for another sprite, or + your spritesheet is wider than 1024 (Don't do this! The engine will automatically + downscale images larger than the max size) + + Example usage: + + local expandButton = Spritesheet("rbxasset://textures/folder/expand.png", { + SpriteSize = 32, + NumSprites = 4, + } + + local onStateImageProps = expandButton[1] + local offStateImageProps = expandButton[2] + + component:render() + local expandImageProps = self.state.expanded and onStateImage or offStateImage + Roact.createElement("ImageLabel", Cryo.Dictionary.join(expandImageProps, { + ... + })) +]] + +-- TODO check if 1K limitation is necessary in /content or only web +local MAX_IMAGE_SIZE = 1024 + +local function Spritesheet(image, props) + local spriteSizeType = typeof(props.SpriteSize) + local spriteCountType = typeof(props.NumSprites) + local sheetWidthType = typeof(props.SpritesheetWidth) + + assert(spriteSizeType == "number" or spriteSizeType == "Vector2", + "SpriteSize must be number or Vector2. Got type '"..spriteSizeType.."'") + assert(spriteCountType == "number", + "NumSprites must be number. Got type'"..spriteCountType.."'") + assert(sheetWidthType == "number" or sheetWidthType == "nil", + "SpritesheetWidth must be a number or nil. Got '"..sheetWidthType.."'") + + local spriteSize = spriteSizeType == "number" and Vector2.new(1, 1) * props.SpriteSize or props.SpriteSize + local numSprites = props.NumSprites + local sheetWidth = props.SpritesheetWidth or MAX_IMAGE_SIZE + + assert(spriteSize.X > 0 and spriteSize.Y > 0, + "SpriteSize does not support <= 0 values. Got '"..tostring(spriteSize).."'") + assert(numSprites > 0, + "NumSprites must be > 0. Got '"..numSprites) + assert(sheetWidth > 0, + "SpritesheetWidth does not support <= 0 values. Got '"..sheetWidth.."'") + + local sprites = {} + + local numColumns = math.floor(sheetWidth / spriteSize.X) + for i = 0, props.NumSprites - 1 do + local row = math.floor(i / numColumns) + local column = i % numColumns + + table.insert(sprites, { + Image = image, + ImageRectSize = spriteSize, + ImageRectOffset = Vector2.new(column * spriteSize.X, row * spriteSize.Y), + }) + end + + return sprites +end + +return Spritesheet \ No newline at end of file diff --git a/BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Utils/Spritesheet.spec.lua b/BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Utils/Spritesheet.spec.lua new file mode 100644 index 0000000000..479459d975 --- /dev/null +++ b/BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Utils/Spritesheet.spec.lua @@ -0,0 +1,115 @@ +return function() + local Spritesheet = require(script.Parent.Spritesheet) + + it("should verify correct props", function() + local success,_ + + -- Missing required props + success,_ = pcall(function() + return Spritesheet("", {}) + end) + expect(success).to.equal(false) + + -- Missing required prop NumSprites + success,_ = pcall(function() + return Spritesheet("", {SpriteSize = 1, NumSprites = nil}) + end) + expect(success).to.equal(false) + + -- Missing required props SpriteSize + success,_ = pcall(function() + return Spritesheet("", {SpriteSize = nil, NumSprites = 1}) + end) + expect(success).to.equal(false) + + -- SpritesheetWidth is invalid type + success,_ = pcall(function() + return Spritesheet("", {SpriteSize = 1, NumSprites = 1, SpritesheetWidth = ""}) + end) + expect(success).to.equal(false) + + -- Has all required props of correct type + success,_ = pcall(function() + return Spritesheet("", {SpriteSize = 1, NumSprites = 1}) + end) + expect(success).to.equal(true) + + -- Has all required props of correct type + success,_ = pcall(function() + return Spritesheet("", {SpriteSize = Vector2.new(1,1), NumSprites = 1}) + end) + expect(success).to.equal(true) + + -- Has all props of correct type + success,_ = pcall(function() + return Spritesheet("", {SpriteSize = 1, NumSprites = 1, SpritesheetWidth = 1}) + end) + expect(success).to.equal(true) + + -- SpriteSize out of range + success,_ = pcall(function() + return Spritesheet("", {SpriteSize = 0, NumSprites = 1}) + end) + expect(success).to.equal(false) + + -- NumSprites out of range + success,_ = pcall(function() + return Spritesheet("", {SpriteSize = 1, NumSprites = 0}) + end) + expect(success).to.equal(false) + + -- SpritesheetWidth out of range + success,_ = pcall(function() + return Spritesheet("", {SpriteSize = 1, NumSprites = 1, SpritesheetWidth = 0}) + end) + expect(success).to.equal(false) + end) + + it("should return correct result for a single-row spritesheet", function() + local asset = "rbxasset://test" + local numSprites = 3 + local spriteSize = Vector2.new(32, 16) + local sprites = Spritesheet(asset, { + NumSprites = numSprites, + SpriteSize = spriteSize, + }) + + -- Correct number of sprites + expect(#sprites).to.equal(numSprites) + + -- Correct sprite properties + for i,sprite in pairs(sprites) do + expect(sprite.Image).to.equal(asset) + expect(sprite.ImageRectSize).to.equal(spriteSize) + expect(sprite.ImageRectOffset.X).to.equal((i - 1) * spriteSize.X) + expect(sprite.ImageRectOffset.Y).to.equal(0) + end + end) + + it("should return correct result for a multi-row spritesheet", function() + local asset = "rbxasset://test" + local numSprites = 5 + local spriteSize = Vector2.new(32, 16) + local sheetWidth = 66 + local sprites = Spritesheet(asset, { + NumSprites = numSprites, + SpriteSize = spriteSize, + SpritesheetWidth = sheetWidth, + }) + + -- Correct number of sprites + expect(#sprites).to.equal(numSprites) + + -- Correct sprite properties + local numColumns = math.floor(sheetWidth / spriteSize.X) + for i,sprite in pairs(sprites) do + local row = math.floor((i - 1) / numColumns) + local column = (i - 1) % numColumns + + expect(sprite.Image).to.equal(asset) + expect(sprite.ImageRectSize).to.equal(spriteSize) + expect(sprite.ImageRectOffset.X).to.equal(column * spriteSize.X) + expect(sprite.ImageRectOffset.Y).to.equal(row * spriteSize.Y) + end + end) +end \ No newline at end of file diff --git a/BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Utils/Symbol.lua b/BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Utils/Symbol.lua new file mode 100644 index 0000000000..d9e26d9c65 --- /dev/null +++ b/BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Utils/Symbol.lua @@ -0,0 +1,44 @@ +--[[ + A 'Symbol' is an opaque marker type. + + Symbols have the type 'userdata', but when printed to the console, the name + of the symbol is shown. +]] + +local Symbol = {} + +--[[ + Creates a Symbol with the given name. + + When printed or coerced to a string, the symbol will turn into the string + given as its name. +]] +function Symbol.named(name) + assert(type(name) == "string", "Symbols must be created using a string name!") + + local self = newproxy(true) + + local wrappedName = ("Symbol(%s)"):format(name) + + getmetatable(self).__tostring = function() + return wrappedName + end + + return self +end + +--[[ + Create an unnamed Symbol. Usually, you should create a named Symbol using + Symbol.named(name) +]] +function Symbol.unnamed() + local self = newproxy(true) + + getmetatable(self).__tostring = function() + return "Unnamed Symbol" + end + + return self +end + +return Symbol \ No newline at end of file diff --git a/BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Utils/Symbol.spec.lua b/BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Utils/Symbol.spec.lua new file mode 100644 index 0000000000..f3312055c9 --- /dev/null +++ b/BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Utils/Symbol.spec.lua @@ -0,0 +1,45 @@ +return function() + local Symbol = require(script.Parent.Symbol) + + describe("named", function() + it("should give an opaque object", function() + local symbol = Symbol.named("foo") + + expect(symbol).to.be.a("userdata") + end) + + it("should coerce to the given name", function() + local symbol = Symbol.named("foo") + + expect(tostring(symbol):match("foo")).to.be.ok() + end) + + it("should be unique when constructed", function() + local symbolA = Symbol.named("abc") + local symbolB = Symbol.named("abc") + + expect(symbolA).never.to.equal(symbolB) + end) + end) + + describe("unnamed", function() + it("should give an opaque object", function() + local symbol = Symbol.unnamed() + + expect(symbol).to.be.a("userdata") + end) + + it("should coerce to some string", function() + local symbol = Symbol.unnamed() + + expect(tostring(symbol)).to.be.a("string") + end) + + it("should be unique when constructed", function() + local symbolA = Symbol.unnamed() + local symbolB = Symbol.unnamed() + + expect(symbolA).never.to.equal(symbolB) + end) + end) +end diff --git a/BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Utils/Urls.lua b/BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Utils/Urls.lua new file mode 100644 index 0000000000..5327c279e5 --- /dev/null +++ b/BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Utils/Urls.lua @@ -0,0 +1,43 @@ +local ContentProvider = game:GetService("ContentProvider") + +-- helper functions +local function parseBaseUrlInformation() + -- get the current base url from the current configuration + local baseUrl = ContentProvider.BaseUrl + -- keep a copy of the base url + -- append a trailing slash if there isn't one + if baseUrl:sub(#baseUrl) ~= "/" then + baseUrl = baseUrl .. "/" + end + -- parse out scheme (http, https) + local _, schemeEnd = baseUrl:find("://") + -- parse out the prefix (www, kyle, ying, etc.) + local prefixIndex, prefixEnd = baseUrl:find("%.", schemeEnd + 1) + local basePrefix = baseUrl:sub(schemeEnd + 1, prefixIndex - 1) + -- parse out the domain + local baseDomain = baseUrl:sub(prefixEnd + 1) + return baseUrl, basePrefix, baseDomain +end + +-- url construction building blocks +local baseUrl, basePrefix, baseDomain = parseBaseUrlInformation() + +local BASE_GAMEASSET_URL = "https://assetgame.%sasset/?id=%d&#assetTypeId=%d&isPackage=%s" +local RBXTHUMB_BASE_URL = "rbxthumb://type=%s&id=%d&w=%d&h=%d" +local ASSET_ID_STRING = "rbxassetid://%d" + +local Urls = {} + +function Urls.constructAssetThumbnailUrl(assetId, width, height) + return RBXTHUMB_BASE_URL:format("Asset", tonumber(assetId) or 0, width, height) +end + +function Urls.constructAssetIdString(assetId) + return ASSET_ID_STRING:format(assetId) +end + +function Urls.constructAssetGameAssetIdUrl(assetId, assetTypeId, isPackage) + return BASE_GAMEASSET_URL:format(baseDomain, assetId, assetTypeId, isPackage) +end + +return Urls \ No newline at end of file diff --git a/BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Utils/getTimeString.lua b/BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Utils/getTimeString.lua new file mode 100644 index 0000000000..3cb586f375 --- /dev/null +++ b/BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Utils/getTimeString.lua @@ -0,0 +1,11 @@ +-- return mm:ss format. +return function(seconds) + assert(type(seconds) == "number", "seconds must be a number") + + local isNegative = seconds < 0 + local adjustedSeconds = math.abs(seconds) + local min = math.floor(adjustedSeconds / 60) + local sec = math.floor(adjustedSeconds % 60) + + return string.format("%s%d:%02d", isNegative and "-" or "", min, sec) +end \ No newline at end of file diff --git a/BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Utils/getTimeString.spec.lua b/BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Utils/getTimeString.spec.lua new file mode 100644 index 0000000000..d24dd1953c --- /dev/null +++ b/BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/Utils/getTimeString.spec.lua @@ -0,0 +1,51 @@ +return function() + local getTimeString = require(script.Parent.getTimeString) + + it("should return a string", function() + local result = getTimeString(0) + + expect(result).to.be.a("string") + end) + + it("should handle 30 seconds correctly", function() + local result = getTimeString(30) + + expect(result).to.equal("0:30") + end) + + it("should ignore decimals", function() + local result = getTimeString(1.5) + + expect(result).to.equal("0:01") + end) + + it("should ignore negative decimals", function() + local result = getTimeString(-1.5) + + expect(result).to.equal("-0:01") + end) + + it("should handle 60 seconds correctly", function() + local result = getTimeString(60) + + expect(result).to.equal("1:00") + end) + + it("should handle 90 seconds correctly", function() + local result = getTimeString(90) + + expect(result).to.equal("1:30") + end) + + it("should handle 120 seconds correctly", function() + local result = getTimeString(120) + + expect(result).to.equal("2:00") + end) + + it("should handle 150 seconds correctly", function() + local result = getTimeString(150) + + expect(result).to.equal("2:30") + end) +end \ No newline at end of file diff --git a/BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/createTheme.lua b/BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/createTheme.lua new file mode 100644 index 0000000000..b9dd6a2d70 --- /dev/null +++ b/BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/createTheme.lua @@ -0,0 +1,578 @@ +--[[ + Transforms values from an app theme into a theme usable by the UILibrary. + + Parameters: + table style: + Specifies default style values which will be used to construct the theme. + These include the basic palette colors (backgroundColor, textColor, etc), + as well as other top-level fonts and sizes. Refer to defaultStyle for + a list of style values that can be overridden. + + table overrides (optional): + After the theme is created, overrides can be set for specific elements. + + For example, an overrides table of: + { + checkBox.backgroundColor = Color3.new(1, 1, 1), + } + + Would change the background color of only the Checkbox component. +]] + +local Style = require(script.Parent.StyleDefaults) +local replaceDefaults = require(script.Parent.deepJoin) + +return function(style, overrides) + style = style or {} + overrides = overrides or {} + + style = replaceDefaults(Style.Defaults, style) + assert(Style.isValid(style), "Provided style table could not be validated.") + + -- Theme entries for UILibrary components are defined below + local checkBox = { + font = style.font, + + --TODO: Move texture to StudioSharedUI + backgroundImage = "rbxasset://textures/GameSettings/UncheckedBox.png", + selectedImage = "rbxasset://textures/GameSettings/CheckedBoxLight.png", + + backgroundColor = style.backgroundColor, + titleColor = style.textColor, + } + + local roundFrame = { + --TODO: Move texture to StudioSharedUI + backgroundImage = "rbxasset://textures/StudioToolbox/RoundedBackground.png", + borderImage = "rbxasset://textures/StudioToolbox/RoundedBorder.png", + slice = Rect.new(3, 3, 13, 13), + } + + local dropShadow = { + --TODO: Move texture to StudioSharedUI + image = "rbxasset://textures/StudioUIEditor/resizeHandleDropShadow.png", + } + + local tooltip = { + font = style.font, + textSize = 12, + + backgroundColor = style.itemColor, + borderColor = style.borderColor, + textColor = style.textColor, + shadowColor = style.shadowColor, + shadowTransparency = style.shadowTransparency, + } + + local keyframe = { + Default = { + backgroundColor = style.itemColor, + borderColor = style.borderColor, + + selected = { + backgroundColor = style.selectionColor, + borderColor = style.selectionBorderColor, + }, + }, + + Primary = { + backgroundColor = style.primaryItemColor, + borderColor = style.primaryBorderColor, + + selected = { + backgroundColor = style.primaryHoveredItemColor, + borderColor = style.selectionBorderColor, + }, + }, + } + + local scrubber = { + backgroundColor = style.selectionColor, + image = "", + } + + local scrollingFrame = { + --TODO: Move texture to StudioSharedUI + topImage = "rbxasset://textures/StudioToolbox/ScrollBarTop.png", + midImage = "rbxasset://textures/StudioToolbox/ScrollBarMiddle.png", + bottomImage = "rbxasset://textures/StudioToolbox/ScrollBarBottom.png", + + backgroundColor = style.backgroundColor, + scrollbarColor = style.borderColor, + } + + local radioButton = { + radioButtonBackground = "rbxasset://textures/GameSettings/RadioButton.png", + radioButtonColor = style.separationLineColor, + radioButtonSelected = "rbxasset://textures/ui/LuaApp/icons/ic-blue-dot.png", + textSize = 18, + buttonHeight = 20, + font = style.font, + textColor = style.textColor, + contentPadding = 16, + buttonPadding = 6, + } + + local dropdownMenu = { + borderColor = style.borderColor, + --TODO: Move texture to StudioSharedUI + borderImage = "rbxasset://textures/StudioToolbox/RoundedBorder.png", + } + + local styledDropdown = { + font = style.font, + + backgroundColor = style.backgroundColor, + borderColor = style.borderColor, + textColor = style.textColor, + + --TODO: Move texture to StudioSharedUI + arrowImage = "rbxasset://textures/StudioToolbox/ArrowDownIconWhite.png", + + hovered = { + backgroundColor = style.hoveredItemColor, + textColor = style.hoveredTextColor, + }, + + selected = { + backgroundColor = style.selectionColor, + borderColor = style.selectionBorderColor, + textColor = style.selectedTextColor, + }, + } + + local detailedDropdown = { + font = style.font, + + backgroundColor = style.backgroundColor, + disabled = style.disabledColor, + disabledText = style.dimmerTextColor, + borderColor = style.borderColor, + displayText = style.textColor, + descriptionText = style.subTextColor, + + --TODO: Move texture to StudioSharedUI + arrowImage = "rbxasset://textures/StudioToolbox/ArrowDownIconWhite.png", + + hovered = { + backgroundColor = style.hoveredItemColor, + displayText = style.hoveredTextColor, + }, + + selected = { + backgroundColor = style.selectionColor, + disabled = style.disabledColor, + borderColor = style.selectionBorderColor, + displayText = style.selectedTextColor, + }, + } + + local titledFrame = { + font = style.font, + text = style.subTextColor, + } + + local textBox = { + font = style.font, + background = style.backgroundColor, + disabled = style.disabledColor, + borderDefault = style.borderColor, + borderHover = style.hoverColor, + tooltip = style.dimmerTextColor, + text = style.textColor, + error = style.errorColor, + } + + local textButton = { + font = style.font, + } + + local textEntry = { + textTransparency = { + enabled = 0, + disabled = 0.5 + } + } + + local separator = { + lineColor = style.borderColor, + } + + local treeView = { + elementPadding = 4, + margins = { + left = 2, + top = 2, + right = 2, + bottom = 2, + }, + indentOffset = 8, + scrollbar = replaceDefaults(scrollingFrame, { + scrollbarThickness = 16, + scrollbarPadding = 2, + scrollbarImageColor = style.borderColor, + }), + defaultElementWidth = 140, + } + + local dialog = { + font = style.font, + + background = style.backgroundColor, + textColor = style.textColor, + } + + local bulletPoint = { + font = style.font, + + text = style.textColor, + } + + local button = { + Default = { + font = style.font, + isRound = true, + + backgroundColor = style.itemColor, + textColor = style.textColor, + borderColor = style.borderColor, + + hovered = { + backgroundColor = style.hoveredItemColor, + textColor = style.hoveredTextColor, + borderColor = style.borderColor, + }, + }, + + Primary = { + font = style.font, + isRound = true, + + backgroundColor = style.primaryItemColor, + textColor = style.primaryTextColor, + borderColor = style.primaryBorderColor, + + hovered = { + backgroundColor = style.primaryHoveredItemColor, + textColor = style.primaryHoveredTextColor, + borderColor = style.primaryHoveredBorderColor, + }, + }, + } + + local loadingBar = { + font = style.font, + fontSize = 16, + text = style.textColor, + bar = { + foregroundColor = style.dimmerTextColor, + backgroundColor = style.backgroundColor, + }, + } + + local loadingIndicator = { + baseColor = style.hoveredItemColor, + endColor = style.dimmerTextColor, + } + + local toggleButton = { + defaultWidth = 20, + defaultHeight = 20, + + onImage = "rbxasset://textures/RoactStudioWidgets/toggle_on_light.png", + offImage = "rbxasset://textures/RoactStudioWidgets/toggle_off_light.png", + disabledImage = "rbxasset://textures/RoactStudioWidgets/toggle_disable_light.png", + } + + local hyperlink = { + textSize = 22, + textColor = style.hyperlinkTextColor, + font = style.font, + } + + local assetPreview = { + font = style.font, + textSize = 14, + textSizeMedium = 16, + textSizeLarge = 18, + textSizeTitle= 22, + fontBold = style.font, + background = style.backgroundColor, + + padding = 12, + + assetNameColor = style.textColor, + descriptionTextColor = style.textColor, + + actionBar = { + background = style.backgroundColor, + + button = { + backgroundColor = style.primaryItemColor, + backgroundDisabledColor = style.disabledColor, + backgroundHoveredColor = style.primaryHoveredItemColor + }, + + showMore = { + backgroundColor = style.backgroundColor, + borderColor = style.borderColor + }, + + text = { + color = style.textColor, + colorDisabled = style.disabledColor, + }, + + padding = 12, + centerPadding = 10, + + robuxSize = UDim2.fromOffset(16,16), + + images = { + showMore = "rbxasset://textures/StudioToolbox/AssetPreview/more.png", + robuxSmall = "rbxasset://textures/ui/common/robux_small.png", + colorWhite = Color3.fromRGB(255, 255, 255), + } + }, + + description = { + height = 28, + + searchBarIconSize = 14, + padding = 8, + + backgroundColor = style.backgroundColor, + leftTextColor = style.textColor, + rightTextColor = style.textColor, + lineColor = style.borderColor, + + images = { + searchIcon = "rbxasset://textures/StudioToolbox/Search.png", + }, + }, + + images = { + deleteButton = "rbxasset://textures/StudioToolbox/DeleteButton.png", + scrollbarTopImage = "rbxasset://textures/StudioToolbox/ScrollBarTop.png", + scrollbarMiddleImage = "rbxasset://textures/StudioToolbox/ScrollBarMiddle.png", + scrollbarBottomImage = "rbxasset://textures/StudioToolbox/ScrollBarBottom.png", + thumbUpSmall = "rbxasset://textures/StudioToolbox/AssetPreview/rating_small.png", + }, + + favorites = { + contentColor = Color3.fromRGB(246, 183, 2), + favorited = "rbxasset://textures/StudioToolbox/AssetPreview/star_filled.png", + unfavorited = "rbxasset://textures/StudioToolbox/AssetPreview/star_stroke.png" + }, + + imagePreview = { + background = style.backgroundColor, + textColor = style.textColor, + }, + + modelPreview = { + background = style.backgroundColor, + }, + + thumbnailIconPreview = { + background = style.backgroundColor, + textColor = style.textColor, + + textLabelPadding = 20, + iconSize = 16, + defaultTextLabelHeight = 20, + }, + + treeViewButton = { + buttonSize = 28, + backgroundTrans = 0.25, + backgroundColor = style.background, + backgroundDisabledColor = style.disabledColor, + hierarchy = "rbxasset://textures/StudioToolbox/AssetPreview/hierarchy.png" + }, + + audioPreview = { + backgroundColor = style.backgroundColor, + textColor = style.textColor, + playButton = "rbxasset://textures/StudioToolbox/AssetPreview/play_button.png", + pauseButton = "rbxasset://textures/StudioToolbox/AssetPreview/pause_button.png", + buttonBackgroundColor = style.background, + buttonDisabledBackgroundColor = style.disabledColor, + buttonDisabledBackgroundTransparency = 0.5, + buttonColor = style.textColor, + audioPlay_BG = "rbxasset://textures/StudioToolbox/AssetPreview/audioPlay_BG.png", + audioPlay_BG_Color = Color3.fromRGB(204, 204, 204), + progressBar = Color3.fromRGB(0, 162, 255), + progressBar_BG_Color = style.background, + progressKnob = "rbxasset://textures/DeveloperFramework/slider_knob.png", + progressKnobColor = style.background, + font = style.font, + fontSize = 16, + }, + + videoPreview = { + backgroundColor = style.backgroundColor, + videoBackgroundColor = style.backgroundColor, + playButton = "rbxasset://textures/StudioToolbox/AssetPreview/play_button.png", + pauseButton = "rbxasset://textures/StudioToolbox/AssetPreview/pause_button.png", + pauseOverlayColor =Color3.fromRGB(0, 0, 0), + pauseOverlayTransparency = 0.5, + }, + + vote = { + backgroundTrans = 0.9, + background = style.backgroundColor, + borderColor = style.borderColor, + textColor = style.textColor, + subTextColor = style.subTextColor, + + button = { + backgroundColor = style.itemColor, + backgroundTrans = 0, + disabledColor = Color3.fromRGB(10, 10, 10), + }, + + voteUp = { + backgroundColor = Color3.fromRGB(0, 100, 0), + borderColor = style.borderColor, + }, + + voteDown = { + backgroundColor = Color3.fromRGB(100, 0, 0), + borderColor = style.borderColor, + }, + + images = { + voteDown = "rbxasset://textures/StudioToolbox/AssetPreview/vote_down.png", + voteUp = "rbxasset://textures/StudioToolbox/AssetPreview/vote_up.png", + thumbUp = "rbxasset://textures/StudioToolbox/AssetPreview/rating_large.png" + } + }, + } + + local searchBar = { + backgroundColor = style.backgroundColor, + + text = { + placeholder = { + color = style.dimmerTextColor, + }, + font = style.font, + size = 16, + color = style.textColor, + }, + + divideLine = { + color = style.borderColor, + }, + + border = { + hovered = { + color = style.hoverColor, + }, + selected = { + color = style.selectionBorderColor, + }, + color = style.borderColor, + }, + + buttons = { + iconSize = 14, + size = 28, + inset = 2, + clear = { + color = Color3.fromRGB(184, 184, 184), + }, + + search = { + hovered = { + color = Color3.fromRGB(0, 162, 255), + }, + color = Color3.fromRGB(184, 184, 184), + }, + }, + + images = { + clear = { + hovered = { + image = "rbxasset://textures/StudioSharedUI/clear-hover.png", + }, + image = "rbxasset://textures/StudioSharedUI/clear.png", + }, + + search = { + image = "rbxasset://textures/StudioSharedUI/search.png", + }, + }, + } + + local instanceTreeView = { + font = style.font, + textSize = 14, + + background = style.background, + + treeItemHeight = 16, + treeViewIndent = 20, + + scrollbarPadding = 2, + scrollbarThickness = 8, + + scrollbarTopImage = "rbxasset://textures/StudioToolbox/ScrollBarTop.png", + scrollbarMiddleImage = "rbxasset://textures/StudioToolbox/ScrollBarMiddle.png", + scrollBarBottomImage = "rbxasset://textures/StudioToolbox/ScrollBarBottom.png", + + arrowExpanded = "rbxasset://textures/StudioToolbox/ArrowExpanded.png", + arrowCollapsed = "rbxasset://textures/StudioToolbox/ArrowCollapsed.png", + + elementPadding = 4, + + borderPadding = 15, + + tooltipShowDelay = 0.3, + + arrowColor = style.textColor, + selectedText = style.selectedTextColor, + textColor = style.textColor, + selected = style.selectedTextColor, + hover = style.hoverColor, + } + + local styledTooltip = { + backgroundColor = style.itemColor, + shadowColor = style.shadowColor, + shadowTransparency = style.shadowTransparency, + shadowOffset = Vector2.new(1, 1), + } + + return replaceDefaults({ + assetPreview = assetPreview, + checkBox = checkBox, + roundFrame = roundFrame, + dropShadow = dropShadow, + tooltip = tooltip, + keyframe = keyframe, + scrollingFrame = scrollingFrame, + dropdownMenu = dropdownMenu, + styledDropdown = styledDropdown, + detailedDropdown = detailedDropdown, + titledFrame = titledFrame, + textBox = textBox, + textButton = textButton, + textEntry = textEntry, + separator = separator, + dialog = dialog, + button = button, + scrubber = scrubber, + loadingBar = loadingBar, + loadingIndicator = loadingIndicator, + bulletPoint = bulletPoint, + toggleButton = toggleButton, + radioButton = radioButton, + treeView = treeView, + hyperlink = hyperlink, + instanceTreeView = instanceTreeView, + searchBar = searchBar, + styledTooltip = styledTooltip, + }, overrides) +end \ No newline at end of file diff --git a/BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/deepJoin.lua b/BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/deepJoin.lua new file mode 100644 index 0000000000..8affe568ef --- /dev/null +++ b/BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/deepJoin.lua @@ -0,0 +1,31 @@ +local function deepJoin(t1, t2) + local new = {} + + for key, value in pairs(t1) do + if typeof(value) == "table" then + if t2[key] and typeof(t2[key]) == "table" then + new[key] = deepJoin(value, t2[key]) + else + -- this essentially acts like a deepcopy to prevent + -- references getting all tangled up + new[key] = deepJoin(value, {}) + end + else + new[key] = value + end + end + + for key, value in pairs(t2) do + if typeof(value) == "table" then + if not t1[key] then + new[key] = deepJoin(value, {}) + end + else + new[key] = value + end + end + + return new +end + +return deepJoin \ No newline at end of file diff --git a/BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/deepJoin.spec.lua b/BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/deepJoin.spec.lua new file mode 100644 index 0000000000..de11f7c5d5 --- /dev/null +++ b/BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/deepJoin.spec.lua @@ -0,0 +1,76 @@ +return function() + local Library = script.Parent + local deepJoin = require(Library.deepJoin) + + it("should join two tables together", function() + local tableA = {key1 = "Value1"} + local tableB = {key2 = "Value2"} + + local result = deepJoin(tableA, tableB) + + expect(result.key1).to.equal("Value1") + expect(result.key2).to.equal("Value2") + end) + + it("should add all the entries from the first table", function() + local tableA = {key1 = "Value1", key2 = "Value2"} + local tableB = {} + + local result = deepJoin(tableA, tableB) + + expect(result.key1).to.equal("Value1") + expect(result.key2).to.equal("Value2") + end) + + it("should add all the entries from the second table", function() + local tableA = {} + local tableB = {key1 = "Value1", key2 = "Value2"} + + local result = deepJoin(tableA, tableB) + + expect(result.key1).to.equal("Value1") + expect(result.key2).to.equal("Value2") + end) + + it("should join values in nested tables", function() + local tableA = { + set = { + key1 = "Value1", + }, + } + + local tableB = { + set = { + key2 = "Value2", + }, + } + + local result = deepJoin(tableA, tableB) + + expect(result.set).to.be.ok() + expect(result.set.key1).to.equal("Value1") + expect(result.set.key2).to.equal("Value2") + end) + + it("should prioritize the second table if values overlap", function() + local tableA = { + outsideKey = "Old", + set = { + insideKey = "Old", + }, + } + + local tableB = { + outsideKey = "New", + set = { + insideKey = "New", + }, + } + + local result = deepJoin(tableA, tableB) + + expect(result.outsideKey).to.equal("New") + expect(result.set).to.be.ok() + expect(result.set.insideKey).to.equal("New") + end) +end \ No newline at end of file diff --git a/BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/join.lua b/BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/join.lua new file mode 100644 index 0000000000..2e0d809150 --- /dev/null +++ b/BuiltInPlugins/PlayerEmulator/Packages/UILibrary/_internal/join.lua @@ -0,0 +1,15 @@ +local function join(...) + local new = {} + + for i = 1, select("#", ...) do + local source = select(i, ...) + + for key, value in pairs(source) do + new[key] = value + end + end + + return new +end + +return join \ No newline at end of file diff --git a/BuiltInPlugins/PlayerEmulator/Src/Components/LanguageSection.lua b/BuiltInPlugins/PlayerEmulator/Src/Components/LanguageSection.lua index c63b3b7239..ca33ab392f 100644 --- a/BuiltInPlugins/PlayerEmulator/Src/Components/LanguageSection.lua +++ b/BuiltInPlugins/PlayerEmulator/Src/Components/LanguageSection.lua @@ -1,3 +1,6 @@ +--!nolint ImplicitReturn +--^ DEVTOOLS-4493 + --[[ Test language section Contains a text label for section title, a dropdown language selector, @@ -289,4 +292,4 @@ local function mapDispatchToProps(dispatch) } end -return RoactRodux.connect(mapStateToProps, mapDispatchToProps)(LanguageSection) \ No newline at end of file +return RoactRodux.connect(mapStateToProps, mapDispatchToProps)(LanguageSection) diff --git a/BuiltInPlugins/PublishPlaceAs/Packages/UILibrary/_internal/Utils/Symbol.spec.lua b/BuiltInPlugins/PublishPlaceAs/Packages/UILibrary/_internal/Utils/Symbol.spec.lua index cde9be065b..f3312055c9 100644 --- a/BuiltInPlugins/PublishPlaceAs/Packages/UILibrary/_internal/Utils/Symbol.spec.lua +++ b/BuiltInPlugins/PublishPlaceAs/Packages/UILibrary/_internal/Utils/Symbol.spec.lua @@ -11,7 +11,7 @@ return function() it("should coerce to the given name", function() local symbol = Symbol.named("foo") - expect(tostring(symbol):find("foo")).to.be.ok() + expect(tostring(symbol):match("foo")).to.be.ok() end) it("should be unique when constructed", function() @@ -42,4 +42,4 @@ return function() expect(symbolA).never.to.equal(symbolB) end) end) -end \ No newline at end of file +end diff --git a/BuiltInPlugins/TerrainToolsV2/Bin/defineLuaFlags.lua b/BuiltInPlugins/TerrainToolsV2/Bin/defineLuaFlags.lua index b9ae12b83d..1d3d2a4610 100644 --- a/BuiltInPlugins/TerrainToolsV2/Bin/defineLuaFlags.lua +++ b/BuiltInPlugins/TerrainToolsV2/Bin/defineLuaFlags.lua @@ -8,7 +8,9 @@ game:DefineFastFlag("TerrainToolsUseMapSettingsWithPreview2", false) game:DefineFastFlag("TerrainEnableErrorReporting", false) game:DefineFastFlag("TerrainToolsReplaceSrcTogglesOff", false) game:DefineFastFlag("TerrainToolsFixRegionPreviewDeactivation", false) +game:DefineFastFlag("TerrainToolsBetterImportTool", false) game:DefineFastFlag("TerrainToolsFixLargeSmoothAirFillerMaterial", false) +game:DefineFastFlag("TerrainToolsUseSiblingZIndex", false) local function handleFlagDependencies(flag, requiredFlags) if not game:GetFastFlag(flag) then @@ -22,6 +24,10 @@ local function handleFlagDependencies(flag, requiredFlags) end handleFlagDependencies("TerrainToolsUseDevFramework", {"TerrainToolsUseMapSettingsWithPreview2"}) +handleFlagDependencies("TerrainToolsBetterImportTool", { + "TerrainToolsUseDevFramework", + "TerrainImportSupportTempId", +}) -- Need to explicitly return something from a module -- Else you get an error "Module code did not return exactly one value" diff --git a/BuiltInPlugins/TerrainToolsV2/Bin/main.server.lua b/BuiltInPlugins/TerrainToolsV2/Bin/main.server.lua index 401c81e0a7..9a9ccb962b 100644 --- a/BuiltInPlugins/TerrainToolsV2/Bin/main.server.lua +++ b/BuiltInPlugins/TerrainToolsV2/Bin/main.server.lua @@ -17,10 +17,14 @@ end local FFlagTerrainToolsConvertPartTool = game:GetFastFlag("TerrainToolsConvertPartTool") local FFlagTerrainOpenCloseMetrics = game:GetFastFlag("TerrainOpenCloseMetrics") +local FFlagStudioShowHideABTestV2 = game:GetFastFlag("StudioShowHideABTestV2") +local FFlagTerrainToolsUseSiblingZIndex = game:GetFastFlag("TerrainToolsUseSiblingZIndex") -- Services +local ABTestService = game:GetService("ABTestService") local AnalyticsService = game:GetService("RbxAnalyticsService") local StudioService = game:GetService("StudioService") + -- libraries local Roact = require(Plugin.Packages.Roact) local Rodux = require(Plugin.Packages.Rodux) @@ -68,6 +72,8 @@ if FFlagTerrainOpenCloseMetrics then TOGGLE_COUNTER = "TerrainToolsToggleButton" end +local ABTEST_SHOWHIDEV2_NAME = "AllUsers.RobloxStudio.ShowHideV2" + -- Plugin Specific Globals local dataStore = Rodux.Store.new(MainReducer, nil, { getReportTerrainToolMetrics({ @@ -243,10 +249,18 @@ local function main() exampleButton:SetActive(pluginGui.Enabled) end + local initiallyEnabled = true + if FFlagStudioShowHideABTestV2 then + -- When toolbox is shown, hide other left-docked plugins + if ABTestService:GetVariant(ABTEST_SHOWHIDEV2_NAME) == "Variation2" then + initiallyEnabled = false + end + end + -- create the plugin local widgetInfo = DockWidgetPluginGuiInfo.new( Enum.InitialDockState.Left, -- Widget will be initialized docked to the left - true, -- Widget will be initially enabled + initiallyEnabled, -- Widget will be initially enabled false, -- Don't override the previous enabled state 300, -- Default width of the floating window 600, -- Default height of the floating window @@ -257,7 +271,8 @@ local function main() pluginGui.Name = localization:getText("Meta", "PluginName") pluginGui.Title = localization:getText("Main", "Title") - pluginGui.ZIndexBehavior = Enum.ZIndexBehavior.Global + pluginGui.ZIndexBehavior = FFlagTerrainToolsUseSiblingZIndex and Enum.ZIndexBehavior.Sibling + or Enum.ZIndexBehavior.Global pluginGui:GetPropertyChangedSignal("Enabled"):Connect(showIfEnabled) -- configure the widget and button if its visible diff --git a/BuiltInPlugins/TerrainToolsV2/Packages/Framework/ContextServices/Theme.spec.lua b/BuiltInPlugins/TerrainToolsV2/Packages/Framework/ContextServices/Theme.spec.lua index 22377dc00b..1547fb3c11 100644 --- a/BuiltInPlugins/TerrainToolsV2/Packages/Framework/ContextServices/Theme.spec.lua +++ b/BuiltInPlugins/TerrainToolsV2/Packages/Framework/ContextServices/Theme.spec.lua @@ -11,6 +11,13 @@ return function() local StyleTable = Util.StyleTable local StyleModifier = Util.StyleModifier local Signal = Util.Signal + local FlagsList = Util.Flags.new({ + FFlagRefactorDevFrameworkTheme = {"RefactorDevFrameworkTheme"}, + }) + + if FlagsList:get("FFlagRefactorDevFrameworkTheme") then + return + end local function createTestThemedComponent(render) local TestThemedComponent = Roact.PureComponent:extend("TestThemedComponent") diff --git a/BuiltInPlugins/TerrainToolsV2/Packages/Framework/ContextServices/UILibraryWrapper.spec.lua b/BuiltInPlugins/TerrainToolsV2/Packages/Framework/ContextServices/UILibraryWrapper.spec.lua index 0823cb2280..d4bbb673db 100644 --- a/BuiltInPlugins/TerrainToolsV2/Packages/Framework/ContextServices/UILibraryWrapper.spec.lua +++ b/BuiltInPlugins/TerrainToolsV2/Packages/Framework/ContextServices/UILibraryWrapper.spec.lua @@ -2,8 +2,10 @@ return function() local Framework = script.Parent.Parent local Util = require(Framework.Util) + local Signal = Util.Signal local FlagsList = Util.Flags.new({ FFlagStudioDevFrameworkPackage = {"StudioDevFrameworkPackage"}, + FFlagRefactorDevFrameworkTheme = {"RefactorDevFrameworkTheme"}, }) local isUsedAsPackage = require(Framework.Util.isUsedAsPackage) @@ -18,6 +20,7 @@ return function() local Focus = require(Framework.ContextServices.Focus) local Theme = require(Framework.ContextServices.Theme) local UILibraryWrapper = require(script.Parent.UILibraryWrapper) + local StudioTheme = require(Framework.Style.Themes.StudioTheme) local WrapperStub = Roact.PureComponent:extend("UILibraryWrapper") @@ -29,12 +32,26 @@ return function() Wrapper = WrapperStub } + local function createThemeMock() + + if FlagsList:get("FFlagRefactorDevFrameworkTheme") then + return StudioTheme.mock() + else + local createStyles = function() + return {} + end + local getTheme = function() + return "Light" + end + local themeChanged = Signal.new() + return Theme.mock(createStyles, getTheme, themeChanged) + end + end + it("should expect a Plugin ContextItem provided above", function() local focus = Focus.new(Instance.new("ScreenGui")) local wrapper = UILibraryWrapper.new(UILibraryStub) - local theme = Theme.new(function() - return {} - end) + local theme = createThemeMock() function theme:getUILibraryTheme() return {} end @@ -55,9 +72,7 @@ return function() it("should expect a Focus ContextItem provided above", function() local plugin = Plugin.new({}) local wrapper = UILibraryWrapper.new(UILibraryStub) - local theme = Theme.new(function() - return {} - end) + local theme = createThemeMock() function theme:getUILibraryTheme() return {} end @@ -97,9 +112,7 @@ return function() local plugin = Plugin.new({}) local focus = Focus.new(Instance.new("ScreenGui")) local wrapper = UILibraryWrapper.new(UILibraryStub) - local theme = Theme.new(function() - return {} - end) + local theme = createThemeMock() local element = provide({ plugin, @@ -119,9 +132,7 @@ return function() local plugin = Plugin.new({}) local focus = Focus.new(Instance.new("ScreenGui")) local wrapper = UILibraryWrapper.new(UILibraryStub) - local theme = Theme.new(function() - return {} - end) + local theme = createThemeMock() function theme:getUILibraryTheme() return {} end diff --git a/BuiltInPlugins/TerrainToolsV2/Packages/Framework/StudioUI/TitledFrame/test.spec.lua b/BuiltInPlugins/TerrainToolsV2/Packages/Framework/StudioUI/TitledFrame/test.spec.lua index cff37efea8..f013a391bc 100644 --- a/BuiltInPlugins/TerrainToolsV2/Packages/Framework/StudioUI/TitledFrame/test.spec.lua +++ b/BuiltInPlugins/TerrainToolsV2/Packages/Framework/StudioUI/TitledFrame/test.spec.lua @@ -17,7 +17,7 @@ return function() local function createTestTitledFrame(children, container) local theme if FlagsList:get("FFlagRefactorDevFrameworkTheme") then - theme = StudioTheme.new() + theme = StudioTheme.mock() else theme = Theme.new(function() return { diff --git a/BuiltInPlugins/TerrainToolsV2/Packages/Framework/StudioUI/createPluginWidget.lua b/BuiltInPlugins/TerrainToolsV2/Packages/Framework/StudioUI/createPluginWidget.lua index 85620982d3..b9dd6d3759 100644 --- a/BuiltInPlugins/TerrainToolsV2/Packages/Framework/StudioUI/createPluginWidget.lua +++ b/BuiltInPlugins/TerrainToolsV2/Packages/Framework/StudioUI/createPluginWidget.lua @@ -9,10 +9,12 @@ ]] game:DefineFastFlag("FixDevFrameworkDockWidgetRestore", false) -game:DefineFastFlag("DevFrameworkPluginWidgetEnabledEvent", false) +game:DefineFastFlag("DevFrameworkPluginWidgetEnabledEvent2", false) +game:DefineFastFlag("DevFrameworkPluginWidgetUseSiblingZIndex", false) local FFlagFixDevFrameworkDockWidgetRestore = game:GetFastFlag("FixDevFrameworkDockWidgetRestore") -local FFlagDevFrameworkPluginWidgetEnabledEvent = game:GetFastFlag("DevFrameworkPluginWidgetEnabledEvent") +local FFlagDevFrameworkPluginWidgetEnabledEvent2 = game:GetFastFlag("DevFrameworkPluginWidgetEnabledEvent2") +local FFlagDevFrameworkPluginWidgetUseSiblingZIndex = game:GetFastFlag("DevFrameworkPluginWidgetUseSiblingZIndex") local Framework = script.Parent.Parent local Roact = require(Framework.Parent.Roact) @@ -31,7 +33,11 @@ local function createPluginWidget(componentName, createWidgetFunc) local widget = createWidgetFunc(props) widget.Name = title or "" - widget.ZIndexBehavior = props.ZIndexBehavior or Enum.ZIndexBehavior.Global + if FFlagDevFrameworkPluginWidgetUseSiblingZIndex then + widget.ZIndexBehavior = props.ZIndexBehavior or Enum.ZIndexBehavior.Sibling + else + widget.ZIndexBehavior = props.ZIndexBehavior or Enum.ZIndexBehavior.Global + end if widget:IsA("PluginGui") then widget:BindToClose(onClose) @@ -67,12 +73,13 @@ local function createPluginWidget(componentName, createWidgetFunc) end end - if FFlagDevFrameworkPluginWidgetEnabledEvent then + if FFlagDevFrameworkPluginWidgetEnabledEvent2 then -- Connect to enabled changing *after* restore -- Otherwise users of this will get 2 enabled changes: one from the onRestore, and the same from Roact.Change.Enabled self.widgetEnabledChangedConnection = widget:GetPropertyChangedSignal("Enabled"):Connect(function() - if self.props[Roact.Change.Enabled] then - self.props[Roact.Change.Enabled](self.widget) + local callback = self.props[Roact.Change.Enabled] + if callback and self.widget and self.widget.Enabled ~= self.props.Enabled then + callback(self.widget) end end) end diff --git a/BuiltInPlugins/TerrainToolsV2/Packages/Framework/Style/Stylizer.lua b/BuiltInPlugins/TerrainToolsV2/Packages/Framework/Style/Stylizer.lua index 47a60c5663..228c4b165a 100644 --- a/BuiltInPlugins/TerrainToolsV2/Packages/Framework/Style/Stylizer.lua +++ b/BuiltInPlugins/TerrainToolsV2/Packages/Framework/Style/Stylizer.lua @@ -1,3 +1,5 @@ +--!nocheck + --[[ Wraps theme styles and update logic into a ContextItem. @@ -318,4 +320,4 @@ function Stylizer.mock(t, themeProps, callback) return self end -return Stylizer \ No newline at end of file +return Stylizer diff --git a/BuiltInPlugins/TerrainToolsV2/Packages/Framework/Style/getRawComponentStyle.lua b/BuiltInPlugins/TerrainToolsV2/Packages/Framework/Style/getRawComponentStyle.lua index 7e659ca0c0..5c76de757c 100644 --- a/BuiltInPlugins/TerrainToolsV2/Packages/Framework/Style/getRawComponentStyle.lua +++ b/BuiltInPlugins/TerrainToolsV2/Packages/Framework/Style/getRawComponentStyle.lua @@ -9,6 +9,7 @@ local UIFolderData = require(Framework.UI.UIFolderData) return function(componentName) local componentData = UIFolderData[componentName] or StudioUIFolderData[componentName] local result + if componentData.style then result = require(componentData.style) end diff --git a/BuiltInPlugins/TerrainToolsV2/Packages/Framework/TestHelpers/provideMockContext.lua b/BuiltInPlugins/TerrainToolsV2/Packages/Framework/TestHelpers/provideMockContext.lua index 04cc85664e..10633a0dc7 100644 --- a/BuiltInPlugins/TerrainToolsV2/Packages/Framework/TestHelpers/provideMockContext.lua +++ b/BuiltInPlugins/TerrainToolsV2/Packages/Framework/TestHelpers/provideMockContext.lua @@ -91,7 +91,7 @@ return function(contextItemsList, children) -- Theme local theme if FlagsList:get("FFlagRefactorDevFrameworkTheme") then - theme = StudioTheme.new() + theme = StudioTheme.mock() else theme = ContextServices.Theme.mock(function(theme, getColor) return { diff --git a/BuiltInPlugins/TerrainToolsV2/Packages/Framework/UI/Box/test.spec.lua b/BuiltInPlugins/TerrainToolsV2/Packages/Framework/UI/Box/test.spec.lua index 27baa93e01..edc3b7c984 100644 --- a/BuiltInPlugins/TerrainToolsV2/Packages/Framework/UI/Box/test.spec.lua +++ b/BuiltInPlugins/TerrainToolsV2/Packages/Framework/UI/Box/test.spec.lua @@ -23,7 +23,7 @@ return function() else local theme if FlagsList:get("FFlagRefactorDevFrameworkTheme") then - theme = StudioTheme.new() + theme = StudioTheme.mock() else theme = Theme.new(function() return { diff --git a/BuiltInPlugins/TerrainToolsV2/Packages/Framework/UI/Button/test.spec.lua b/BuiltInPlugins/TerrainToolsV2/Packages/Framework/UI/Button/test.spec.lua index 3ffc122f23..cafabfaa40 100644 --- a/BuiltInPlugins/TerrainToolsV2/Packages/Framework/UI/Button/test.spec.lua +++ b/BuiltInPlugins/TerrainToolsV2/Packages/Framework/UI/Button/test.spec.lua @@ -18,7 +18,7 @@ return function() local function createTestButton(props, children) local theme if FlagsList:get("FFlagRefactorDevFrameworkTheme") then - theme = StudioTheme.new() + theme = StudioTheme.mock() else theme = Theme.new(function() return { diff --git a/BuiltInPlugins/TerrainToolsV2/Packages/Framework/UI/Container/test.spec.lua b/BuiltInPlugins/TerrainToolsV2/Packages/Framework/UI/Container/test.spec.lua index 21e7c8874a..1417100db0 100644 --- a/BuiltInPlugins/TerrainToolsV2/Packages/Framework/UI/Container/test.spec.lua +++ b/BuiltInPlugins/TerrainToolsV2/Packages/Framework/UI/Container/test.spec.lua @@ -18,7 +18,7 @@ return function() local function createTestContainer(props, children) local theme if FlagsList:get("FFlagRefactorDevFrameworkTheme") then - theme = StudioTheme.new() + theme = StudioTheme.mock() else theme = Theme.new(function() return { diff --git a/BuiltInPlugins/TerrainToolsV2/Packages/Framework/UI/DropdownMenu.lua b/BuiltInPlugins/TerrainToolsV2/Packages/Framework/UI/DropdownMenu.lua index 60f9fadd0c..b1e6b3c757 100644 --- a/BuiltInPlugins/TerrainToolsV2/Packages/Framework/UI/DropdownMenu.lua +++ b/BuiltInPlugins/TerrainToolsV2/Packages/Framework/UI/DropdownMenu.lua @@ -44,6 +44,7 @@ local TextLabel = require(UI.TextLabel) local FlagsList = Util.Flags.new({ FFlagRefactorDevFrameworkTheme = {"RefactorDevFrameworkTheme"}, + FFlagRefactorDevFrameworkContextItems = {"RefactorDevFrameworkContextItems"}, }) local DropdownMenu = Roact.PureComponent:extend("DropdownMenu") @@ -201,7 +202,7 @@ function DropdownMenu:renderMenu() local width = style.Width local offset = prioritize(style.Offset, Vector2.new(0, 0)) - local pluginGui = props.Focus:getTarget() + local pluginGui = FlagsList:get("FFlagRefactorDevFrameworkContextItems") and props.Focus:get() or props.Focus:getTarget() local menuPositionAndSize = self.getPositionAndSize(pluginGui, width, offset) local x = menuPositionAndSize.X diff --git a/BuiltInPlugins/TerrainToolsV2/Packages/Framework/UI/FakeLoadingBar/test.spec.lua b/BuiltInPlugins/TerrainToolsV2/Packages/Framework/UI/FakeLoadingBar/test.spec.lua index 4533d1b2a4..1306596adc 100644 --- a/BuiltInPlugins/TerrainToolsV2/Packages/Framework/UI/FakeLoadingBar/test.spec.lua +++ b/BuiltInPlugins/TerrainToolsV2/Packages/Framework/UI/FakeLoadingBar/test.spec.lua @@ -17,7 +17,7 @@ return function() local function createTestFakeLoadingBar(props, children) local theme if FlagsList:get("FFlagRefactorDevFrameworkTheme") then - theme = StudioTheme.new() + theme = StudioTheme.mock() else theme = Theme.new(function() return { diff --git a/BuiltInPlugins/TerrainToolsV2/Packages/Framework/UI/Image/test.spec.lua b/BuiltInPlugins/TerrainToolsV2/Packages/Framework/UI/Image/test.spec.lua index c5aa507a3a..6860be79dd 100644 --- a/BuiltInPlugins/TerrainToolsV2/Packages/Framework/UI/Image/test.spec.lua +++ b/BuiltInPlugins/TerrainToolsV2/Packages/Framework/UI/Image/test.spec.lua @@ -17,7 +17,7 @@ return function() local function createTestImageDecoration() local theme if FlagsList:get("FFlagRefactorDevFrameworkTheme") then - theme = StudioTheme.new() + theme = StudioTheme.mock() else theme = Theme.new(function() return { diff --git a/BuiltInPlugins/TerrainToolsV2/Packages/Framework/UI/InstanceTreeView/InstanceTreeRow.lua b/BuiltInPlugins/TerrainToolsV2/Packages/Framework/UI/InstanceTreeView/InstanceTreeRow.lua index 8000e57785..6110490a54 100644 --- a/BuiltInPlugins/TerrainToolsV2/Packages/Framework/UI/InstanceTreeView/InstanceTreeRow.lua +++ b/BuiltInPlugins/TerrainToolsV2/Packages/Framework/UI/InstanceTreeView/InstanceTreeRow.lua @@ -25,7 +25,7 @@ local TextLabel = require(UI.TextLabel) Typecheck.wrap(InstanceTreeRow, script) local function getClassIcon(instance) - local StudioService = game:GetService("StudioService") + local StudioService = game:GetService("StudioService") local className = instance.ClassName if instance.IsA then if instance:IsA("JointInstance") and className == "ManualWeld" or className == "ManualGlue" then @@ -73,9 +73,10 @@ function InstanceTreeRow:render() local arrowSize = style.Arrow.Size local padding = style.IconPadding local iconInfo = getClassIcon(item) - local iconSize = iconInfo.ImageRectSize.X + -- Default iconSize to (0, 0) as ImageRectSize is unavailable in Roblox CLI + local iconSize = iconInfo.ImageRectSize or Vector2.new() local labelOffset = indent + arrowSize + 2 * padding - local textOffset = iconSize + 3 * padding + local textOffset = iconSize.X + 3 * padding return Roact.createElement(Container, { Size = UDim2.new(1, -indent, 0, style.RowHeight), @@ -105,10 +106,10 @@ function InstanceTreeRow:render() Size = UDim2.new(1, -arrowSize, 1, 0), }, { Icon = Roact.createElement("ImageLabel", { - Size = UDim2.fromOffset(iconSize, iconInfo.ImageRectSize.Y), + Size = UDim2.fromOffset(iconSize.X, iconSize.Y), BackgroundTransparency = 1, Image = iconInfo.Image, - ImageRectSize = iconInfo.ImageRectSize, + ImageRectSize = iconSize, ImageRectOffset = iconInfo.ImageRectOffset, Position = UDim2.new(0, padding, 0.5, 0), AnchorPoint = Vector2.new(0, 0.5) diff --git a/BuiltInPlugins/TerrainToolsV2/Packages/Framework/UI/LinkText/test.spec.lua b/BuiltInPlugins/TerrainToolsV2/Packages/Framework/UI/LinkText/test.spec.lua index d72d172041..37cecf30b1 100644 --- a/BuiltInPlugins/TerrainToolsV2/Packages/Framework/UI/LinkText/test.spec.lua +++ b/BuiltInPlugins/TerrainToolsV2/Packages/Framework/UI/LinkText/test.spec.lua @@ -19,7 +19,7 @@ return function() local mouse = Mouse.new({}) local theme if FlagsList:get("FFlagRefactorDevFrameworkTheme") then - theme = StudioTheme.new() + theme = StudioTheme.mock() else theme = Theme.new(function() return { diff --git a/BuiltInPlugins/TerrainToolsV2/Packages/Framework/UI/LoadingBar/test.spec.lua b/BuiltInPlugins/TerrainToolsV2/Packages/Framework/UI/LoadingBar/test.spec.lua index ea437b136c..c898cde0f4 100644 --- a/BuiltInPlugins/TerrainToolsV2/Packages/Framework/UI/LoadingBar/test.spec.lua +++ b/BuiltInPlugins/TerrainToolsV2/Packages/Framework/UI/LoadingBar/test.spec.lua @@ -18,7 +18,7 @@ return function() local function createTestLoadingBar(props, children) local theme if FlagsList:get("FFlagRefactorDevFrameworkTheme") then - theme = StudioTheme.new() + theme = StudioTheme.mock() else theme = Theme.new(function() return { diff --git a/BuiltInPlugins/TerrainToolsV2/Packages/Framework/UI/RadioButton/test.spec.lua b/BuiltInPlugins/TerrainToolsV2/Packages/Framework/UI/RadioButton/test.spec.lua index 31e2c12c77..4e578229a9 100644 --- a/BuiltInPlugins/TerrainToolsV2/Packages/Framework/UI/RadioButton/test.spec.lua +++ b/BuiltInPlugins/TerrainToolsV2/Packages/Framework/UI/RadioButton/test.spec.lua @@ -23,7 +23,7 @@ return function() local function createTestRadioButton(props, children) local theme if FlagsList:get("FFlagRefactorDevFrameworkTheme") then - theme = StudioTheme.new() + theme = StudioTheme.mock() else theme = Theme.new(function() return { diff --git a/BuiltInPlugins/TerrainToolsV2/Packages/Framework/UI/RoundBox/test.spec.lua b/BuiltInPlugins/TerrainToolsV2/Packages/Framework/UI/RoundBox/test.spec.lua index 10b16f79a0..d9715cf163 100644 --- a/BuiltInPlugins/TerrainToolsV2/Packages/Framework/UI/RoundBox/test.spec.lua +++ b/BuiltInPlugins/TerrainToolsV2/Packages/Framework/UI/RoundBox/test.spec.lua @@ -17,7 +17,7 @@ return function() local function createTestRoundBoxDecoration() local theme if FlagsList:get("FFlagRefactorDevFrameworkTheme") then - theme = StudioTheme.new() + theme = StudioTheme.mock() else theme = Theme.new(function() return { diff --git a/BuiltInPlugins/TerrainToolsV2/Packages/Framework/UI/TextLabel/test.spec.lua b/BuiltInPlugins/TerrainToolsV2/Packages/Framework/UI/TextLabel/test.spec.lua index a403d8b7d3..5f739cb55f 100644 --- a/BuiltInPlugins/TerrainToolsV2/Packages/Framework/UI/TextLabel/test.spec.lua +++ b/BuiltInPlugins/TerrainToolsV2/Packages/Framework/UI/TextLabel/test.spec.lua @@ -17,7 +17,7 @@ return function() local function createTestTextLabelDecoration() local theme if FlagsList:get("FFlagRefactorDevFrameworkTheme") then - theme = StudioTheme.new() + theme = StudioTheme.mock() else theme = Theme.new(function() return { diff --git a/BuiltInPlugins/TerrainToolsV2/Packages/Framework/UI/ToggleButton/test.spec.lua b/BuiltInPlugins/TerrainToolsV2/Packages/Framework/UI/ToggleButton/test.spec.lua index 0d712bdf45..c4db80d801 100644 --- a/BuiltInPlugins/TerrainToolsV2/Packages/Framework/UI/ToggleButton/test.spec.lua +++ b/BuiltInPlugins/TerrainToolsV2/Packages/Framework/UI/ToggleButton/test.spec.lua @@ -27,7 +27,7 @@ return function() local mouse = Mouse.new({}) local theme if FlagsList:get("FFlagRefactorDevFrameworkTheme") then - theme = StudioTheme.new() + theme = StudioTheme.mock() else theme = Theme.new(function() return { diff --git a/BuiltInPlugins/TerrainToolsV2/Packages/Framework/UI/Tooltip/test.spec.lua b/BuiltInPlugins/TerrainToolsV2/Packages/Framework/UI/Tooltip/test.spec.lua index 984329d7eb..8f7a0a6fc9 100644 --- a/BuiltInPlugins/TerrainToolsV2/Packages/Framework/UI/Tooltip/test.spec.lua +++ b/BuiltInPlugins/TerrainToolsV2/Packages/Framework/UI/Tooltip/test.spec.lua @@ -25,7 +25,7 @@ return function() local focus = Focus.new(target) local theme if FlagsList:get("FFlagRefactorDevFrameworkTheme") then - theme = StudioTheme.new() + theme = StudioTheme.mock() else theme = Theme.new(function() return { diff --git a/BuiltInPlugins/TerrainToolsV2/Packages/Framework/UI/TreeView/test.spec.lua b/BuiltInPlugins/TerrainToolsV2/Packages/Framework/UI/TreeView/test.spec.lua index 953aef3483..8d06ee8a86 100644 --- a/BuiltInPlugins/TerrainToolsV2/Packages/Framework/UI/TreeView/test.spec.lua +++ b/BuiltInPlugins/TerrainToolsV2/Packages/Framework/UI/TreeView/test.spec.lua @@ -63,7 +63,7 @@ return function() local function createTreeView() local theme if FlagsList:get("FFlagRefactorDevFrameworkTheme") then - theme = StudioTheme.new() + theme = StudioTheme.mock() else theme = Theme.new(function() return { diff --git a/BuiltInPlugins/TerrainToolsV2/Packages/Framework/Util.lua b/BuiltInPlugins/TerrainToolsV2/Packages/Framework/Util.lua index 5454c404f1..6a3e12c089 100644 --- a/BuiltInPlugins/TerrainToolsV2/Packages/Framework/Util.lua +++ b/BuiltInPlugins/TerrainToolsV2/Packages/Framework/Util.lua @@ -19,6 +19,7 @@ return strict({ FitFrame = require(script.FitFrame), Flags = require(script.Flags), + getTestVariation = require(script.getTestVariation), Immutable = require(script.Immutable), LayoutOrderIterator = require(script.LayoutOrderIterator), Math = require(script.Math), diff --git a/BuiltInPlugins/TerrainToolsV2/Packages/Framework/Util/CrossPluginCommunication.lua b/BuiltInPlugins/TerrainToolsV2/Packages/Framework/Util/CrossPluginCommunication.lua index 37caa1bece..d3b82f5783 100644 --- a/BuiltInPlugins/TerrainToolsV2/Packages/Framework/Util/CrossPluginCommunication.lua +++ b/BuiltInPlugins/TerrainToolsV2/Packages/Framework/Util/CrossPluginCommunication.lua @@ -22,10 +22,12 @@ CrossPluginCommunication.__index = CrossPluginCommunication CrossPluginCommunication.BASE_FOLDER_NAME = "CrossPluginCommunication" -function CrossPluginCommunication.new(pluginNamespace) +function CrossPluginCommunication.new(pluginNamespace, hostService) assert(t.string(pluginNamespace), "pluginNamespace must be a string") + assert(t.optional(t.instance(pluginNamespace)), "hostService must be an instance if defined") local self = { + hostService = hostService or RobloxPluginGuiService, pluginNamespace = pluginNamespace } @@ -49,7 +51,7 @@ function CrossPluginCommunication:ensureFolderExists(parent, name) end function CrossPluginCommunication:getNamespaceFolder() - local base = self:ensureFolderExists(RobloxPluginGuiService, self.BASE_FOLDER_NAME) + local base = self:ensureFolderExists(self.hostService, self.BASE_FOLDER_NAME) return self:ensureFolderExists(base, self.pluginNamespace) end diff --git a/BuiltInPlugins/TerrainToolsV2/Packages/Framework/Util/CrossPluginCommunication.spec.lua b/BuiltInPlugins/TerrainToolsV2/Packages/Framework/Util/CrossPluginCommunication.spec.lua index 60338f2b0c..f3c1035f50 100644 --- a/BuiltInPlugins/TerrainToolsV2/Packages/Framework/Util/CrossPluginCommunication.spec.lua +++ b/BuiltInPlugins/TerrainToolsV2/Packages/Framework/Util/CrossPluginCommunication.spec.lua @@ -1,5 +1,3 @@ -local RobloxPluginGuiService = game:GetService("RobloxPluginGuiService") - local Framework = script.Parent.Parent local CrossPluginCommunication = require(Framework.Util.CrossPluginCommunication) @@ -8,8 +6,10 @@ return function() -- TODO DEVTOOLS-4397: Move cleanup to afterEach hook after the dependency structure is fixed + local hostService = workspace + local function createTestComms() - return CrossPluginCommunication.new(TEST_NAMESPACE) + return CrossPluginCommunication.new(TEST_NAMESPACE, hostService) end describe("Invoke", function() @@ -32,7 +32,7 @@ return function() comms:OnInvoke("blah", function() end) - local namespace = RobloxPluginGuiService:FindFirstChild(CrossPluginCommunication.BASE_FOLDER_NAME):FindFirstChild(TEST_NAMESPACE) + local namespace = hostService:FindFirstChild(CrossPluginCommunication.BASE_FOLDER_NAME):FindFirstChild(TEST_NAMESPACE) assert(namespace) diff --git a/BuiltInPlugins/TerrainToolsV2/Packages/Framework/Util/Palette.spec.lua b/BuiltInPlugins/TerrainToolsV2/Packages/Framework/Util/Palette.spec.lua index 8794c80717..02911b17c7 100644 --- a/BuiltInPlugins/TerrainToolsV2/Packages/Framework/Util/Palette.spec.lua +++ b/BuiltInPlugins/TerrainToolsV2/Packages/Framework/Util/Palette.spec.lua @@ -1,5 +1,16 @@ return function() - local Palette = require(script.Parent.Palette) + + local Util = script.Parent + local Flags = require(Util.Flags) + local FlagsList = Flags.new({ + FFlagRefactorDevFrameworkTheme = {"RefactorDevFrameworkTheme"}, + }) + + if FlagsList:get("FFlagRefactorDevFrameworkTheme") then + return + end + + local Palette = require(Util.Palette) it("should be a table of color StyleValues", function() expect(Palette).to.be.ok() diff --git a/BuiltInPlugins/TerrainToolsV2/Packages/Framework/Util/Typecheck/t.lua b/BuiltInPlugins/TerrainToolsV2/Packages/Framework/Util/Typecheck/t.lua index 44c8d50af8..045d7ac0dd 100644 --- a/BuiltInPlugins/TerrainToolsV2/Packages/Framework/Util/Typecheck/t.lua +++ b/BuiltInPlugins/TerrainToolsV2/Packages/Framework/Util/Typecheck/t.lua @@ -1,3 +1,5 @@ +--!nocheck + --[[ DeveloperFramework uses t from Osyris https://github.com/osyrisrblx/t diff --git a/BuiltInPlugins/TerrainToolsV2/Packages/Framework/Util/getTestVariation.lua b/BuiltInPlugins/TerrainToolsV2/Packages/Framework/Util/getTestVariation.lua new file mode 100644 index 0000000000..d09873db1d --- /dev/null +++ b/BuiltInPlugins/TerrainToolsV2/Packages/Framework/Util/getTestVariation.lua @@ -0,0 +1,30 @@ +-- This function hides ABTestService away and provides a quick interface for getting a test's variation. +-- Should the requested test not exist, it will return the Control group. + +local ABTestService = game:GetService("ABTestService") + +local AB_TEST_GROUP_CONTROL = "Control" + +-- abTestName : (string) the name of test. ex) "AllUsers.Studio.ExampleTestName" +-- abTestService : (table, optional) an optional override for ABTestService +-- RETURNS : (int) the variation that the user is in. 0 = Control +return function(abTestName, abTestService) + assert(type(abTestName) == "string", "Expected abTestName to be a string") + if abTestService then + assert(type(abTestService.GetVariant) == "function", "Expected the abTestService object to have a GetVariant member function") + else + abTestService = ABTestService + end + + local variation = abTestService:GetVariant(abTestName) + if variation == AB_TEST_GROUP_CONTROL then + return 0 + else + local _, _, variationNumber = string.find(variation, "Variation(%d+)") + if not variationNumber then + return 0 + end + + return tonumber(variationNumber) + end +end diff --git a/BuiltInPlugins/TerrainToolsV2/Packages/Framework/Util/getTestVariation.spec.lua b/BuiltInPlugins/TerrainToolsV2/Packages/Framework/Util/getTestVariation.spec.lua new file mode 100644 index 0000000000..5febceee91 --- /dev/null +++ b/BuiltInPlugins/TerrainToolsV2/Packages/Framework/Util/getTestVariation.spec.lua @@ -0,0 +1,41 @@ +return function() + local getTestVariation = require(script.Parent.getTestVariation) + local VARIATION_NAME_CONTROL = "Control" + + local function createTestService(result) + local DebugABService = {} + + function DebugABService:GetVariant(testName) + return result + end + + return DebugABService + end + + it("should expect some test name", function() + expect(function() + getTestVariation() + end).to.throw() + end) + + it("should return zero when in the control group", function() + local debugABTestService = createTestService(VARIATION_NAME_CONTROL) + local variation = getTestVariation("AllUsers.Studio.ShouldShowZero", debugABTestService) + expect(variation).to.equal(0) + end) + + it("should return the number of the variation when not in the control group", function() + local debugABTestService = createTestService("Variation1") + local variation = getTestVariation("AllUsers.Studio.ShouldShowNumber", debugABTestService) + expect(variation).to.equal(1) + + -- there aren't any variations past 8, but it should work with any number + debugABTestService = createTestService("Variation23") + variation = getTestVariation("AllUsers.Studio.ShouldShowNumber", debugABTestService) + expect(variation).to.equal(23) + + debugABTestService = createTestService("Variation456") + variation = getTestVariation("AllUsers.Studio.ShouldShowNumber", debugABTestService) + expect(variation).to.equal(456) + end) +end \ No newline at end of file diff --git a/BuiltInPlugins/TerrainToolsV2/Packages/UILibrary/_internal/Utils/Symbol.spec.lua b/BuiltInPlugins/TerrainToolsV2/Packages/UILibrary/_internal/Utils/Symbol.spec.lua index cde9be065b..f3312055c9 100644 --- a/BuiltInPlugins/TerrainToolsV2/Packages/UILibrary/_internal/Utils/Symbol.spec.lua +++ b/BuiltInPlugins/TerrainToolsV2/Packages/UILibrary/_internal/Utils/Symbol.spec.lua @@ -11,7 +11,7 @@ return function() it("should coerce to the given name", function() local symbol = Symbol.named("foo") - expect(tostring(symbol):find("foo")).to.be.ok() + expect(tostring(symbol):match("foo")).to.be.ok() end) it("should be unique when constructed", function() @@ -42,4 +42,4 @@ return function() expect(symbolA).never.to.equal(symbolB) end) end) -end \ No newline at end of file +end diff --git a/BuiltInPlugins/TerrainToolsV2/Src/Actions/SelectColormap.lua b/BuiltInPlugins/TerrainToolsV2/Src/Actions/SelectColormap.lua new file mode 100644 index 0000000000..06b1dc2e99 --- /dev/null +++ b/BuiltInPlugins/TerrainToolsV2/Src/Actions/SelectColormap.lua @@ -0,0 +1,7 @@ +local Action = require(script.Parent.Action) + +return Action(script.Name, function(colormap) + return { + colormap = colormap, + } +end) diff --git a/BuiltInPlugins/TerrainToolsV2/Src/Actions/SelectHeightmap.lua b/BuiltInPlugins/TerrainToolsV2/Src/Actions/SelectHeightmap.lua new file mode 100644 index 0000000000..71967b869b --- /dev/null +++ b/BuiltInPlugins/TerrainToolsV2/Src/Actions/SelectHeightmap.lua @@ -0,0 +1,7 @@ +local Action = require(script.Parent.Action) + +return Action(script.Name, function(heightmap) + return { + heightmap = heightmap, + } +end) diff --git a/BuiltInPlugins/TerrainToolsV2/Src/Components/TerrainTools.lua b/BuiltInPlugins/TerrainToolsV2/Src/Components/TerrainTools.lua index 22e11a9eff..27560911a9 100644 --- a/BuiltInPlugins/TerrainToolsV2/Src/Components/TerrainTools.lua +++ b/BuiltInPlugins/TerrainToolsV2/Src/Components/TerrainTools.lua @@ -1,3 +1,5 @@ +local FFlagTerrainToolsUseSiblingZIndex = game:GetFastFlag("TerrainToolsUseSiblingZIndex") + local Plugin = script.Parent.Parent.Parent local Framework = require(Plugin.Packages.Framework) @@ -19,11 +21,23 @@ local TOOLBAR_NAME = "TerrainToolsLuaToolbarName" local MIN_WIDGET_SIZE = Vector2.new(270, 256) local INITIAL_WIDGET_SIZE = Vector2.new(300, 600) +local ABTEST_SHOWHIDEV2_NAME = "AllUsers.RobloxStudio.ShowHideV2" +local FFlagStudioShowHideABTestV2 = game:GetFastFlag("StudioShowHideABTestV2") + local TerrainTools = Roact.PureComponent:extend("TerrainTools") function TerrainTools:init() + local initiallyEnabled = true + + if FFlagStudioShowHideABTestV2 then + local variation = Framework.Util.getTestVariation(ABTEST_SHOWHIDEV2_NAME) + if variation == 2 then + initiallyEnabled = false + end + end + self.state = { - enabled = true, + enabled = initiallyEnabled, } self.toggleEnabled = function() @@ -147,7 +161,7 @@ function TerrainTools:render() Title = localization:get():getText("Main", "Title"), Enabled = enabled, - ZIndexBehavior = Enum.ZIndexBehavior.Global, + ZIndexBehavior = FFlagTerrainToolsUseSiblingZIndex and Enum.ZIndexBehavior.Sibling or Enum.ZIndexBehavior.Global, InitialDockState = Enum.InitialDockState.Left, Size = INITIAL_WIDGET_SIZE, MinSize = MIN_WIDGET_SIZE, diff --git a/BuiltInPlugins/TerrainToolsV2/Src/Components/ToolManager.lua b/BuiltInPlugins/TerrainToolsV2/Src/Components/ToolManager.lua index b86c665fc8..57bd8d5739 100644 --- a/BuiltInPlugins/TerrainToolsV2/Src/Components/ToolManager.lua +++ b/BuiltInPlugins/TerrainToolsV2/Src/Components/ToolManager.lua @@ -3,6 +3,7 @@ ]] local FFlagTerrainToolsConvertPartTool = game:GetFastFlag("TerrainToolsConvertPartTool") +local FFlagTerrainToolsBetterImportTool = game:GetFastFlag("TerrainToolsBetterImportTool") local Plugin = script.Parent.Parent.Parent @@ -23,7 +24,7 @@ local ToolManager = Roact.PureComponent:extend(script.Name) local tabLookup = { [TabId.Create] = { - ToolId.Generate, ToolId.Import, ToolId.Clear + ToolId.Generate, FFlagTerrainToolsBetterImportTool and ToolId.ImportLocal or ToolId.Import, ToolId.Clear }, [TabId.Region] = { ToolId.Select, ToolId.Move, ToolId.Resize, ToolId.Rotate, diff --git a/BuiltInPlugins/TerrainToolsV2/Src/Components/ToolRenderer.lua b/BuiltInPlugins/TerrainToolsV2/Src/Components/ToolRenderer.lua index 9fb3afa91c..f59dc35390 100644 --- a/BuiltInPlugins/TerrainToolsV2/Src/Components/ToolRenderer.lua +++ b/BuiltInPlugins/TerrainToolsV2/Src/Components/ToolRenderer.lua @@ -30,6 +30,7 @@ local Flatten = require(Tools.Flatten) local Generate = require(Tools.Generate) local Grow = require(Tools.Grow) local Import = require(Tools.Import) +local ImportLocal = require(Tools.ImportLocal) local Paint = require(Tools.Paint) local Region = require(Tools.Region) local SeaLevel = require(Tools.SeaLevel) @@ -60,6 +61,7 @@ local toolToScript = { local toolComponent = { [ToolId.Generate] = Generate, [ToolId.Import] = Import, + [ToolId.ImportLocal] = ImportLocal, [ToolId.ConvertPart] = ConvertPart, [ToolId.Clear] = Clear, diff --git a/BuiltInPlugins/TerrainToolsV2/Src/Components/Tools/Clear.lua b/BuiltInPlugins/TerrainToolsV2/Src/Components/Tools/Clear.lua index 1b0fb950d0..92bc05304b 100644 --- a/BuiltInPlugins/TerrainToolsV2/Src/Components/Tools/Clear.lua +++ b/BuiltInPlugins/TerrainToolsV2/Src/Components/Tools/Clear.lua @@ -3,6 +3,7 @@ ]] local FFlagTerrainToolsUseDevFramework = game:GetFastFlag("TerrainToolsUseDevFramework") +local FFlagTerrainToolsUseSiblingZIndex = game:GetFastFlag("TerrainToolsUseSiblingZIndex") local Plugin = script.Parent.Parent.Parent.Parent @@ -176,6 +177,7 @@ function Clear:render() MinSize = Vector2.new(dialogWidth, DIALOG_HEIGHT), Resizable = false, Modal = true, + ZIndexBehavior = FFlagTerrainToolsUseSiblingZIndex and Enum.ZIndexBehavior.Sibling or nil, Enabled = true, diff --git a/BuiltInPlugins/TerrainToolsV2/Src/Components/Tools/ImportLocal.lua b/BuiltInPlugins/TerrainToolsV2/Src/Components/Tools/ImportLocal.lua new file mode 100644 index 0000000000..8519812375 --- /dev/null +++ b/BuiltInPlugins/TerrainToolsV2/Src/Components/Tools/ImportLocal.lua @@ -0,0 +1,204 @@ +--[[ + Displays panels associated with the improved import tool +]] + +local Plugin = script.Parent.Parent.Parent.Parent + +local Framework = require(Plugin.Packages.Framework) +local Roact = require(Plugin.Packages.Roact) +local RoactRodux = require(Plugin.Packages.RoactRodux) + +local ContextServices = Framework.ContextServices +local ContextItems = require(Plugin.Src.ContextItems) + +local ToolParts = script.Parent.ToolParts +local ButtonGroup = require(ToolParts.ButtonGroup) +local ImportProgressFrame = require(Plugin.Src.Components.ImportProgressFrame) +local LabeledElementPair = require(ToolParts.LabeledElementPair) +local LabeledToggle = require(ToolParts.LabeledToggle) +local LocalImageSelector = require(ToolParts.LocalImageSelector) +local MapSettingsWithPreviewFragment = require(ToolParts.MapSettingsWithPreviewFragment) +local Panel = require(ToolParts.Panel) + +local Actions = Plugin.Src.Actions +local ApplyToolAction = require(Actions.ApplyToolAction) +local ChangePosition = require(Actions.ChangePosition) +local ChangeSize = require(Actions.ChangeSize) +local SelectColormap = require(Actions.SelectColormap) +local SelectHeightmap = require(Actions.SelectHeightmap) +local SetUseColorMap = require(Actions.SetUseColorMap) + +local TerrainEnums = require(Plugin.Src.Util.TerrainEnums) + +local REDUCER_KEY = "ImportLocalTool" + +local ImportLocal = Roact.PureComponent:extend(script.Name) + +function ImportLocal:init() + self.state = { + mapSettingsValid = true, + } + + self.onImportButtonClicked = function() + -- TODO MOD-46, MOD-49: Handle registering asset usage and uploading local files to get real asset ids + self.props.TerrainImporter:startImport() + end + + self.setMapSettingsValid = function(mapSettingsValid) + self:setState({ + mapSettingsValid = mapSettingsValid, + }) + end +end + +function ImportLocal:updateImportProps() + self.props.TerrainImporter:updateSettings({ + size = Vector3.new(self.props.size.X, self.props.size.Y, self.props.size.Z), + position = Vector3.new(self.props.position.X, self.props.position.Y, self.props.position.Z), + + heightMapUrl = self.props.heightmap and self.props.heightmap:GetTemporaryId() or "", + useColorMap = self.props.useColorMap, + colorMapUrl = self.props.colormap and self.props.colormap:GetTemporaryId() or "", + }) +end + +function ImportLocal:didMount() + self:updateImportProps() +end + +function ImportLocal:didUpdate() + self:updateImportProps() +end + +function ImportLocal:render() + local localization = self.props.Localization:get() + + local importInProgress = self.props.TerrainImporter:isImporting() + local importProgress = importInProgress and self.props.TerrainImporter:getImportProgress() or 0 + + local canImport = not importInProgress + and self.state.mapSettingsValid + and self.props.heightmap + and (not self.props.useColorMap or self.props.colormap) + + return Roact.createFragment({ + MapSettings = Roact.createElement(Panel, { + LayoutOrder = 1, + Title = localization:getText("MapSettings", "MapSettings"), + Padding = UDim.new(0, 12), + }, { + Heightmap = Roact.createElement(LabeledElementPair, { + Text = localization:getText("Import", "Heightmap"), + Size = UDim2.new(1, 0, 0, 60), + LayoutOrder = 1, + SizeToContent = true, + }, { + LocalImageSelector = Roact.createElement(LocalImageSelector, { + CurrentFile = self.props.heightmap, + SelectFile = self.props.dispatchSelectHeightmap, + PreviewTitle = localization:getText("Import", "HeightmapPreview"), + }), + }), + + MapSettingsWithPreview = Roact.createElement(MapSettingsWithPreviewFragment, { + toolName = self.props.toolName, + InitialLayoutOrder = 2, + + Position = self.props.position, + Size = self.props.size, + PreviewOffset = Vector3.new(0, 0.5, 0), + + OnPositionChanged = self.props.dispatchChangePosition, + OnSizeChanged = self.props.dispatchChangeSize, + SetMapSettingsValid = self.setMapSettingsValid, + }), + }), + + MaterialSettings = Roact.createElement(Panel, { + Title = localization:getText("MaterialSettings", "MaterialSettings"), + LayoutOrder = 2, + }, { + UseColorMapToggle = Roact.createElement(LabeledToggle, { + LayoutOrder = 1, + Text = localization:getText("Import", "UseColormap"), + IsOn = self.props.useColorMap, + SetIsOn = self.props.dispatchSetUseColorMap, + }), + + -- TODO: Should we hide this, or render it as disabled when not using colour maps? + Colormap = self.props.useColorMap and Roact.createElement(LabeledElementPair, { + -- Use empty text so this looks like it's part of the toggle above + -- When it's actually a separate row + Text = "", + Size = UDim2.new(1, 0, 0, 60), + LayoutOrder = 2, + SizeToContent = true, + }, { + LocalImageSelector = Roact.createElement(LocalImageSelector, { + CurrentFile = self.props.colormap, + SelectFile = self.props.dispatchSelectColormap, + PreviewTitle = localization:getText("Import", "ColormapPreview"), + }), + }), + }), + + ImportButtonFrame = Roact.createElement(ButtonGroup, { + LayoutOrder = 4, + Buttons = { + { + Key = "Import", + Name = localization:getText("ToolName", "Import"), + Active = canImport, + OnClicked = self.onImportButtonClicked, + } + } + }), + + ImportProgressFrame = importInProgress and Roact.createElement(ImportProgressFrame, { + ImportProgress = importProgress, + }), + }) +end + +ContextServices.mapToProps(ImportLocal, { + Localization = ContextItems.UILibraryLocalization, + TerrainImporter = ContextItems.TerrainImporter, +}) + +local function mapStateToProps(state, props) + return { + toolName = TerrainEnums.ToolId.ImportLocal, + position = state[REDUCER_KEY].position, + size = state[REDUCER_KEY].size, + + useColorMap = state[REDUCER_KEY].useColorMap, + heightmap = state[REDUCER_KEY].heightmap, + colormap = state[REDUCER_KEY].colormap, + } +end + +local function mapDispatchToProps(dispatch) + local dispatchToImportLocal = function(action) + dispatch(ApplyToolAction(REDUCER_KEY, action)) + end + + return { + dispatchChangePosition = function(position) + dispatchToImportLocal(ChangePosition(position)) + end, + dispatchChangeSize = function(size) + dispatchToImportLocal(ChangeSize(size)) + end, + dispatchSelectHeightmap = function(heightmap) + dispatchToImportLocal(SelectHeightmap(heightmap)) + end, + dispatchSelectColormap = function(colormap) + dispatchToImportLocal(SelectColormap(colormap)) + end, + dispatchSetUseColorMap = function(useColorMap) + dispatchToImportLocal(SetUseColorMap(useColorMap)) + end + } +end + +return RoactRodux.connect(mapStateToProps, mapDispatchToProps)(ImportLocal) diff --git a/BuiltInPlugins/TerrainToolsV2/Src/Components/Tools/ImportLocal.spec.lua b/BuiltInPlugins/TerrainToolsV2/Src/Components/Tools/ImportLocal.spec.lua new file mode 100644 index 0000000000..2c2bf4fece --- /dev/null +++ b/BuiltInPlugins/TerrainToolsV2/Src/Components/Tools/ImportLocal.spec.lua @@ -0,0 +1,19 @@ +if not game:GetFastFlag("TerrainToolsUseDevFramework") then + return function() end +end + +local Plugin = script.Parent.Parent.Parent.Parent + +local Roact = require(Plugin.Packages.Roact) + +local MockProvider = require(Plugin.Src.TestHelpers.MockProvider) + +local ImportLocal = require(script.Parent.ImportLocal) + +return function() + it("should create and destroy without errors", function() + local element = MockProvider.createElementWithMockContext(ImportLocal) + local instance = Roact.mount(element) + Roact.unmount(instance) + end) +end diff --git a/BuiltInPlugins/TerrainToolsV2/Src/Components/Tools/ToolParts/LocalImageSelector.lua b/BuiltInPlugins/TerrainToolsV2/Src/Components/Tools/ToolParts/LocalImageSelector.lua new file mode 100644 index 0000000000..c6db5f1e7a --- /dev/null +++ b/BuiltInPlugins/TerrainToolsV2/Src/Components/Tools/ToolParts/LocalImageSelector.lua @@ -0,0 +1,89 @@ +--[[ + Wraps a PromptSelectorWithPreview to give the user a way to select a local image and see a preview. + + Props: + CurrentFile : File? + The currently selected file we should render a preview of. + Can be nil to mean no file is selected. + SelectFile : File? -> void + Callback to select a new file. Can be passed nil to mean clear selection. + PreviewTitle : string + Title to use on the expanded preview window +]] + +local Plugin = script.Parent.Parent.Parent.Parent.Parent + +local Framework = require(Plugin.Packages.Framework) +local Roact = require(Plugin.Packages.Roact) + +local ContextServices = Framework.ContextServices +local ContextItems = require(Plugin.Src.ContextItems) + +local ToolParts = script.Parent +local PromptSelectorWithPreview = require(ToolParts.PromptSelectorWithPreview) + +local StudioService = game:GetService("StudioService") + +local LocalImageSelector = Roact.PureComponent:extend(script.Name) + +function LocalImageSelector:init() + self.promptSelection = function() + local file + local success, err = pcall(function() + -- TODO MOD-110: other image formats, 16 bit etc. channels, TIFF support + file = StudioService:PromptImportFile({"png"}) + end) + if success then + if file then + self.props.SelectFile(file) + else + warn("Failed to select image: prompt was successful but file is nil") + end + else + warn(("Failed to select image: %s"):format(tostring(err))) + end + end + + self.clearSelection = function() + self.props.SelectFile(nil) + end + + self.renderPreview = function() + local imageId = "" + if self.props.CurrentFile then + imageId = self.props.CurrentFile:GetTemporaryId() + end + return Roact.createElement("ImageLabel", { + BackgroundTransparency = 1, + Size = UDim2.new(1, 0, 1, 0), + Image = imageId, + ScaleType = Enum.ScaleType.Fit, + }) + end +end + +function LocalImageSelector:render() + local filename + if self.props.CurrentFile then + filename = self.props.CurrentFile.Name + else + filename = self.props.Localization:get():getText("LocalImageSelector", "NoImageSelected") + end + + return Roact.createElement(PromptSelectorWithPreview, { + SelectionName = filename, + HasSelection = self.props.CurrentFile ~= nil, + + RenderPreview = self.renderPreview, + PreviewTitle = self.props.PreviewTitle, + + PromptSelection = self.promptSelection, + ClearSelection = self.clearSelection, + }) +end + +ContextServices.mapToProps(LocalImageSelector, { + Localization = ContextItems.UILibraryLocalization, +}) + +return LocalImageSelector diff --git a/BuiltInPlugins/TerrainToolsV2/Src/Components/Tools/ToolParts/LocalImageSelector.spec.lua b/BuiltInPlugins/TerrainToolsV2/Src/Components/Tools/ToolParts/LocalImageSelector.spec.lua new file mode 100644 index 0000000000..b06c4a9ba1 --- /dev/null +++ b/BuiltInPlugins/TerrainToolsV2/Src/Components/Tools/ToolParts/LocalImageSelector.spec.lua @@ -0,0 +1,19 @@ +if not game:GetFastFlag("TerrainToolsUseDevFramework") then + return function() end +end + +local Plugin = script.Parent.Parent.Parent.Parent.Parent + +local Roact = require(Plugin.Packages.Roact) + +local MockProvider = require(Plugin.Src.TestHelpers.MockProvider) + +local LocalImageSelector = require(script.Parent.LocalImageSelector) + +return function() + it("should create and destroy without errors", function() + local element = MockProvider.createElementWithMockContext(LocalImageSelector) + local instance = Roact.mount(element) + Roact.unmount(instance) + end) +end diff --git a/BuiltInPlugins/TerrainToolsV2/Src/Components/Tools/ToolParts/MapSettingsFragment.lua b/BuiltInPlugins/TerrainToolsV2/Src/Components/Tools/ToolParts/MapSettingsFragment.lua index f32afa5f10..79c6819e3c 100644 --- a/BuiltInPlugins/TerrainToolsV2/Src/Components/Tools/ToolParts/MapSettingsFragment.lua +++ b/BuiltInPlugins/TerrainToolsV2/Src/Components/Tools/ToolParts/MapSettingsFragment.lua @@ -13,6 +13,8 @@ Props: SetMapSettingsValid : (bool) -> void ]] +local FFlagTerrainToolsBetterImportTool = game:GetFastFlag("TerrainToolsBetterImportTool") + local Plugin = script.Parent.Parent.Parent.Parent.Parent local Framework = require(Plugin.Packages.Framework) @@ -86,6 +88,9 @@ function MapSettingsFragment:init(props) self.onVectorValueChanged = function(vector, axis, text, isValid) self.validFieldState[vector][axis] = isValid + if FFlagTerrainToolsBetterImportTool then + verifyFields() + end dispatchVectorChanged(vector, axis, text, isValid) end end diff --git a/BuiltInPlugins/TerrainToolsV2/Src/Components/Tools/ToolParts/MaterialSelector.lua b/BuiltInPlugins/TerrainToolsV2/Src/Components/Tools/ToolParts/MaterialSelector.lua index d895ae4de2..294b35e1b6 100644 --- a/BuiltInPlugins/TerrainToolsV2/Src/Components/Tools/ToolParts/MaterialSelector.lua +++ b/BuiltInPlugins/TerrainToolsV2/Src/Components/Tools/ToolParts/MaterialSelector.lua @@ -9,6 +9,7 @@ Props: ]] local FFlagTerrainToolsUseDevFramework = game:GetFastFlag("TerrainToolsUseDevFramework") +local FFlagTerrainToolsUseSiblingZIndex = game:GetFastFlag("TerrainToolsUseSiblingZIndex") local Plugin = script.Parent.Parent.Parent.Parent.Parent @@ -93,7 +94,7 @@ do TextSize = theme.textSize, TextColor3 = theme.textColor, Font = theme.textFont, - ZIndex = 5, + ZIndex = FFlagTerrainToolsUseSiblingZIndex and 1 or 5, [Roact.Ref] = self.ref, }) @@ -150,6 +151,7 @@ do BackgroundColor3 = theme.backgroundColor, BorderSizePixel = isSelected and 2 or 0, BorderColor3 = isSelected and theme.selectionBorderColor or theme.borderColor, + ZIndex = FFlagTerrainToolsUseSiblingZIndex and (isHovered and 2 or 1) or 1, [Roact.Event.MouseEnter] = self.onMouseEnter, [Roact.Event.MouseLeave] = self.onMouseLeave, diff --git a/BuiltInPlugins/TerrainToolsV2/Src/Components/Tools/ToolParts/PromptSelectorWithPreview.lua b/BuiltInPlugins/TerrainToolsV2/Src/Components/Tools/ToolParts/PromptSelectorWithPreview.lua new file mode 100644 index 0000000000..df118fc035 --- /dev/null +++ b/BuiltInPlugins/TerrainToolsV2/Src/Components/Tools/ToolParts/PromptSelectorWithPreview.lua @@ -0,0 +1,328 @@ +--[[ + Generic component for "user selects something that can be previewed". + Implement PromptSelection and RenderPreview to define how the user selects an item, and how this component should + display that item. + + Props + SelectionName : string + Text to display below the preview + HasSelection : bool + Whether something is currently selected + PreviewTitle : string + Title to use on the expanded preview window + + RenderPreview : void -> Roact element + Function to render a preview of the current item + PromptSelection : void -> void + Callback to prompt the user to select an item (e.g. with StudioService:PromptImportFile()) + ClearSelection : void -> void + Callback to clear the current selection +]] + +local Plugin = script.Parent.Parent.Parent.Parent.Parent + +local Framework = require(Plugin.Packages.Framework) +local Roact = require(Plugin.Packages.Roact) +local Cryo = require(Plugin.Packages.Cryo) + +local ContextServices = Framework.ContextServices +local ContextItems = require(Plugin.Src.ContextItems) + +local Dialog = Framework.StudioUI.Dialog + +local PREVIEW_SIZE = 88 +local VERT_PADDING = 4 +local TEXT_HEIGHT = 16 + +local TOOLBAR_HEIGHT = 32 +local TOOLBAR_BUTTON_SIZE = 28 +local TOOLBAR_ICON_SIZE = 18 + +local IMPORT_ICON_SIZE = 24 + +-- TODO: Get sizes from design +local EXPANDED_PREVIEW_DEFAULT_SIZE = Vector2.new(200, 200) +local EXPANDED_PREVIEW_MIN_SIZE = Vector2.new(100, 100) +local EXPANDED_PREVIEW_PADDING = UDim.new(0, 16) + +-- Button used in the toolbar shown on hover in PromptSelectorWithPreview +local PreviewToolbarButton = Roact.PureComponent:extend("PreviewToolbarButton") + +function PreviewToolbarButton:init() + self.state = { + isHovered = false, + } + + self.onHovered = function() + self:setState({ + isHovered = true, + }) + end + + self.onHoverEnded = function() + self:setState({ + isHovered = false, + }) + end +end + +function PreviewToolbarButton:render() + local theme = self.props.Theme:get() + local promptSelectorWithPreviewTheme = theme.promptSelectorWithPreviewTheme + + local newProps = Cryo.Dictionary.join(self.props, { + Size = UDim2.new(1, 0, 1, 0), + SizeConstraint = Enum.SizeConstraint.RelativeYY, + ZIndex = 5, + + BackgroundTransparency = 1, + BorderSizePixel = 0, + + [Roact.Event.MouseEnter] = self.onHovered, + [Roact.Event.MouseLeave] = self.onHoverEnded, + + Image = "", + Icon = Cryo.None, + Theme = Cryo.None, + }) + + return Roact.createElement("ImageButton", newProps, { + Background = Roact.createElement("Frame", { + AnchorPoint = Vector2.new(0.5, 0.5), + Position = UDim2.new(0.5, 0, 0.5, 0), + Size = UDim2.new(0, TOOLBAR_BUTTON_SIZE, 0, TOOLBAR_BUTTON_SIZE), + + BackgroundTransparency = self.state.isHovered and 0 or 1, + BorderSizePixel = 0, + BackgroundColor3 = promptSelectorWithPreviewTheme.toolbarButtonBackgroundColor, + }, { + Icon = Roact.createElement("ImageLabel", { + AnchorPoint = Vector2.new(0.5, 0.5), + Position = UDim2.new(0.5, 0, 0.5, 0), + Size = UDim2.new(0, TOOLBAR_ICON_SIZE, 0, TOOLBAR_ICON_SIZE), + ZIndex = 6, + + BackgroundTransparency = 1, + BorderSizePixel = 0, + + Image = self.props.Icon, + ImageColor3 = self.state.isHovered + and promptSelectorWithPreviewTheme.buttonIconHoveredColor + or promptSelectorWithPreviewTheme.buttonIconColor, + }), + }), + }) +end + +ContextServices.mapToProps(PreviewToolbarButton, { + Theme = ContextItems.UILibraryTheme, +}) + +local PromptSelectorWithPreview = Roact.PureComponent:extend(script.Name) + +function PromptSelectorWithPreview:init() + self.state = { + promptSelectionHovered = false, + showingExpandedPreview = false, + } + + self.onPromptSelectionHover = function() + self:setState({ + promptSelectionHovered = true, + }) + end + + self.onPromptSelectionHoverEnd = function() + self:setState({ + promptSelectionHovered = false, + }) + end + + self.openExpandedPreview = function() + self:setState({ + showingExpandedPreview = true, + }) + end + + self.closeExpandedPreview = function() + self:setState({ + showingExpandedPreview = false, + }) + end +end + +function PromptSelectorWithPreview:render() + local theme = self.props.Theme:get() + local promptSelectorWithPreviewTheme = theme.promptSelectorWithPreviewTheme + + local selectionName = self.props.SelectionName or "" + local previewTitle = self.props.PreviewTitle or "" + + local width = PREVIEW_SIZE + local height = PREVIEW_SIZE + VERT_PADDING + TEXT_HEIGHT + + local hasSelection = self.props.HasSelection + local promptSelectionHovered = self.state.promptSelectionHovered + local showingExpandedPreview = self.state.showingExpandedPreview + + local previewRenderResult + if hasSelection and self.props.RenderPreview then + previewRenderResult = self.props.RenderPreview() + end + + local expandedPreviewRenderResult + if hasSelection and showingExpandedPreview and self.props.RenderPreview then + expandedPreviewRenderResult = self.props.RenderPreview() + end + + local shouldShowToolbar = (hasSelection and promptSelectionHovered) and true or false + + local content = { + UIListLayout = Roact.createElement("UIListLayout", { + SortOrder = Enum.SortOrder.LayoutOrder, + Padding = UDim.new(0, VERT_PADDING), + }), + + PreviewRow = Roact.createElement("Frame", { + LayoutOrder = 1, + Size = UDim2.new(0, PREVIEW_SIZE, 0, PREVIEW_SIZE), + + BackgroundTransparency = 1, + BorderSizePixel = 0, + }, { + -- Import new asset button goes underneath the preview+toolbar + -- If we have a preview, block clicking through to this button + -- To import something new, require users to click "clear" first + + ImportButton = Roact.createElement("ImageButton", { + ZIndex = 1, + Size = UDim2.new(1, 0, 1, 0), + + BackgroundTransparency = 0, + BorderSizePixel = 1, + BackgroundColor3 = promptSelectionHovered + and promptSelectorWithPreviewTheme.previewHoveredBackgroundColor + or promptSelectorWithPreviewTheme.previewBackgroundColor, + BorderColor3 = promptSelectorWithPreviewTheme.previewBorderColor, + Image = "", + AutoButtonColor = false, + + [Roact.Event.Activated] = self.props.PromptSelection, + [Roact.Event.MouseEnter] = self.onPromptSelectionHover, + [Roact.Event.MouseLeave] = self.onPromptSelectionHoverEnd, + }, { + Icon = Roact.createElement("ImageLabel", { + AnchorPoint = Vector2.new(0.5, 0.5), + Position = UDim2.new(0.5, 0, 0.5, 0), + Size = UDim2.new(0, IMPORT_ICON_SIZE, 0, IMPORT_ICON_SIZE), + + BackgroundTransparency = 1, + Image = promptSelectorWithPreviewTheme.importIcon, + ImageColor3 = promptSelectionHovered + and promptSelectorWithPreviewTheme.buttonIconHoveredColor + or promptSelectorWithPreviewTheme.buttonIconColor, + }) + }), + + ClickBlocker = hasSelection and Roact.createElement("ImageButton", { + ZIndex = 2, + Size = UDim2.new(1, 0, 1, 0), + + Image = "", + AutoButtonColor = false, + + BackgroundTransparency = 1, + BorderSizePixel = 0, + }, { + PreviewContentContainer = Roact.createElement("Frame", { + ZIndex = 3, + Size = UDim2.new(1, 0, 1, 0), + BackgroundTransparency = 1, + BorderSizePixel = 0, + }, { + PreviewContent = previewRenderResult, + }), + + Toolbar = Roact.createElement("Frame", { + ZIndex = 4, + + AnchorPoint = Vector2.new(0, 1), + Position = UDim2.new(0, 0, 1, 0), + Size = UDim2.new(1, 0, 0, TOOLBAR_HEIGHT), + Visible = shouldShowToolbar, + + BackgroundTransparency = promptSelectorWithPreviewTheme.toolbarTransparency, + BorderSizePixel = 0, + BackgroundColor3 = promptSelectorWithPreviewTheme.toolbarBackgroundColor, + }, { + ExpandPreview = Roact.createElement(PreviewToolbarButton, { + Icon = promptSelectorWithPreviewTheme.expandIcon, + [Roact.Event.Activated] = self.openExpandedPreview, + }), + + ClearButton = Roact.createElement(PreviewToolbarButton, { + AnchorPoint = Vector2.new(1, 0), + Position = UDim2.new(1, 0, 0, 0), + Icon = promptSelectorWithPreviewTheme.clearIcon, + [Roact.Event.Activated] = self.props.ClearSelection, + }), + }), + }), + }), + + SelectionName = Roact.createElement("TextLabel", { + LayoutOrder = 2, + BackgroundTransparency = 1, + Text = selectionName, + Size = UDim2.new(1, 0, 0, TEXT_HEIGHT), + TextXAlignment = Enum.TextXAlignment.Left, + TextColor3 = theme.textColor, + }), + + ExpandedPreview = showingExpandedPreview and Roact.createElement(Dialog, { + Title = previewTitle, + + Size = EXPANDED_PREVIEW_DEFAULT_SIZE, + MinSize = EXPANDED_PREVIEW_MIN_SIZE, + Resizable = true, + Enabled = true, + Modal = false, -- TODO: Should this be modal? + ZIndexBehavior = Enum.ZIndexBehavior.Sibling, + + OnClose = self.closeExpandedPreview, + }, { + Background = Roact.createElement("Frame", { + Size = UDim2.new(1, 0, 1, 0), + BackgroundColor3 = theme.backgroundColor, + }, { + UIPadding = Roact.createElement("UIPadding", { + PaddingTop = EXPANDED_PREVIEW_PADDING, + PaddingBottom = EXPANDED_PREVIEW_PADDING, + PaddingLeft = EXPANDED_PREVIEW_PADDING, + PaddingRight = EXPANDED_PREVIEW_PADDING, + }), + + PreviewContentContainer = Roact.createElement("Frame", { + Size = UDim2.new(1, 0, 1, 0), + BackgroundColor3 = promptSelectorWithPreviewTheme.previewBackgroundColor, + BorderColor3 = promptSelectorWithPreviewTheme.previewBorderColor, + }, { + PreviewContent = expandedPreviewRenderResult, + }), + + -- TODO MOD-180: Add metadata + }), + }), + } + + return Roact.createElement("Frame", { + BackgroundTransparency = 1, + Size = UDim2.new(0, width, 0, height), + }, content) +end + +ContextServices.mapToProps(PromptSelectorWithPreview, { + Theme = ContextItems.UILibraryTheme, +}) + +return PromptSelectorWithPreview diff --git a/BuiltInPlugins/TerrainToolsV2/Src/Components/Tools/ToolParts/PromptSelectorWithPreview.spec.lua b/BuiltInPlugins/TerrainToolsV2/Src/Components/Tools/ToolParts/PromptSelectorWithPreview.spec.lua new file mode 100644 index 0000000000..497894de0e --- /dev/null +++ b/BuiltInPlugins/TerrainToolsV2/Src/Components/Tools/ToolParts/PromptSelectorWithPreview.spec.lua @@ -0,0 +1,19 @@ +if not game:GetFastFlag("TerrainToolsUseDevFramework") then + return function() end +end + +local Plugin = script.Parent.Parent.Parent.Parent.Parent + +local Roact = require(Plugin.Packages.Roact) + +local MockProvider = require(Plugin.Src.TestHelpers.MockProvider) + +local PromptSelectorWithPreview = require(script.Parent.PromptSelectorWithPreview) + +return function() + it("should create and destroy without errors", function() + local element = MockProvider.createElementWithMockContext(PromptSelectorWithPreview) + local instance = Roact.mount(element) + Roact.unmount(instance) + end) +end diff --git a/BuiltInPlugins/TerrainToolsV2/Src/Components/Tools/ToolParts/SingleSelectButtonGroup.lua b/BuiltInPlugins/TerrainToolsV2/Src/Components/Tools/ToolParts/SingleSelectButtonGroup.lua index 561a7cdcae..93bdf6b26f 100644 --- a/BuiltInPlugins/TerrainToolsV2/Src/Components/Tools/ToolParts/SingleSelectButtonGroup.lua +++ b/BuiltInPlugins/TerrainToolsV2/Src/Components/Tools/ToolParts/SingleSelectButtonGroup.lua @@ -1,4 +1,5 @@ local FFlagTerrainToolsUseDevFramework = game:GetFastFlag("TerrainToolsUseDevFramework") +local FFlagTerrainToolsUseSiblingZIndex = game:GetFastFlag("TerrainToolsUseSiblingZIndex") local Plugin = script.Parent.Parent.Parent.Parent.Parent @@ -61,7 +62,7 @@ local function SingleSelectButtonGroup_render(props, theme) ImageColor3 = selectedColour, ScaleType = Enum.ScaleType.Slice, SliceCenter = theme.singleSelectButtonGroupTheme.roundedElementSlice, - ZIndex = 2, + ZIndex = FFlagTerrainToolsUseSiblingZIndex and 1 or 2, }), -- Render a round rectangle on the right side if this is the rightmost button @@ -78,7 +79,7 @@ local function SingleSelectButtonGroup_render(props, theme) ImageColor3 = selectedColour, ScaleType = Enum.ScaleType.Slice, SliceCenter = theme.singleSelectButtonGroupTheme.roundedElementSlice, - ZIndex = 2, + ZIndex = FFlagTerrainToolsUseSiblingZIndex and 1 or 2, }), -- Render a rectangle in the middle that either covers the whole button @@ -89,7 +90,7 @@ local function SingleSelectButtonGroup_render(props, theme) Size = (isLeftmost and isRightmost) and UDim2.new(1, -INSET_FOR_CURVED_CORNERS * 2, 1, 0) or (isLeftmost or isRightmost) and UDim2.new(1, -INSET_FOR_CURVED_CORNERS, 1, 0) or UDim2.new(1, 0, 1, 0), - ZIndex = 3, + ZIndex = FFlagTerrainToolsUseSiblingZIndex and 1 or 3, BackgroundColor3 = selectedColour, BorderSizePixel = 0, @@ -101,7 +102,7 @@ local function SingleSelectButtonGroup_render(props, theme) BorderSizePixel = 0, Text = option.Text, TextColor3 = theme.textColor, - ZIndex = 4, + ZIndex = FFlagTerrainToolsUseSiblingZIndex and 3 or 4, }), }) end @@ -113,20 +114,39 @@ local function SingleSelectButtonGroup_render(props, theme) Size = UDim2.new(0, 1, 1, 0), BorderSizePixel = 0, BackgroundColor3 = theme.borderColor, - ZIndex = 10, + ZIndex = FFlagTerrainToolsUseSiblingZIndex and 2 or 10, }) end - return Roact.createElement("ImageLabel", { - Size = size, - BackgroundTransparency = 1, - Image = theme.singleSelectButtonGroupTheme.roundedBorderImage, - ImageTransparency = 0, - ImageColor3 = theme.borderColor, - ScaleType = Enum.ScaleType.Slice, - SliceCenter = theme.singleSelectButtonGroupTheme.roundedElementSlice, - ZIndex = 10, - }, content) + if FFlagTerrainToolsUseSiblingZIndex then + content.Border = Roact.createElement("ImageLabel", { + Size = UDim2.new(1, 0, 1, 0), + BackgroundTransparency = 1, + Image = theme.singleSelectButtonGroupTheme.roundedBorderImage, + ImageTransparency = 0, + ImageColor3 = theme.borderColor, + ScaleType = Enum.ScaleType.Slice, + SliceCenter = theme.singleSelectButtonGroupTheme.roundedElementSlice, + ZIndex = 3, + }) + + return Roact.createElement("Frame", { + Size = size, + BackgroundTransparency = 1, + }, content) + + else + return Roact.createElement("ImageLabel", { + Size = size, + BackgroundTransparency = 1, + Image = theme.singleSelectButtonGroupTheme.roundedBorderImage, + ImageTransparency = 0, + ImageColor3 = theme.borderColor, + ScaleType = Enum.ScaleType.Slice, + SliceCenter = theme.singleSelectButtonGroupTheme.roundedElementSlice, + ZIndex = 10, + }, content) + end end if FFlagTerrainToolsUseDevFramework then diff --git a/BuiltInPlugins/TerrainToolsV2/Src/Reducers/ImportLocalTool.lua b/BuiltInPlugins/TerrainToolsV2/Src/Reducers/ImportLocalTool.lua new file mode 100644 index 0000000000..635f381156 --- /dev/null +++ b/BuiltInPlugins/TerrainToolsV2/Src/Reducers/ImportLocalTool.lua @@ -0,0 +1,59 @@ +local Plugin = script.Parent.Parent.Parent +local Rodux = require(Plugin.Packages.Rodux) +local Cryo = require(Plugin.Packages.Cryo) + +local ImportLocalTool = Rodux.createReducer({ + position = { + X = 0, + Y = 0, + Z = 0, + }, + + size = { + X = 1024, + Y = 512, + Z = 1024, + }, + + heightmap = nil, + colormap = nil, + useColorMap = false, +}, { + ChangePosition = function(state, action) + local position = action.position + + return Cryo.Dictionary.join(state, { + position = position, + }) + end, + + ChangeSize = function(state, action) + local size = action.size + + return Cryo.Dictionary.join(state, { + size = size, + }) + end, + + SetUseColorMap = function(state, action) + return Cryo.Dictionary.join(state, { + useColorMap = action.useColorMap + }) + end, + + SelectHeightmap = function(state, action) + return Cryo.Dictionary.join(state, { + -- Ensure that selecting nil (i.e. clearing the selection) actually clears it from state + heightmap = action.heightmap or Cryo.None, + }) + end, + + SelectColormap = function(state, action) + return Cryo.Dictionary.join(state, { + -- Ensure that selecting nil (i.e. clearing the selection) actually clears it from state + colormap = action.colormap or Cryo.None, + }) + end, +}) + +return ImportLocalTool diff --git a/BuiltInPlugins/TerrainToolsV2/Src/Reducers/ImportLocalTool.spec.lua b/BuiltInPlugins/TerrainToolsV2/Src/Reducers/ImportLocalTool.spec.lua new file mode 100644 index 0000000000..4dfef364b8 --- /dev/null +++ b/BuiltInPlugins/TerrainToolsV2/Src/Reducers/ImportLocalTool.spec.lua @@ -0,0 +1,153 @@ +local Plugin = script.Parent.Parent.Parent + +local Framework = require(Plugin.Packages.Framework) +local Rodux = require(Plugin.Packages.Rodux) + +local testImmutability = Framework.TestHelpers.testImmutability + +local ImportLocalTool = require(script.Parent.ImportLocalTool) + +local Actions = Plugin.Src.Actions +local ChangePosition = require(Actions.ChangePosition) +local ChangeSize = require(Actions.ChangeSize) +local SelectColormap = require(Actions.SelectColormap) +local SelectHeightmap = require(Actions.SelectHeightmap) +local SetUseColorMap = require(Actions.SetUseColorMap) + +return function() + it("should return its expected default state", function() + local r = Rodux.Store.new(ImportLocalTool) + expect(r:getState()).to.be.ok() + + expect(r:getState().position).to.be.ok() + expect(r:getState().position.X).to.equal(0) + expect(r:getState().position.Y).to.equal(0) + expect(r:getState().position.Z).to.equal(0) + + expect(r:getState().size).to.be.ok() + expect(r:getState().size.X).to.equal(1024) + expect(r:getState().size.Y).to.equal(512) + expect(r:getState().size.Z).to.equal(1024) + + expect(r:getState().useColorMap).to.equal(false) + expect(r:getState().heightmap).to.equal(nil) + expect(r:getState().colormap).to.equal(nil) + end) + + describe("ChangePosition", function() + it("should set position", function() + local state = ImportLocalTool(nil, ChangePosition({ + X = 321, + Y = 654, + Z = 987, + })) + + expect(state).to.be.ok() + expect(state.position).to.be.ok() + expect(state.position.X).to.equal(321) + expect(state.position.Y).to.equal(654) + expect(state.position.Z).to.equal(987) + end) + + it("should preserve immutability", function() + local immutabilityPreserved = testImmutability(ImportLocalTool, ChangePosition({ + X = 321, + Y = 654, + Z = 987, + })) + expect(immutabilityPreserved).to.equal(true) + end) + end) + + describe("ChangeSize", function() + it("should set size", function() + local state = ImportLocalTool(nil, ChangeSize({ + X = 123, + Y = 456, + Z = 789, + })) + + expect(state).to.be.ok() + expect(state.size).to.be.ok() + expect(state.size.X).to.equal(123) + expect(state.size.Y).to.equal(456) + expect(state.size.Z).to.equal(789) + end) + + it("should preserve immutability", function() + local immutabilityPreserved = testImmutability(ImportLocalTool, ChangeSize({ + X = 123, + Y = 456, + Z = 789, + })) + expect(immutabilityPreserved).to.equal(true) + end) + end) + + describe("SetUseColorMap", function() + it("should set useColorMap", function() + local state = ImportLocalTool(nil, SetUseColorMap(false)) + + expect(state).to.be.ok() + expect(state.useColorMap).to.be.ok() + expect(state.useColorMap).to.equal(false) + end) + + it("should preserve immutability", function() + local immutabilityPreserved = testImmutability(ImportLocalTool, SetUseColorMap(false)) + expect(immutabilityPreserved).to.equal(true) + end) + end) + + describe("SelectHeightmap", function() + it("should select the heightmap", function() + local heightmap = {} + local state = ImportLocalTool(nil, SelectHeightmap(heightmap)) + + expect(state).to.be.ok() + expect(state.heightmap).to.be.ok() + expect(state.heightmap).to.equal(heightmap) + end) + + it("should be clearable", function() + local heightmap = {} + local state = ImportLocalTool(nil, SelectHeightmap(heightmap)) + + expect(state.heightmap).to.equal(heightmap) + state = ImportLocalTool(state, SelectHeightmap(nil)) + expect(state.heightmap).to.equal(nil) + end) + + it("should preserve immutability", function() + local heightmap = {} + local immutabilityPreserved = testImmutability(ImportLocalTool, SelectHeightmap(heightmap)) + expect(immutabilityPreserved).to.equal(true) + end) + end) + + describe("SelectColormap", function() + it("should select the colormap", function() + local colormap = {} + local state = ImportLocalTool(nil, SelectColormap(colormap)) + + expect(state).to.be.ok() + expect(state.colormap).to.be.ok() + expect(state.colormap).to.equal(colormap) + end) + + it("should be clearable", function() + local colormap = {} + local state = ImportLocalTool(nil, SelectColormap(colormap)) + + expect(state.colormap).to.equal(colormap) + state = ImportLocalTool(state, SelectColormap(nil)) + expect(state.colormap).to.equal(nil) + end) + + it("should preserve immutability", function() + local colormap = {} + local immutabilityPreserved = testImmutability(ImportLocalTool, SelectColormap(colormap)) + expect(immutabilityPreserved).to.equal(true) + end) + end) +end diff --git a/BuiltInPlugins/TerrainToolsV2/Src/Reducers/MainReducer.lua b/BuiltInPlugins/TerrainToolsV2/Src/Reducers/MainReducer.lua index 62115a4b40..435e54960b 100644 --- a/BuiltInPlugins/TerrainToolsV2/Src/Reducers/MainReducer.lua +++ b/BuiltInPlugins/TerrainToolsV2/Src/Reducers/MainReducer.lua @@ -12,6 +12,7 @@ local FlattenTool = require(Reducers.FlattenTool) local GenerateTool = require(Reducers.GenerateTool) local GrowTool = require(Reducers.GrowTool) local ImportTool = require(Reducers.ImportTool) +local ImportLocalTool = require(Reducers.ImportLocalTool) local PaintTool = require(Reducers.PaintTool) local RegionTool = require(Reducers.RegionTool) local SeaLevelTool = require(Reducers.SeaLevelTool) @@ -20,10 +21,12 @@ local SmoothTool = require(Reducers.SmoothTool) local SubtractTool = require(Reducers.SubtractTool) local FFlagTerrainToolsConvertPartTool = game:GetFastFlag("TerrainToolsConvertPartTool") +local FFlagTerrainToolsBetterImportTool = game:GetFastFlag("TerrainToolsBetterImportTool") local toolReducerTable = { GenerateTool = GenerateTool, ImportTool = ImportTool, + ImportLocalTool = FFlagTerrainToolsBetterImportTool and ImportLocalTool or nil, ConvertPartTool = FFlagTerrainToolsConvertPartTool and ConvertPartTool or nil, RegionTool = RegionTool, FillTool = FillTool, @@ -47,6 +50,7 @@ local MainReducer = function(state, action) GenerateTool = GenerateTool(state, action), ImportTool = ImportTool(state, action), + ImportLocalTool = FFlagTerrainToolsBetterImportTool and ImportLocalTool(state, action) or nil, ConvertPartTool = FFlagTerrainToolsConvertPartTool and ConvertPartTool(state, action) or nil, diff --git a/BuiltInPlugins/TerrainToolsV2/Src/Resources/PluginTheme.lua b/BuiltInPlugins/TerrainToolsV2/Src/Resources/PluginTheme.lua index 036651696b..07a0cca459 100644 --- a/BuiltInPlugins/TerrainToolsV2/Src/Resources/PluginTheme.lua +++ b/BuiltInPlugins/TerrainToolsV2/Src/Resources/PluginTheme.lua @@ -233,6 +233,31 @@ function Theme.createValues(getColor, c, m) otherTabOffset = UDim2.new(0, 0, 0, 0), }) + local promptSelectorWithPreviewTheme = defineTheme({ + expandIcon = "rbxasset://textures/StudioSharedUI/preview_expand.png", + clearIcon = "rbxasset://textures/StudioSharedUI/preview_clear.png", + importIcon = "rbxasset://textures/StudioSharedUI/import_2x.png", + + previewBackgroundColor = getColor(c.RibbonButton), + previewHoveredBackgroundColor = getColor(c.RibbonButton, m.Hover), + previewBorderColor = getColor(c.Border), + + buttonIconColor = Color3.fromRGB(167, 167, 167), + buttonIconHoveredColor = Color3.fromRGB(254, 254, 254), + + toolbarTransparency = 0.4, + toolbarBackgroundColor = Color3.fromRGB(0, 0, 0), + toolbarButtonBackgroundColor = Color3.fromRGB(39, 39, 39), + }, { + Dark = { + buttonIconColor = Color3.fromRGB(167, 167, 167), + buttonIconHoveredColor = Color3.fromRGB(254, 254, 254), + + toolbarBackgroundColor = Color3.fromRGB(0, 0, 0), + toolbarButtonBackgroundColor = Color3.fromRGB(39, 39, 39), + }, + }) + if FFlagTerrainToolsUseDevFramework then -- In the first part of the move to dev framework, we've had to port some components from UI library -- Those components used colours etc. defined inside UI library, so we port them here too @@ -315,6 +340,7 @@ function Theme.createValues(getColor, c, m) roundToggleTextButtonTheme = roundToggleTextButtonTheme, singleSelectButtonGroupTheme = singleSelectButtonGroupTheme, propertyLockTheme = propertyLockTheme, + promptSelectorWithPreviewTheme = promptSelectorWithPreviewTheme, -- Extras for ui library compatibility checkBox = checkBox, @@ -351,6 +377,7 @@ function Theme.createValues(getColor, c, m) roundToggleTextButtonTheme = roundToggleTextButtonTheme, singleSelectButtonGroupTheme = singleSelectButtonGroupTheme, propertyLockTheme = propertyLockTheme, + promptSelectorWithPreviewTheme = promptSelectorWithPreviewTheme, textSize = 14, padding = 4, font = Enum.Font.SourceSans, diff --git a/BuiltInPlugins/TerrainToolsV2/Src/TerrainInterfaces/TerrainImporterInstance.lua b/BuiltInPlugins/TerrainToolsV2/Src/TerrainInterfaces/TerrainImporterInstance.lua index 99a2addda9..6a1b8da465 100644 --- a/BuiltInPlugins/TerrainToolsV2/Src/TerrainInterfaces/TerrainImporterInstance.lua +++ b/BuiltInPlugins/TerrainToolsV2/Src/TerrainInterfaces/TerrainImporterInstance.lua @@ -1,5 +1,7 @@ local FFlagTerrainToolsUseDevFramework = game:GetFastFlag("TerrainToolsUseDevFramework") +local FFlagTerrainToolsBetterImportTool = game:GetFastFlag("TerrainToolsBetterImportTool") + local Plugin = script.Parent.Parent.Parent local Framework = require(Plugin.Packages.Framework) @@ -19,9 +21,11 @@ local AnalyticsService = game:GetService("RbxAnalyticsService") local StudioService = game:GetService("StudioService") local function validateImportSettingsOrWarn(importSettings, localization) - if tonumber(game.GameId) == 0 then - warn(localization:getText("Warning", "RequirePublishedForImport")) - return false + if not FFlagTerrainToolsBetterImportTool then + if tonumber(game.GameId) == 0 then + warn(localization:getText("Warning", "RequirePublishedForImport")) + return false + end end if type(importSettings.heightMapUrl) ~= "string" or importSettings.heightMapUrl == "" then @@ -51,7 +55,7 @@ function TerrainImporter.new(options) _importSettings = { position = Vector3.new(0, 0, 0), size = Vector3.new(0, 0, 0), - useColorMap = true, + useColorMap = not FFlagTerrainToolsBetterImportTool, -- Default to false when flag on heightMapUrl = "", colorMapUrl = "", }, diff --git a/BuiltInPlugins/TerrainToolsV2/Src/Util/Constants.lua b/BuiltInPlugins/TerrainToolsV2/Src/Util/Constants.lua index 27351bc634..d7fb0ef417 100644 --- a/BuiltInPlugins/TerrainToolsV2/Src/Util/Constants.lua +++ b/BuiltInPlugins/TerrainToolsV2/Src/Util/Constants.lua @@ -13,6 +13,7 @@ local Constants = {} Constants.ToolIcons = { [ToolId.Generate] = TexturePath .. "mt_generate.png", [ToolId.Import] = TexturePath .. "mt_terrain_import.png", + [ToolId.ImportLocal] = TexturePath .. "mt_terrain_import.png", [ToolId.ConvertPart] = TexturePath .. "mt_convert_part.png", [ToolId.SeaLevel] = TexturePath .. "mt_sea_level.png", [ToolId.Replace] = TexturePath .. "mt_replace.png", @@ -70,6 +71,7 @@ Constants.ToolActivatesPlugin = { [ToolId.SeaLevel] = true, [ToolId.Generate] = FFlagTerrainToolsUseMapSettingsWithPreview, [ToolId.Import] = FFlagTerrainToolsUseMapSettingsWithPreview, + [ToolId.ImportLocal] = true, [ToolId.Replace] = true, } diff --git a/BuiltInPlugins/TerrainToolsV2/Src/Util/TerrainEnums.lua b/BuiltInPlugins/TerrainToolsV2/Src/Util/TerrainEnums.lua index e7c0b43086..86b6f1d500 100644 --- a/BuiltInPlugins/TerrainToolsV2/Src/Util/TerrainEnums.lua +++ b/BuiltInPlugins/TerrainToolsV2/Src/Util/TerrainEnums.lua @@ -9,7 +9,10 @@ local TerrainEnums = {} TerrainEnums.ToolId = { Generate = "Generate", + Import = "Import", + ImportLocal = "ImportLocal", + SeaLevel = "SeaLevel", Replace = "Replace", Clear = "Clear", diff --git a/BuiltInPlugins/Toolbox/Core/Components/ToolboxPlugin.lua b/BuiltInPlugins/Toolbox/Core/Components/ToolboxPlugin.lua index 06a61838d9..7054941e17 100644 --- a/BuiltInPlugins/Toolbox/Core/Components/ToolboxPlugin.lua +++ b/BuiltInPlugins/Toolbox/Core/Components/ToolboxPlugin.lua @@ -18,25 +18,27 @@ local makeTheme = require(Util.makeTheme) local ContextServices = require(Libs.Framework.ContextServices) local UILibraryWrapper = ContextServices.UILibraryWrapper +local FrameworkUtil = require(Libs.Framework.Util) +local getTestVariation = FrameworkUtil.getTestVariation local Analytics = require(Util.Analytics.Analytics) local FFlagEnableToolboxImpressionAnalytics = game:GetFastFlag("EnableToolboxImpressionAnalytics") local FFlagBootstrapperTryAsset = game:GetFastFlag("BootstrapperTryAsset") +-- Be sure to turn off ToolboxShowHideABTest before turning on StudioShowHideABTestV2 local FFlagToolboxShowHideABTest = game:GetFastFlag("ToolboxShowHideABTest") +local FFlagStudioShowHideABTestV2 = game:GetFastFlag("StudioShowHideABTestV2") -local AB_TEST_GROUP_CONTROL = "Control" - --- ShowHideToolbox : AB Test where Toolbox shows on startup for users not in the Control group --- Control : Toolbox appears on startup --- All Variations : Toolbox hidden on startup local ShowHideABTestName = "AllUsers.RobloxStudio.ShowHideToolbox" +local ABTEST_SHOWHIDEV2_NAME = "AllUsers.RobloxStudio.ShowHideV2" local function shouldSeeTestBehavior(abTestName) + -- REMOVE THIS WITH FFlagShowHideABTest + -- helper function for showing a behavior so long as the result is not "Control" -- further specificity can be used if the exact variation is required local variation = ABTestService:GetVariant(abTestName) - local shouldShowBehavior = variation ~= AB_TEST_GROUP_CONTROL + local shouldShowBehavior = variation ~= "Control" return shouldShowBehavior, variation end @@ -151,8 +153,15 @@ function ToolboxPlugin:render() local isToolboxHidden = shouldSeeTestBehavior(ShowHideABTestName) if isToolboxHidden then initialEnabled = false - else + end + elseif FFlagStudioShowHideABTestV2 then + local variation = getTestVariation(ABTEST_SHOWHIDEV2_NAME) + if variation == 0 or variation == 2 then + -- Even though 0 is supposed to be the Control group and preserve existing behaviors, + -- Toolbox should be enabled by default. The fact that it isn't is a bug. initialEnabled = true + elseif variation == 1 then + initialEnabled = false end end diff --git a/BuiltInPlugins/Toolbox/Core/Networking/Requests/ChangeMarketplaceTab.lua b/BuiltInPlugins/Toolbox/Core/Networking/Requests/ChangeMarketplaceTab.lua index 44473f81e0..eda0f9eae2 100644 --- a/BuiltInPlugins/Toolbox/Core/Networking/Requests/ChangeMarketplaceTab.lua +++ b/BuiltInPlugins/Toolbox/Core/Networking/Requests/ChangeMarketplaceTab.lua @@ -3,6 +3,7 @@ local FFlagUseCategoryNameInToolbox = game:GetFastFlag("UseCategoryNameInToolbox local Plugin = script.Parent.Parent.Parent.Parent local Cryo = require(Plugin.Libs.Cryo) +local RobloxAPI = require(Plugin.Libs.Framework).RobloxAPI local RequestReason = require(Plugin.Core.Types.RequestReason) @@ -19,7 +20,7 @@ return function(networkInterface, tabName, newCategories, settings, options) local categories = Category.getCategories(tabName, store:getState().roles) local creator = Cryo.None - if FFlagToolboxShowRobloxCreatedAssetsForLuobu then + if FFlagToolboxShowRobloxCreatedAssetsForLuobu and RobloxAPI:baseURLHasChineseHost() then creator = options.creator or Cryo.None end diff --git a/BuiltInPlugins/Toolbox/Core/Types/Category.lua b/BuiltInPlugins/Toolbox/Core/Types/Category.lua index f117ff45f1..50afc33d8c 100644 --- a/BuiltInPlugins/Toolbox/Core/Types/Category.lua +++ b/BuiltInPlugins/Toolbox/Core/Types/Category.lua @@ -281,12 +281,10 @@ Category.RECENT_KEY = "Recent" Category.CREATIONS_KEY = "Creations" table.insert(Category.INVENTORY, Category.MY_PLUGINS) -if not FFlagToolboxShowRobloxCreatedAssetsForLuobu then - if FFlagOnlyWhitelistedPluginsInStudio then - table.insert(Category.MARKETPLACE, Category.WHITELISTED_PLUGINS) - else - table.insert(Category.MARKETPLACE, Category.FREE_PLUGINS) - end +if FFlagOnlyWhitelistedPluginsInStudio then + table.insert(Category.MARKETPLACE, Category.WHITELISTED_PLUGINS) +else + table.insert(Category.MARKETPLACE, Category.FREE_PLUGINS) end local insertIndex = Cryo.List.find(Category.INVENTORY_WITH_GROUPS, Category.MY_PACKAGES) + 1 @@ -294,7 +292,7 @@ table.insert(Category.INVENTORY_WITH_GROUPS, insertIndex, Category.MY_PLUGINS) local insertIndex2 = Cryo.List.find(Category.INVENTORY_WITH_GROUPS, Category.GROUP_AUDIO) + 1 table.insert(Category.INVENTORY_WITH_GROUPS, insertIndex2, Category.GROUP_PLUGINS) -if FFlagToolboxShowRobloxCreatedAssetsForLuobu then +if FFlagToolboxShowRobloxCreatedAssetsForLuobu and RobloxAPI:baseURLHasChineseHost() then local disabledCategories = string.split(FStringLuobuMarketplaceDisabledCategories, ";") for _, categoryName in pairs(disabledCategories) do diff --git a/BuiltInPlugins/Toolbox/Core/Util/InsertAsset.lua b/BuiltInPlugins/Toolbox/Core/Util/InsertAsset.lua index b06f4790b2..b3cfd145d8 100644 --- a/BuiltInPlugins/Toolbox/Core/Util/InsertAsset.lua +++ b/BuiltInPlugins/Toolbox/Core/Util/InsertAsset.lua @@ -186,7 +186,7 @@ local function insertDecal(plugin, assetId, assetName) decal.Name = assetName if FFlagMarketplaceSourceAssetIds then - decal.SourceAssetId = tbl[1] + decal.SourceAssetId = assetId end if FFlagToolboxFixDecalInsert then diff --git a/BuiltInPlugins/Toolbox/Libs/Framework/ContextServices/Theme.spec.lua b/BuiltInPlugins/Toolbox/Libs/Framework/ContextServices/Theme.spec.lua index 22377dc00b..1547fb3c11 100644 --- a/BuiltInPlugins/Toolbox/Libs/Framework/ContextServices/Theme.spec.lua +++ b/BuiltInPlugins/Toolbox/Libs/Framework/ContextServices/Theme.spec.lua @@ -11,6 +11,13 @@ return function() local StyleTable = Util.StyleTable local StyleModifier = Util.StyleModifier local Signal = Util.Signal + local FlagsList = Util.Flags.new({ + FFlagRefactorDevFrameworkTheme = {"RefactorDevFrameworkTheme"}, + }) + + if FlagsList:get("FFlagRefactorDevFrameworkTheme") then + return + end local function createTestThemedComponent(render) local TestThemedComponent = Roact.PureComponent:extend("TestThemedComponent") diff --git a/BuiltInPlugins/Toolbox/Libs/Framework/ContextServices/UILibraryWrapper.spec.lua b/BuiltInPlugins/Toolbox/Libs/Framework/ContextServices/UILibraryWrapper.spec.lua index 0823cb2280..d4bbb673db 100644 --- a/BuiltInPlugins/Toolbox/Libs/Framework/ContextServices/UILibraryWrapper.spec.lua +++ b/BuiltInPlugins/Toolbox/Libs/Framework/ContextServices/UILibraryWrapper.spec.lua @@ -2,8 +2,10 @@ return function() local Framework = script.Parent.Parent local Util = require(Framework.Util) + local Signal = Util.Signal local FlagsList = Util.Flags.new({ FFlagStudioDevFrameworkPackage = {"StudioDevFrameworkPackage"}, + FFlagRefactorDevFrameworkTheme = {"RefactorDevFrameworkTheme"}, }) local isUsedAsPackage = require(Framework.Util.isUsedAsPackage) @@ -18,6 +20,7 @@ return function() local Focus = require(Framework.ContextServices.Focus) local Theme = require(Framework.ContextServices.Theme) local UILibraryWrapper = require(script.Parent.UILibraryWrapper) + local StudioTheme = require(Framework.Style.Themes.StudioTheme) local WrapperStub = Roact.PureComponent:extend("UILibraryWrapper") @@ -29,12 +32,26 @@ return function() Wrapper = WrapperStub } + local function createThemeMock() + + if FlagsList:get("FFlagRefactorDevFrameworkTheme") then + return StudioTheme.mock() + else + local createStyles = function() + return {} + end + local getTheme = function() + return "Light" + end + local themeChanged = Signal.new() + return Theme.mock(createStyles, getTheme, themeChanged) + end + end + it("should expect a Plugin ContextItem provided above", function() local focus = Focus.new(Instance.new("ScreenGui")) local wrapper = UILibraryWrapper.new(UILibraryStub) - local theme = Theme.new(function() - return {} - end) + local theme = createThemeMock() function theme:getUILibraryTheme() return {} end @@ -55,9 +72,7 @@ return function() it("should expect a Focus ContextItem provided above", function() local plugin = Plugin.new({}) local wrapper = UILibraryWrapper.new(UILibraryStub) - local theme = Theme.new(function() - return {} - end) + local theme = createThemeMock() function theme:getUILibraryTheme() return {} end @@ -97,9 +112,7 @@ return function() local plugin = Plugin.new({}) local focus = Focus.new(Instance.new("ScreenGui")) local wrapper = UILibraryWrapper.new(UILibraryStub) - local theme = Theme.new(function() - return {} - end) + local theme = createThemeMock() local element = provide({ plugin, @@ -119,9 +132,7 @@ return function() local plugin = Plugin.new({}) local focus = Focus.new(Instance.new("ScreenGui")) local wrapper = UILibraryWrapper.new(UILibraryStub) - local theme = Theme.new(function() - return {} - end) + local theme = createThemeMock() function theme:getUILibraryTheme() return {} end diff --git a/BuiltInPlugins/Toolbox/Libs/Framework/StudioUI/TitledFrame/test.spec.lua b/BuiltInPlugins/Toolbox/Libs/Framework/StudioUI/TitledFrame/test.spec.lua index cff37efea8..f013a391bc 100644 --- a/BuiltInPlugins/Toolbox/Libs/Framework/StudioUI/TitledFrame/test.spec.lua +++ b/BuiltInPlugins/Toolbox/Libs/Framework/StudioUI/TitledFrame/test.spec.lua @@ -17,7 +17,7 @@ return function() local function createTestTitledFrame(children, container) local theme if FlagsList:get("FFlagRefactorDevFrameworkTheme") then - theme = StudioTheme.new() + theme = StudioTheme.mock() else theme = Theme.new(function() return { diff --git a/BuiltInPlugins/Toolbox/Libs/Framework/StudioUI/createPluginWidget.lua b/BuiltInPlugins/Toolbox/Libs/Framework/StudioUI/createPluginWidget.lua index 85620982d3..b9dd6d3759 100644 --- a/BuiltInPlugins/Toolbox/Libs/Framework/StudioUI/createPluginWidget.lua +++ b/BuiltInPlugins/Toolbox/Libs/Framework/StudioUI/createPluginWidget.lua @@ -9,10 +9,12 @@ ]] game:DefineFastFlag("FixDevFrameworkDockWidgetRestore", false) -game:DefineFastFlag("DevFrameworkPluginWidgetEnabledEvent", false) +game:DefineFastFlag("DevFrameworkPluginWidgetEnabledEvent2", false) +game:DefineFastFlag("DevFrameworkPluginWidgetUseSiblingZIndex", false) local FFlagFixDevFrameworkDockWidgetRestore = game:GetFastFlag("FixDevFrameworkDockWidgetRestore") -local FFlagDevFrameworkPluginWidgetEnabledEvent = game:GetFastFlag("DevFrameworkPluginWidgetEnabledEvent") +local FFlagDevFrameworkPluginWidgetEnabledEvent2 = game:GetFastFlag("DevFrameworkPluginWidgetEnabledEvent2") +local FFlagDevFrameworkPluginWidgetUseSiblingZIndex = game:GetFastFlag("DevFrameworkPluginWidgetUseSiblingZIndex") local Framework = script.Parent.Parent local Roact = require(Framework.Parent.Roact) @@ -31,7 +33,11 @@ local function createPluginWidget(componentName, createWidgetFunc) local widget = createWidgetFunc(props) widget.Name = title or "" - widget.ZIndexBehavior = props.ZIndexBehavior or Enum.ZIndexBehavior.Global + if FFlagDevFrameworkPluginWidgetUseSiblingZIndex then + widget.ZIndexBehavior = props.ZIndexBehavior or Enum.ZIndexBehavior.Sibling + else + widget.ZIndexBehavior = props.ZIndexBehavior or Enum.ZIndexBehavior.Global + end if widget:IsA("PluginGui") then widget:BindToClose(onClose) @@ -67,12 +73,13 @@ local function createPluginWidget(componentName, createWidgetFunc) end end - if FFlagDevFrameworkPluginWidgetEnabledEvent then + if FFlagDevFrameworkPluginWidgetEnabledEvent2 then -- Connect to enabled changing *after* restore -- Otherwise users of this will get 2 enabled changes: one from the onRestore, and the same from Roact.Change.Enabled self.widgetEnabledChangedConnection = widget:GetPropertyChangedSignal("Enabled"):Connect(function() - if self.props[Roact.Change.Enabled] then - self.props[Roact.Change.Enabled](self.widget) + local callback = self.props[Roact.Change.Enabled] + if callback and self.widget and self.widget.Enabled ~= self.props.Enabled then + callback(self.widget) end end) end diff --git a/BuiltInPlugins/Toolbox/Libs/Framework/Style/Stylizer.lua b/BuiltInPlugins/Toolbox/Libs/Framework/Style/Stylizer.lua index 47a60c5663..228c4b165a 100644 --- a/BuiltInPlugins/Toolbox/Libs/Framework/Style/Stylizer.lua +++ b/BuiltInPlugins/Toolbox/Libs/Framework/Style/Stylizer.lua @@ -1,3 +1,5 @@ +--!nocheck + --[[ Wraps theme styles and update logic into a ContextItem. @@ -318,4 +320,4 @@ function Stylizer.mock(t, themeProps, callback) return self end -return Stylizer \ No newline at end of file +return Stylizer diff --git a/BuiltInPlugins/Toolbox/Libs/Framework/Style/getRawComponentStyle.lua b/BuiltInPlugins/Toolbox/Libs/Framework/Style/getRawComponentStyle.lua index 7e659ca0c0..5c76de757c 100644 --- a/BuiltInPlugins/Toolbox/Libs/Framework/Style/getRawComponentStyle.lua +++ b/BuiltInPlugins/Toolbox/Libs/Framework/Style/getRawComponentStyle.lua @@ -9,6 +9,7 @@ local UIFolderData = require(Framework.UI.UIFolderData) return function(componentName) local componentData = UIFolderData[componentName] or StudioUIFolderData[componentName] local result + if componentData.style then result = require(componentData.style) end diff --git a/BuiltInPlugins/Toolbox/Libs/Framework/TestHelpers/provideMockContext.lua b/BuiltInPlugins/Toolbox/Libs/Framework/TestHelpers/provideMockContext.lua index 04cc85664e..10633a0dc7 100644 --- a/BuiltInPlugins/Toolbox/Libs/Framework/TestHelpers/provideMockContext.lua +++ b/BuiltInPlugins/Toolbox/Libs/Framework/TestHelpers/provideMockContext.lua @@ -91,7 +91,7 @@ return function(contextItemsList, children) -- Theme local theme if FlagsList:get("FFlagRefactorDevFrameworkTheme") then - theme = StudioTheme.new() + theme = StudioTheme.mock() else theme = ContextServices.Theme.mock(function(theme, getColor) return { diff --git a/BuiltInPlugins/Toolbox/Libs/Framework/UI/Box/test.spec.lua b/BuiltInPlugins/Toolbox/Libs/Framework/UI/Box/test.spec.lua index 27baa93e01..edc3b7c984 100644 --- a/BuiltInPlugins/Toolbox/Libs/Framework/UI/Box/test.spec.lua +++ b/BuiltInPlugins/Toolbox/Libs/Framework/UI/Box/test.spec.lua @@ -23,7 +23,7 @@ return function() else local theme if FlagsList:get("FFlagRefactorDevFrameworkTheme") then - theme = StudioTheme.new() + theme = StudioTheme.mock() else theme = Theme.new(function() return { diff --git a/BuiltInPlugins/Toolbox/Libs/Framework/UI/Button/test.spec.lua b/BuiltInPlugins/Toolbox/Libs/Framework/UI/Button/test.spec.lua index 3ffc122f23..cafabfaa40 100644 --- a/BuiltInPlugins/Toolbox/Libs/Framework/UI/Button/test.spec.lua +++ b/BuiltInPlugins/Toolbox/Libs/Framework/UI/Button/test.spec.lua @@ -18,7 +18,7 @@ return function() local function createTestButton(props, children) local theme if FlagsList:get("FFlagRefactorDevFrameworkTheme") then - theme = StudioTheme.new() + theme = StudioTheme.mock() else theme = Theme.new(function() return { diff --git a/BuiltInPlugins/Toolbox/Libs/Framework/UI/Container/test.spec.lua b/BuiltInPlugins/Toolbox/Libs/Framework/UI/Container/test.spec.lua index 21e7c8874a..1417100db0 100644 --- a/BuiltInPlugins/Toolbox/Libs/Framework/UI/Container/test.spec.lua +++ b/BuiltInPlugins/Toolbox/Libs/Framework/UI/Container/test.spec.lua @@ -18,7 +18,7 @@ return function() local function createTestContainer(props, children) local theme if FlagsList:get("FFlagRefactorDevFrameworkTheme") then - theme = StudioTheme.new() + theme = StudioTheme.mock() else theme = Theme.new(function() return { diff --git a/BuiltInPlugins/Toolbox/Libs/Framework/UI/DropdownMenu.lua b/BuiltInPlugins/Toolbox/Libs/Framework/UI/DropdownMenu.lua index 60f9fadd0c..b1e6b3c757 100644 --- a/BuiltInPlugins/Toolbox/Libs/Framework/UI/DropdownMenu.lua +++ b/BuiltInPlugins/Toolbox/Libs/Framework/UI/DropdownMenu.lua @@ -44,6 +44,7 @@ local TextLabel = require(UI.TextLabel) local FlagsList = Util.Flags.new({ FFlagRefactorDevFrameworkTheme = {"RefactorDevFrameworkTheme"}, + FFlagRefactorDevFrameworkContextItems = {"RefactorDevFrameworkContextItems"}, }) local DropdownMenu = Roact.PureComponent:extend("DropdownMenu") @@ -201,7 +202,7 @@ function DropdownMenu:renderMenu() local width = style.Width local offset = prioritize(style.Offset, Vector2.new(0, 0)) - local pluginGui = props.Focus:getTarget() + local pluginGui = FlagsList:get("FFlagRefactorDevFrameworkContextItems") and props.Focus:get() or props.Focus:getTarget() local menuPositionAndSize = self.getPositionAndSize(pluginGui, width, offset) local x = menuPositionAndSize.X diff --git a/BuiltInPlugins/Toolbox/Libs/Framework/UI/FakeLoadingBar/test.spec.lua b/BuiltInPlugins/Toolbox/Libs/Framework/UI/FakeLoadingBar/test.spec.lua index 4533d1b2a4..1306596adc 100644 --- a/BuiltInPlugins/Toolbox/Libs/Framework/UI/FakeLoadingBar/test.spec.lua +++ b/BuiltInPlugins/Toolbox/Libs/Framework/UI/FakeLoadingBar/test.spec.lua @@ -17,7 +17,7 @@ return function() local function createTestFakeLoadingBar(props, children) local theme if FlagsList:get("FFlagRefactorDevFrameworkTheme") then - theme = StudioTheme.new() + theme = StudioTheme.mock() else theme = Theme.new(function() return { diff --git a/BuiltInPlugins/Toolbox/Libs/Framework/UI/Image/test.spec.lua b/BuiltInPlugins/Toolbox/Libs/Framework/UI/Image/test.spec.lua index c5aa507a3a..6860be79dd 100644 --- a/BuiltInPlugins/Toolbox/Libs/Framework/UI/Image/test.spec.lua +++ b/BuiltInPlugins/Toolbox/Libs/Framework/UI/Image/test.spec.lua @@ -17,7 +17,7 @@ return function() local function createTestImageDecoration() local theme if FlagsList:get("FFlagRefactorDevFrameworkTheme") then - theme = StudioTheme.new() + theme = StudioTheme.mock() else theme = Theme.new(function() return { diff --git a/BuiltInPlugins/Toolbox/Libs/Framework/UI/InstanceTreeView/InstanceTreeRow.lua b/BuiltInPlugins/Toolbox/Libs/Framework/UI/InstanceTreeView/InstanceTreeRow.lua index 8000e57785..6110490a54 100644 --- a/BuiltInPlugins/Toolbox/Libs/Framework/UI/InstanceTreeView/InstanceTreeRow.lua +++ b/BuiltInPlugins/Toolbox/Libs/Framework/UI/InstanceTreeView/InstanceTreeRow.lua @@ -25,7 +25,7 @@ local TextLabel = require(UI.TextLabel) Typecheck.wrap(InstanceTreeRow, script) local function getClassIcon(instance) - local StudioService = game:GetService("StudioService") + local StudioService = game:GetService("StudioService") local className = instance.ClassName if instance.IsA then if instance:IsA("JointInstance") and className == "ManualWeld" or className == "ManualGlue" then @@ -73,9 +73,10 @@ function InstanceTreeRow:render() local arrowSize = style.Arrow.Size local padding = style.IconPadding local iconInfo = getClassIcon(item) - local iconSize = iconInfo.ImageRectSize.X + -- Default iconSize to (0, 0) as ImageRectSize is unavailable in Roblox CLI + local iconSize = iconInfo.ImageRectSize or Vector2.new() local labelOffset = indent + arrowSize + 2 * padding - local textOffset = iconSize + 3 * padding + local textOffset = iconSize.X + 3 * padding return Roact.createElement(Container, { Size = UDim2.new(1, -indent, 0, style.RowHeight), @@ -105,10 +106,10 @@ function InstanceTreeRow:render() Size = UDim2.new(1, -arrowSize, 1, 0), }, { Icon = Roact.createElement("ImageLabel", { - Size = UDim2.fromOffset(iconSize, iconInfo.ImageRectSize.Y), + Size = UDim2.fromOffset(iconSize.X, iconSize.Y), BackgroundTransparency = 1, Image = iconInfo.Image, - ImageRectSize = iconInfo.ImageRectSize, + ImageRectSize = iconSize, ImageRectOffset = iconInfo.ImageRectOffset, Position = UDim2.new(0, padding, 0.5, 0), AnchorPoint = Vector2.new(0, 0.5) diff --git a/BuiltInPlugins/Toolbox/Libs/Framework/UI/LinkText/test.spec.lua b/BuiltInPlugins/Toolbox/Libs/Framework/UI/LinkText/test.spec.lua index d72d172041..37cecf30b1 100644 --- a/BuiltInPlugins/Toolbox/Libs/Framework/UI/LinkText/test.spec.lua +++ b/BuiltInPlugins/Toolbox/Libs/Framework/UI/LinkText/test.spec.lua @@ -19,7 +19,7 @@ return function() local mouse = Mouse.new({}) local theme if FlagsList:get("FFlagRefactorDevFrameworkTheme") then - theme = StudioTheme.new() + theme = StudioTheme.mock() else theme = Theme.new(function() return { diff --git a/BuiltInPlugins/Toolbox/Libs/Framework/UI/LoadingBar/test.spec.lua b/BuiltInPlugins/Toolbox/Libs/Framework/UI/LoadingBar/test.spec.lua index ea437b136c..c898cde0f4 100644 --- a/BuiltInPlugins/Toolbox/Libs/Framework/UI/LoadingBar/test.spec.lua +++ b/BuiltInPlugins/Toolbox/Libs/Framework/UI/LoadingBar/test.spec.lua @@ -18,7 +18,7 @@ return function() local function createTestLoadingBar(props, children) local theme if FlagsList:get("FFlagRefactorDevFrameworkTheme") then - theme = StudioTheme.new() + theme = StudioTheme.mock() else theme = Theme.new(function() return { diff --git a/BuiltInPlugins/Toolbox/Libs/Framework/UI/RadioButton/test.spec.lua b/BuiltInPlugins/Toolbox/Libs/Framework/UI/RadioButton/test.spec.lua index 31e2c12c77..4e578229a9 100644 --- a/BuiltInPlugins/Toolbox/Libs/Framework/UI/RadioButton/test.spec.lua +++ b/BuiltInPlugins/Toolbox/Libs/Framework/UI/RadioButton/test.spec.lua @@ -23,7 +23,7 @@ return function() local function createTestRadioButton(props, children) local theme if FlagsList:get("FFlagRefactorDevFrameworkTheme") then - theme = StudioTheme.new() + theme = StudioTheme.mock() else theme = Theme.new(function() return { diff --git a/BuiltInPlugins/Toolbox/Libs/Framework/UI/RoundBox/test.spec.lua b/BuiltInPlugins/Toolbox/Libs/Framework/UI/RoundBox/test.spec.lua index 10b16f79a0..d9715cf163 100644 --- a/BuiltInPlugins/Toolbox/Libs/Framework/UI/RoundBox/test.spec.lua +++ b/BuiltInPlugins/Toolbox/Libs/Framework/UI/RoundBox/test.spec.lua @@ -17,7 +17,7 @@ return function() local function createTestRoundBoxDecoration() local theme if FlagsList:get("FFlagRefactorDevFrameworkTheme") then - theme = StudioTheme.new() + theme = StudioTheme.mock() else theme = Theme.new(function() return { diff --git a/BuiltInPlugins/Toolbox/Libs/Framework/UI/TextLabel/test.spec.lua b/BuiltInPlugins/Toolbox/Libs/Framework/UI/TextLabel/test.spec.lua index a403d8b7d3..5f739cb55f 100644 --- a/BuiltInPlugins/Toolbox/Libs/Framework/UI/TextLabel/test.spec.lua +++ b/BuiltInPlugins/Toolbox/Libs/Framework/UI/TextLabel/test.spec.lua @@ -17,7 +17,7 @@ return function() local function createTestTextLabelDecoration() local theme if FlagsList:get("FFlagRefactorDevFrameworkTheme") then - theme = StudioTheme.new() + theme = StudioTheme.mock() else theme = Theme.new(function() return { diff --git a/BuiltInPlugins/Toolbox/Libs/Framework/UI/ToggleButton/test.spec.lua b/BuiltInPlugins/Toolbox/Libs/Framework/UI/ToggleButton/test.spec.lua index 0d712bdf45..c4db80d801 100644 --- a/BuiltInPlugins/Toolbox/Libs/Framework/UI/ToggleButton/test.spec.lua +++ b/BuiltInPlugins/Toolbox/Libs/Framework/UI/ToggleButton/test.spec.lua @@ -27,7 +27,7 @@ return function() local mouse = Mouse.new({}) local theme if FlagsList:get("FFlagRefactorDevFrameworkTheme") then - theme = StudioTheme.new() + theme = StudioTheme.mock() else theme = Theme.new(function() return { diff --git a/BuiltInPlugins/Toolbox/Libs/Framework/UI/Tooltip/test.spec.lua b/BuiltInPlugins/Toolbox/Libs/Framework/UI/Tooltip/test.spec.lua index 984329d7eb..8f7a0a6fc9 100644 --- a/BuiltInPlugins/Toolbox/Libs/Framework/UI/Tooltip/test.spec.lua +++ b/BuiltInPlugins/Toolbox/Libs/Framework/UI/Tooltip/test.spec.lua @@ -25,7 +25,7 @@ return function() local focus = Focus.new(target) local theme if FlagsList:get("FFlagRefactorDevFrameworkTheme") then - theme = StudioTheme.new() + theme = StudioTheme.mock() else theme = Theme.new(function() return { diff --git a/BuiltInPlugins/Toolbox/Libs/Framework/UI/TreeView/test.spec.lua b/BuiltInPlugins/Toolbox/Libs/Framework/UI/TreeView/test.spec.lua index 953aef3483..8d06ee8a86 100644 --- a/BuiltInPlugins/Toolbox/Libs/Framework/UI/TreeView/test.spec.lua +++ b/BuiltInPlugins/Toolbox/Libs/Framework/UI/TreeView/test.spec.lua @@ -63,7 +63,7 @@ return function() local function createTreeView() local theme if FlagsList:get("FFlagRefactorDevFrameworkTheme") then - theme = StudioTheme.new() + theme = StudioTheme.mock() else theme = Theme.new(function() return { diff --git a/BuiltInPlugins/Toolbox/Libs/Framework/Util.lua b/BuiltInPlugins/Toolbox/Libs/Framework/Util.lua index 5454c404f1..6a3e12c089 100644 --- a/BuiltInPlugins/Toolbox/Libs/Framework/Util.lua +++ b/BuiltInPlugins/Toolbox/Libs/Framework/Util.lua @@ -19,6 +19,7 @@ return strict({ FitFrame = require(script.FitFrame), Flags = require(script.Flags), + getTestVariation = require(script.getTestVariation), Immutable = require(script.Immutable), LayoutOrderIterator = require(script.LayoutOrderIterator), Math = require(script.Math), diff --git a/BuiltInPlugins/Toolbox/Libs/Framework/Util/CrossPluginCommunication.lua b/BuiltInPlugins/Toolbox/Libs/Framework/Util/CrossPluginCommunication.lua index 37caa1bece..d3b82f5783 100644 --- a/BuiltInPlugins/Toolbox/Libs/Framework/Util/CrossPluginCommunication.lua +++ b/BuiltInPlugins/Toolbox/Libs/Framework/Util/CrossPluginCommunication.lua @@ -22,10 +22,12 @@ CrossPluginCommunication.__index = CrossPluginCommunication CrossPluginCommunication.BASE_FOLDER_NAME = "CrossPluginCommunication" -function CrossPluginCommunication.new(pluginNamespace) +function CrossPluginCommunication.new(pluginNamespace, hostService) assert(t.string(pluginNamespace), "pluginNamespace must be a string") + assert(t.optional(t.instance(pluginNamespace)), "hostService must be an instance if defined") local self = { + hostService = hostService or RobloxPluginGuiService, pluginNamespace = pluginNamespace } @@ -49,7 +51,7 @@ function CrossPluginCommunication:ensureFolderExists(parent, name) end function CrossPluginCommunication:getNamespaceFolder() - local base = self:ensureFolderExists(RobloxPluginGuiService, self.BASE_FOLDER_NAME) + local base = self:ensureFolderExists(self.hostService, self.BASE_FOLDER_NAME) return self:ensureFolderExists(base, self.pluginNamespace) end diff --git a/BuiltInPlugins/Toolbox/Libs/Framework/Util/CrossPluginCommunication.spec.lua b/BuiltInPlugins/Toolbox/Libs/Framework/Util/CrossPluginCommunication.spec.lua index 60338f2b0c..f3c1035f50 100644 --- a/BuiltInPlugins/Toolbox/Libs/Framework/Util/CrossPluginCommunication.spec.lua +++ b/BuiltInPlugins/Toolbox/Libs/Framework/Util/CrossPluginCommunication.spec.lua @@ -1,5 +1,3 @@ -local RobloxPluginGuiService = game:GetService("RobloxPluginGuiService") - local Framework = script.Parent.Parent local CrossPluginCommunication = require(Framework.Util.CrossPluginCommunication) @@ -8,8 +6,10 @@ return function() -- TODO DEVTOOLS-4397: Move cleanup to afterEach hook after the dependency structure is fixed + local hostService = workspace + local function createTestComms() - return CrossPluginCommunication.new(TEST_NAMESPACE) + return CrossPluginCommunication.new(TEST_NAMESPACE, hostService) end describe("Invoke", function() @@ -32,7 +32,7 @@ return function() comms:OnInvoke("blah", function() end) - local namespace = RobloxPluginGuiService:FindFirstChild(CrossPluginCommunication.BASE_FOLDER_NAME):FindFirstChild(TEST_NAMESPACE) + local namespace = hostService:FindFirstChild(CrossPluginCommunication.BASE_FOLDER_NAME):FindFirstChild(TEST_NAMESPACE) assert(namespace) diff --git a/BuiltInPlugins/Toolbox/Libs/Framework/Util/Palette.spec.lua b/BuiltInPlugins/Toolbox/Libs/Framework/Util/Palette.spec.lua index 8794c80717..02911b17c7 100644 --- a/BuiltInPlugins/Toolbox/Libs/Framework/Util/Palette.spec.lua +++ b/BuiltInPlugins/Toolbox/Libs/Framework/Util/Palette.spec.lua @@ -1,5 +1,16 @@ return function() - local Palette = require(script.Parent.Palette) + + local Util = script.Parent + local Flags = require(Util.Flags) + local FlagsList = Flags.new({ + FFlagRefactorDevFrameworkTheme = {"RefactorDevFrameworkTheme"}, + }) + + if FlagsList:get("FFlagRefactorDevFrameworkTheme") then + return + end + + local Palette = require(Util.Palette) it("should be a table of color StyleValues", function() expect(Palette).to.be.ok() diff --git a/BuiltInPlugins/Toolbox/Libs/Framework/Util/Typecheck/t.lua b/BuiltInPlugins/Toolbox/Libs/Framework/Util/Typecheck/t.lua index 44c8d50af8..045d7ac0dd 100644 --- a/BuiltInPlugins/Toolbox/Libs/Framework/Util/Typecheck/t.lua +++ b/BuiltInPlugins/Toolbox/Libs/Framework/Util/Typecheck/t.lua @@ -1,3 +1,5 @@ +--!nocheck + --[[ DeveloperFramework uses t from Osyris https://github.com/osyrisrblx/t diff --git a/BuiltInPlugins/Toolbox/Libs/Framework/Util/getTestVariation.lua b/BuiltInPlugins/Toolbox/Libs/Framework/Util/getTestVariation.lua new file mode 100644 index 0000000000..d09873db1d --- /dev/null +++ b/BuiltInPlugins/Toolbox/Libs/Framework/Util/getTestVariation.lua @@ -0,0 +1,30 @@ +-- This function hides ABTestService away and provides a quick interface for getting a test's variation. +-- Should the requested test not exist, it will return the Control group. + +local ABTestService = game:GetService("ABTestService") + +local AB_TEST_GROUP_CONTROL = "Control" + +-- abTestName : (string) the name of test. ex) "AllUsers.Studio.ExampleTestName" +-- abTestService : (table, optional) an optional override for ABTestService +-- RETURNS : (int) the variation that the user is in. 0 = Control +return function(abTestName, abTestService) + assert(type(abTestName) == "string", "Expected abTestName to be a string") + if abTestService then + assert(type(abTestService.GetVariant) == "function", "Expected the abTestService object to have a GetVariant member function") + else + abTestService = ABTestService + end + + local variation = abTestService:GetVariant(abTestName) + if variation == AB_TEST_GROUP_CONTROL then + return 0 + else + local _, _, variationNumber = string.find(variation, "Variation(%d+)") + if not variationNumber then + return 0 + end + + return tonumber(variationNumber) + end +end diff --git a/BuiltInPlugins/Toolbox/Libs/Framework/Util/getTestVariation.spec.lua b/BuiltInPlugins/Toolbox/Libs/Framework/Util/getTestVariation.spec.lua new file mode 100644 index 0000000000..5febceee91 --- /dev/null +++ b/BuiltInPlugins/Toolbox/Libs/Framework/Util/getTestVariation.spec.lua @@ -0,0 +1,41 @@ +return function() + local getTestVariation = require(script.Parent.getTestVariation) + local VARIATION_NAME_CONTROL = "Control" + + local function createTestService(result) + local DebugABService = {} + + function DebugABService:GetVariant(testName) + return result + end + + return DebugABService + end + + it("should expect some test name", function() + expect(function() + getTestVariation() + end).to.throw() + end) + + it("should return zero when in the control group", function() + local debugABTestService = createTestService(VARIATION_NAME_CONTROL) + local variation = getTestVariation("AllUsers.Studio.ShouldShowZero", debugABTestService) + expect(variation).to.equal(0) + end) + + it("should return the number of the variation when not in the control group", function() + local debugABTestService = createTestService("Variation1") + local variation = getTestVariation("AllUsers.Studio.ShouldShowNumber", debugABTestService) + expect(variation).to.equal(1) + + -- there aren't any variations past 8, but it should work with any number + debugABTestService = createTestService("Variation23") + variation = getTestVariation("AllUsers.Studio.ShouldShowNumber", debugABTestService) + expect(variation).to.equal(23) + + debugABTestService = createTestService("Variation456") + variation = getTestVariation("AllUsers.Studio.ShouldShowNumber", debugABTestService) + expect(variation).to.equal(456) + end) +end \ No newline at end of file diff --git a/BuiltInPlugins/Toolbox/Libs/UILibrary/_internal/Utils/Symbol.spec.lua b/BuiltInPlugins/Toolbox/Libs/UILibrary/_internal/Utils/Symbol.spec.lua index cde9be065b..f3312055c9 100644 --- a/BuiltInPlugins/Toolbox/Libs/UILibrary/_internal/Utils/Symbol.spec.lua +++ b/BuiltInPlugins/Toolbox/Libs/UILibrary/_internal/Utils/Symbol.spec.lua @@ -11,7 +11,7 @@ return function() it("should coerce to the given name", function() local symbol = Symbol.named("foo") - expect(tostring(symbol):find("foo")).to.be.ok() + expect(tostring(symbol):match("foo")).to.be.ok() end) it("should be unique when constructed", function() @@ -42,4 +42,4 @@ return function() expect(symbolA).never.to.equal(symbolB) end) end) -end \ No newline at end of file +end diff --git a/BuiltInStandalonePlugins/CollisionGroupsEditor/modules/UILibrary/_internal/Utils/Symbol.spec.lua b/BuiltInStandalonePlugins/CollisionGroupsEditor/modules/UILibrary/_internal/Utils/Symbol.spec.lua index cde9be065b..f3312055c9 100644 --- a/BuiltInStandalonePlugins/CollisionGroupsEditor/modules/UILibrary/_internal/Utils/Symbol.spec.lua +++ b/BuiltInStandalonePlugins/CollisionGroupsEditor/modules/UILibrary/_internal/Utils/Symbol.spec.lua @@ -11,7 +11,7 @@ return function() it("should coerce to the given name", function() local symbol = Symbol.named("foo") - expect(tostring(symbol):find("foo")).to.be.ok() + expect(tostring(symbol):match("foo")).to.be.ok() end) it("should be unique when constructed", function() @@ -42,4 +42,4 @@ return function() expect(symbolA).never.to.equal(symbolB) end) end) -end \ No newline at end of file +end diff --git a/BuiltInStandalonePlugins/PluginManagement/Packages/Framework/ContextServices/Theme.spec.lua b/BuiltInStandalonePlugins/PluginManagement/Packages/Framework/ContextServices/Theme.spec.lua index 22377dc00b..1547fb3c11 100644 --- a/BuiltInStandalonePlugins/PluginManagement/Packages/Framework/ContextServices/Theme.spec.lua +++ b/BuiltInStandalonePlugins/PluginManagement/Packages/Framework/ContextServices/Theme.spec.lua @@ -11,6 +11,13 @@ return function() local StyleTable = Util.StyleTable local StyleModifier = Util.StyleModifier local Signal = Util.Signal + local FlagsList = Util.Flags.new({ + FFlagRefactorDevFrameworkTheme = {"RefactorDevFrameworkTheme"}, + }) + + if FlagsList:get("FFlagRefactorDevFrameworkTheme") then + return + end local function createTestThemedComponent(render) local TestThemedComponent = Roact.PureComponent:extend("TestThemedComponent") diff --git a/BuiltInStandalonePlugins/PluginManagement/Packages/Framework/ContextServices/UILibraryWrapper.spec.lua b/BuiltInStandalonePlugins/PluginManagement/Packages/Framework/ContextServices/UILibraryWrapper.spec.lua index 0823cb2280..d4bbb673db 100644 --- a/BuiltInStandalonePlugins/PluginManagement/Packages/Framework/ContextServices/UILibraryWrapper.spec.lua +++ b/BuiltInStandalonePlugins/PluginManagement/Packages/Framework/ContextServices/UILibraryWrapper.spec.lua @@ -2,8 +2,10 @@ return function() local Framework = script.Parent.Parent local Util = require(Framework.Util) + local Signal = Util.Signal local FlagsList = Util.Flags.new({ FFlagStudioDevFrameworkPackage = {"StudioDevFrameworkPackage"}, + FFlagRefactorDevFrameworkTheme = {"RefactorDevFrameworkTheme"}, }) local isUsedAsPackage = require(Framework.Util.isUsedAsPackage) @@ -18,6 +20,7 @@ return function() local Focus = require(Framework.ContextServices.Focus) local Theme = require(Framework.ContextServices.Theme) local UILibraryWrapper = require(script.Parent.UILibraryWrapper) + local StudioTheme = require(Framework.Style.Themes.StudioTheme) local WrapperStub = Roact.PureComponent:extend("UILibraryWrapper") @@ -29,12 +32,26 @@ return function() Wrapper = WrapperStub } + local function createThemeMock() + + if FlagsList:get("FFlagRefactorDevFrameworkTheme") then + return StudioTheme.mock() + else + local createStyles = function() + return {} + end + local getTheme = function() + return "Light" + end + local themeChanged = Signal.new() + return Theme.mock(createStyles, getTheme, themeChanged) + end + end + it("should expect a Plugin ContextItem provided above", function() local focus = Focus.new(Instance.new("ScreenGui")) local wrapper = UILibraryWrapper.new(UILibraryStub) - local theme = Theme.new(function() - return {} - end) + local theme = createThemeMock() function theme:getUILibraryTheme() return {} end @@ -55,9 +72,7 @@ return function() it("should expect a Focus ContextItem provided above", function() local plugin = Plugin.new({}) local wrapper = UILibraryWrapper.new(UILibraryStub) - local theme = Theme.new(function() - return {} - end) + local theme = createThemeMock() function theme:getUILibraryTheme() return {} end @@ -97,9 +112,7 @@ return function() local plugin = Plugin.new({}) local focus = Focus.new(Instance.new("ScreenGui")) local wrapper = UILibraryWrapper.new(UILibraryStub) - local theme = Theme.new(function() - return {} - end) + local theme = createThemeMock() local element = provide({ plugin, @@ -119,9 +132,7 @@ return function() local plugin = Plugin.new({}) local focus = Focus.new(Instance.new("ScreenGui")) local wrapper = UILibraryWrapper.new(UILibraryStub) - local theme = Theme.new(function() - return {} - end) + local theme = createThemeMock() function theme:getUILibraryTheme() return {} end diff --git a/BuiltInStandalonePlugins/PluginManagement/Packages/Framework/StudioUI/TitledFrame/test.spec.lua b/BuiltInStandalonePlugins/PluginManagement/Packages/Framework/StudioUI/TitledFrame/test.spec.lua index cff37efea8..f013a391bc 100644 --- a/BuiltInStandalonePlugins/PluginManagement/Packages/Framework/StudioUI/TitledFrame/test.spec.lua +++ b/BuiltInStandalonePlugins/PluginManagement/Packages/Framework/StudioUI/TitledFrame/test.spec.lua @@ -17,7 +17,7 @@ return function() local function createTestTitledFrame(children, container) local theme if FlagsList:get("FFlagRefactorDevFrameworkTheme") then - theme = StudioTheme.new() + theme = StudioTheme.mock() else theme = Theme.new(function() return { diff --git a/BuiltInStandalonePlugins/PluginManagement/Packages/Framework/StudioUI/createPluginWidget.lua b/BuiltInStandalonePlugins/PluginManagement/Packages/Framework/StudioUI/createPluginWidget.lua index 85620982d3..b9dd6d3759 100644 --- a/BuiltInStandalonePlugins/PluginManagement/Packages/Framework/StudioUI/createPluginWidget.lua +++ b/BuiltInStandalonePlugins/PluginManagement/Packages/Framework/StudioUI/createPluginWidget.lua @@ -9,10 +9,12 @@ ]] game:DefineFastFlag("FixDevFrameworkDockWidgetRestore", false) -game:DefineFastFlag("DevFrameworkPluginWidgetEnabledEvent", false) +game:DefineFastFlag("DevFrameworkPluginWidgetEnabledEvent2", false) +game:DefineFastFlag("DevFrameworkPluginWidgetUseSiblingZIndex", false) local FFlagFixDevFrameworkDockWidgetRestore = game:GetFastFlag("FixDevFrameworkDockWidgetRestore") -local FFlagDevFrameworkPluginWidgetEnabledEvent = game:GetFastFlag("DevFrameworkPluginWidgetEnabledEvent") +local FFlagDevFrameworkPluginWidgetEnabledEvent2 = game:GetFastFlag("DevFrameworkPluginWidgetEnabledEvent2") +local FFlagDevFrameworkPluginWidgetUseSiblingZIndex = game:GetFastFlag("DevFrameworkPluginWidgetUseSiblingZIndex") local Framework = script.Parent.Parent local Roact = require(Framework.Parent.Roact) @@ -31,7 +33,11 @@ local function createPluginWidget(componentName, createWidgetFunc) local widget = createWidgetFunc(props) widget.Name = title or "" - widget.ZIndexBehavior = props.ZIndexBehavior or Enum.ZIndexBehavior.Global + if FFlagDevFrameworkPluginWidgetUseSiblingZIndex then + widget.ZIndexBehavior = props.ZIndexBehavior or Enum.ZIndexBehavior.Sibling + else + widget.ZIndexBehavior = props.ZIndexBehavior or Enum.ZIndexBehavior.Global + end if widget:IsA("PluginGui") then widget:BindToClose(onClose) @@ -67,12 +73,13 @@ local function createPluginWidget(componentName, createWidgetFunc) end end - if FFlagDevFrameworkPluginWidgetEnabledEvent then + if FFlagDevFrameworkPluginWidgetEnabledEvent2 then -- Connect to enabled changing *after* restore -- Otherwise users of this will get 2 enabled changes: one from the onRestore, and the same from Roact.Change.Enabled self.widgetEnabledChangedConnection = widget:GetPropertyChangedSignal("Enabled"):Connect(function() - if self.props[Roact.Change.Enabled] then - self.props[Roact.Change.Enabled](self.widget) + local callback = self.props[Roact.Change.Enabled] + if callback and self.widget and self.widget.Enabled ~= self.props.Enabled then + callback(self.widget) end end) end diff --git a/BuiltInStandalonePlugins/PluginManagement/Packages/Framework/Style/Stylizer.lua b/BuiltInStandalonePlugins/PluginManagement/Packages/Framework/Style/Stylizer.lua index 47a60c5663..228c4b165a 100644 --- a/BuiltInStandalonePlugins/PluginManagement/Packages/Framework/Style/Stylizer.lua +++ b/BuiltInStandalonePlugins/PluginManagement/Packages/Framework/Style/Stylizer.lua @@ -1,3 +1,5 @@ +--!nocheck + --[[ Wraps theme styles and update logic into a ContextItem. @@ -318,4 +320,4 @@ function Stylizer.mock(t, themeProps, callback) return self end -return Stylizer \ No newline at end of file +return Stylizer diff --git a/BuiltInStandalonePlugins/PluginManagement/Packages/Framework/Style/getRawComponentStyle.lua b/BuiltInStandalonePlugins/PluginManagement/Packages/Framework/Style/getRawComponentStyle.lua index 7e659ca0c0..5c76de757c 100644 --- a/BuiltInStandalonePlugins/PluginManagement/Packages/Framework/Style/getRawComponentStyle.lua +++ b/BuiltInStandalonePlugins/PluginManagement/Packages/Framework/Style/getRawComponentStyle.lua @@ -9,6 +9,7 @@ local UIFolderData = require(Framework.UI.UIFolderData) return function(componentName) local componentData = UIFolderData[componentName] or StudioUIFolderData[componentName] local result + if componentData.style then result = require(componentData.style) end diff --git a/BuiltInStandalonePlugins/PluginManagement/Packages/Framework/TestHelpers/provideMockContext.lua b/BuiltInStandalonePlugins/PluginManagement/Packages/Framework/TestHelpers/provideMockContext.lua index 04cc85664e..10633a0dc7 100644 --- a/BuiltInStandalonePlugins/PluginManagement/Packages/Framework/TestHelpers/provideMockContext.lua +++ b/BuiltInStandalonePlugins/PluginManagement/Packages/Framework/TestHelpers/provideMockContext.lua @@ -91,7 +91,7 @@ return function(contextItemsList, children) -- Theme local theme if FlagsList:get("FFlagRefactorDevFrameworkTheme") then - theme = StudioTheme.new() + theme = StudioTheme.mock() else theme = ContextServices.Theme.mock(function(theme, getColor) return { diff --git a/BuiltInStandalonePlugins/PluginManagement/Packages/Framework/UI/Box/test.spec.lua b/BuiltInStandalonePlugins/PluginManagement/Packages/Framework/UI/Box/test.spec.lua index 27baa93e01..edc3b7c984 100644 --- a/BuiltInStandalonePlugins/PluginManagement/Packages/Framework/UI/Box/test.spec.lua +++ b/BuiltInStandalonePlugins/PluginManagement/Packages/Framework/UI/Box/test.spec.lua @@ -23,7 +23,7 @@ return function() else local theme if FlagsList:get("FFlagRefactorDevFrameworkTheme") then - theme = StudioTheme.new() + theme = StudioTheme.mock() else theme = Theme.new(function() return { diff --git a/BuiltInStandalonePlugins/PluginManagement/Packages/Framework/UI/Button/test.spec.lua b/BuiltInStandalonePlugins/PluginManagement/Packages/Framework/UI/Button/test.spec.lua index 3ffc122f23..cafabfaa40 100644 --- a/BuiltInStandalonePlugins/PluginManagement/Packages/Framework/UI/Button/test.spec.lua +++ b/BuiltInStandalonePlugins/PluginManagement/Packages/Framework/UI/Button/test.spec.lua @@ -18,7 +18,7 @@ return function() local function createTestButton(props, children) local theme if FlagsList:get("FFlagRefactorDevFrameworkTheme") then - theme = StudioTheme.new() + theme = StudioTheme.mock() else theme = Theme.new(function() return { diff --git a/BuiltInStandalonePlugins/PluginManagement/Packages/Framework/UI/Container/test.spec.lua b/BuiltInStandalonePlugins/PluginManagement/Packages/Framework/UI/Container/test.spec.lua index 21e7c8874a..1417100db0 100644 --- a/BuiltInStandalonePlugins/PluginManagement/Packages/Framework/UI/Container/test.spec.lua +++ b/BuiltInStandalonePlugins/PluginManagement/Packages/Framework/UI/Container/test.spec.lua @@ -18,7 +18,7 @@ return function() local function createTestContainer(props, children) local theme if FlagsList:get("FFlagRefactorDevFrameworkTheme") then - theme = StudioTheme.new() + theme = StudioTheme.mock() else theme = Theme.new(function() return { diff --git a/BuiltInStandalonePlugins/PluginManagement/Packages/Framework/UI/DropdownMenu.lua b/BuiltInStandalonePlugins/PluginManagement/Packages/Framework/UI/DropdownMenu.lua index 60f9fadd0c..b1e6b3c757 100644 --- a/BuiltInStandalonePlugins/PluginManagement/Packages/Framework/UI/DropdownMenu.lua +++ b/BuiltInStandalonePlugins/PluginManagement/Packages/Framework/UI/DropdownMenu.lua @@ -44,6 +44,7 @@ local TextLabel = require(UI.TextLabel) local FlagsList = Util.Flags.new({ FFlagRefactorDevFrameworkTheme = {"RefactorDevFrameworkTheme"}, + FFlagRefactorDevFrameworkContextItems = {"RefactorDevFrameworkContextItems"}, }) local DropdownMenu = Roact.PureComponent:extend("DropdownMenu") @@ -201,7 +202,7 @@ function DropdownMenu:renderMenu() local width = style.Width local offset = prioritize(style.Offset, Vector2.new(0, 0)) - local pluginGui = props.Focus:getTarget() + local pluginGui = FlagsList:get("FFlagRefactorDevFrameworkContextItems") and props.Focus:get() or props.Focus:getTarget() local menuPositionAndSize = self.getPositionAndSize(pluginGui, width, offset) local x = menuPositionAndSize.X diff --git a/BuiltInStandalonePlugins/PluginManagement/Packages/Framework/UI/FakeLoadingBar/test.spec.lua b/BuiltInStandalonePlugins/PluginManagement/Packages/Framework/UI/FakeLoadingBar/test.spec.lua index 4533d1b2a4..1306596adc 100644 --- a/BuiltInStandalonePlugins/PluginManagement/Packages/Framework/UI/FakeLoadingBar/test.spec.lua +++ b/BuiltInStandalonePlugins/PluginManagement/Packages/Framework/UI/FakeLoadingBar/test.spec.lua @@ -17,7 +17,7 @@ return function() local function createTestFakeLoadingBar(props, children) local theme if FlagsList:get("FFlagRefactorDevFrameworkTheme") then - theme = StudioTheme.new() + theme = StudioTheme.mock() else theme = Theme.new(function() return { diff --git a/BuiltInStandalonePlugins/PluginManagement/Packages/Framework/UI/Image/test.spec.lua b/BuiltInStandalonePlugins/PluginManagement/Packages/Framework/UI/Image/test.spec.lua index c5aa507a3a..6860be79dd 100644 --- a/BuiltInStandalonePlugins/PluginManagement/Packages/Framework/UI/Image/test.spec.lua +++ b/BuiltInStandalonePlugins/PluginManagement/Packages/Framework/UI/Image/test.spec.lua @@ -17,7 +17,7 @@ return function() local function createTestImageDecoration() local theme if FlagsList:get("FFlagRefactorDevFrameworkTheme") then - theme = StudioTheme.new() + theme = StudioTheme.mock() else theme = Theme.new(function() return { diff --git a/BuiltInStandalonePlugins/PluginManagement/Packages/Framework/UI/InstanceTreeView/InstanceTreeRow.lua b/BuiltInStandalonePlugins/PluginManagement/Packages/Framework/UI/InstanceTreeView/InstanceTreeRow.lua index 8000e57785..6110490a54 100644 --- a/BuiltInStandalonePlugins/PluginManagement/Packages/Framework/UI/InstanceTreeView/InstanceTreeRow.lua +++ b/BuiltInStandalonePlugins/PluginManagement/Packages/Framework/UI/InstanceTreeView/InstanceTreeRow.lua @@ -25,7 +25,7 @@ local TextLabel = require(UI.TextLabel) Typecheck.wrap(InstanceTreeRow, script) local function getClassIcon(instance) - local StudioService = game:GetService("StudioService") + local StudioService = game:GetService("StudioService") local className = instance.ClassName if instance.IsA then if instance:IsA("JointInstance") and className == "ManualWeld" or className == "ManualGlue" then @@ -73,9 +73,10 @@ function InstanceTreeRow:render() local arrowSize = style.Arrow.Size local padding = style.IconPadding local iconInfo = getClassIcon(item) - local iconSize = iconInfo.ImageRectSize.X + -- Default iconSize to (0, 0) as ImageRectSize is unavailable in Roblox CLI + local iconSize = iconInfo.ImageRectSize or Vector2.new() local labelOffset = indent + arrowSize + 2 * padding - local textOffset = iconSize + 3 * padding + local textOffset = iconSize.X + 3 * padding return Roact.createElement(Container, { Size = UDim2.new(1, -indent, 0, style.RowHeight), @@ -105,10 +106,10 @@ function InstanceTreeRow:render() Size = UDim2.new(1, -arrowSize, 1, 0), }, { Icon = Roact.createElement("ImageLabel", { - Size = UDim2.fromOffset(iconSize, iconInfo.ImageRectSize.Y), + Size = UDim2.fromOffset(iconSize.X, iconSize.Y), BackgroundTransparency = 1, Image = iconInfo.Image, - ImageRectSize = iconInfo.ImageRectSize, + ImageRectSize = iconSize, ImageRectOffset = iconInfo.ImageRectOffset, Position = UDim2.new(0, padding, 0.5, 0), AnchorPoint = Vector2.new(0, 0.5) diff --git a/BuiltInStandalonePlugins/PluginManagement/Packages/Framework/UI/LinkText/test.spec.lua b/BuiltInStandalonePlugins/PluginManagement/Packages/Framework/UI/LinkText/test.spec.lua index d72d172041..37cecf30b1 100644 --- a/BuiltInStandalonePlugins/PluginManagement/Packages/Framework/UI/LinkText/test.spec.lua +++ b/BuiltInStandalonePlugins/PluginManagement/Packages/Framework/UI/LinkText/test.spec.lua @@ -19,7 +19,7 @@ return function() local mouse = Mouse.new({}) local theme if FlagsList:get("FFlagRefactorDevFrameworkTheme") then - theme = StudioTheme.new() + theme = StudioTheme.mock() else theme = Theme.new(function() return { diff --git a/BuiltInStandalonePlugins/PluginManagement/Packages/Framework/UI/LoadingBar/test.spec.lua b/BuiltInStandalonePlugins/PluginManagement/Packages/Framework/UI/LoadingBar/test.spec.lua index ea437b136c..c898cde0f4 100644 --- a/BuiltInStandalonePlugins/PluginManagement/Packages/Framework/UI/LoadingBar/test.spec.lua +++ b/BuiltInStandalonePlugins/PluginManagement/Packages/Framework/UI/LoadingBar/test.spec.lua @@ -18,7 +18,7 @@ return function() local function createTestLoadingBar(props, children) local theme if FlagsList:get("FFlagRefactorDevFrameworkTheme") then - theme = StudioTheme.new() + theme = StudioTheme.mock() else theme = Theme.new(function() return { diff --git a/BuiltInStandalonePlugins/PluginManagement/Packages/Framework/UI/RadioButton/test.spec.lua b/BuiltInStandalonePlugins/PluginManagement/Packages/Framework/UI/RadioButton/test.spec.lua index 31e2c12c77..4e578229a9 100644 --- a/BuiltInStandalonePlugins/PluginManagement/Packages/Framework/UI/RadioButton/test.spec.lua +++ b/BuiltInStandalonePlugins/PluginManagement/Packages/Framework/UI/RadioButton/test.spec.lua @@ -23,7 +23,7 @@ return function() local function createTestRadioButton(props, children) local theme if FlagsList:get("FFlagRefactorDevFrameworkTheme") then - theme = StudioTheme.new() + theme = StudioTheme.mock() else theme = Theme.new(function() return { diff --git a/BuiltInStandalonePlugins/PluginManagement/Packages/Framework/UI/RoundBox/test.spec.lua b/BuiltInStandalonePlugins/PluginManagement/Packages/Framework/UI/RoundBox/test.spec.lua index 10b16f79a0..d9715cf163 100644 --- a/BuiltInStandalonePlugins/PluginManagement/Packages/Framework/UI/RoundBox/test.spec.lua +++ b/BuiltInStandalonePlugins/PluginManagement/Packages/Framework/UI/RoundBox/test.spec.lua @@ -17,7 +17,7 @@ return function() local function createTestRoundBoxDecoration() local theme if FlagsList:get("FFlagRefactorDevFrameworkTheme") then - theme = StudioTheme.new() + theme = StudioTheme.mock() else theme = Theme.new(function() return { diff --git a/BuiltInStandalonePlugins/PluginManagement/Packages/Framework/UI/TextLabel/test.spec.lua b/BuiltInStandalonePlugins/PluginManagement/Packages/Framework/UI/TextLabel/test.spec.lua index a403d8b7d3..5f739cb55f 100644 --- a/BuiltInStandalonePlugins/PluginManagement/Packages/Framework/UI/TextLabel/test.spec.lua +++ b/BuiltInStandalonePlugins/PluginManagement/Packages/Framework/UI/TextLabel/test.spec.lua @@ -17,7 +17,7 @@ return function() local function createTestTextLabelDecoration() local theme if FlagsList:get("FFlagRefactorDevFrameworkTheme") then - theme = StudioTheme.new() + theme = StudioTheme.mock() else theme = Theme.new(function() return { diff --git a/BuiltInStandalonePlugins/PluginManagement/Packages/Framework/UI/ToggleButton/test.spec.lua b/BuiltInStandalonePlugins/PluginManagement/Packages/Framework/UI/ToggleButton/test.spec.lua index 0d712bdf45..c4db80d801 100644 --- a/BuiltInStandalonePlugins/PluginManagement/Packages/Framework/UI/ToggleButton/test.spec.lua +++ b/BuiltInStandalonePlugins/PluginManagement/Packages/Framework/UI/ToggleButton/test.spec.lua @@ -27,7 +27,7 @@ return function() local mouse = Mouse.new({}) local theme if FlagsList:get("FFlagRefactorDevFrameworkTheme") then - theme = StudioTheme.new() + theme = StudioTheme.mock() else theme = Theme.new(function() return { diff --git a/BuiltInStandalonePlugins/PluginManagement/Packages/Framework/UI/Tooltip/test.spec.lua b/BuiltInStandalonePlugins/PluginManagement/Packages/Framework/UI/Tooltip/test.spec.lua index 984329d7eb..8f7a0a6fc9 100644 --- a/BuiltInStandalonePlugins/PluginManagement/Packages/Framework/UI/Tooltip/test.spec.lua +++ b/BuiltInStandalonePlugins/PluginManagement/Packages/Framework/UI/Tooltip/test.spec.lua @@ -25,7 +25,7 @@ return function() local focus = Focus.new(target) local theme if FlagsList:get("FFlagRefactorDevFrameworkTheme") then - theme = StudioTheme.new() + theme = StudioTheme.mock() else theme = Theme.new(function() return { diff --git a/BuiltInStandalonePlugins/PluginManagement/Packages/Framework/UI/TreeView/test.spec.lua b/BuiltInStandalonePlugins/PluginManagement/Packages/Framework/UI/TreeView/test.spec.lua index 953aef3483..8d06ee8a86 100644 --- a/BuiltInStandalonePlugins/PluginManagement/Packages/Framework/UI/TreeView/test.spec.lua +++ b/BuiltInStandalonePlugins/PluginManagement/Packages/Framework/UI/TreeView/test.spec.lua @@ -63,7 +63,7 @@ return function() local function createTreeView() local theme if FlagsList:get("FFlagRefactorDevFrameworkTheme") then - theme = StudioTheme.new() + theme = StudioTheme.mock() else theme = Theme.new(function() return { diff --git a/BuiltInStandalonePlugins/PluginManagement/Packages/Framework/Util.lua b/BuiltInStandalonePlugins/PluginManagement/Packages/Framework/Util.lua index 5454c404f1..6a3e12c089 100644 --- a/BuiltInStandalonePlugins/PluginManagement/Packages/Framework/Util.lua +++ b/BuiltInStandalonePlugins/PluginManagement/Packages/Framework/Util.lua @@ -19,6 +19,7 @@ return strict({ FitFrame = require(script.FitFrame), Flags = require(script.Flags), + getTestVariation = require(script.getTestVariation), Immutable = require(script.Immutable), LayoutOrderIterator = require(script.LayoutOrderIterator), Math = require(script.Math), diff --git a/BuiltInStandalonePlugins/PluginManagement/Packages/Framework/Util/CrossPluginCommunication.lua b/BuiltInStandalonePlugins/PluginManagement/Packages/Framework/Util/CrossPluginCommunication.lua index 37caa1bece..d3b82f5783 100644 --- a/BuiltInStandalonePlugins/PluginManagement/Packages/Framework/Util/CrossPluginCommunication.lua +++ b/BuiltInStandalonePlugins/PluginManagement/Packages/Framework/Util/CrossPluginCommunication.lua @@ -22,10 +22,12 @@ CrossPluginCommunication.__index = CrossPluginCommunication CrossPluginCommunication.BASE_FOLDER_NAME = "CrossPluginCommunication" -function CrossPluginCommunication.new(pluginNamespace) +function CrossPluginCommunication.new(pluginNamespace, hostService) assert(t.string(pluginNamespace), "pluginNamespace must be a string") + assert(t.optional(t.instance(pluginNamespace)), "hostService must be an instance if defined") local self = { + hostService = hostService or RobloxPluginGuiService, pluginNamespace = pluginNamespace } @@ -49,7 +51,7 @@ function CrossPluginCommunication:ensureFolderExists(parent, name) end function CrossPluginCommunication:getNamespaceFolder() - local base = self:ensureFolderExists(RobloxPluginGuiService, self.BASE_FOLDER_NAME) + local base = self:ensureFolderExists(self.hostService, self.BASE_FOLDER_NAME) return self:ensureFolderExists(base, self.pluginNamespace) end diff --git a/BuiltInStandalonePlugins/PluginManagement/Packages/Framework/Util/CrossPluginCommunication.spec.lua b/BuiltInStandalonePlugins/PluginManagement/Packages/Framework/Util/CrossPluginCommunication.spec.lua index 60338f2b0c..f3c1035f50 100644 --- a/BuiltInStandalonePlugins/PluginManagement/Packages/Framework/Util/CrossPluginCommunication.spec.lua +++ b/BuiltInStandalonePlugins/PluginManagement/Packages/Framework/Util/CrossPluginCommunication.spec.lua @@ -1,5 +1,3 @@ -local RobloxPluginGuiService = game:GetService("RobloxPluginGuiService") - local Framework = script.Parent.Parent local CrossPluginCommunication = require(Framework.Util.CrossPluginCommunication) @@ -8,8 +6,10 @@ return function() -- TODO DEVTOOLS-4397: Move cleanup to afterEach hook after the dependency structure is fixed + local hostService = workspace + local function createTestComms() - return CrossPluginCommunication.new(TEST_NAMESPACE) + return CrossPluginCommunication.new(TEST_NAMESPACE, hostService) end describe("Invoke", function() @@ -32,7 +32,7 @@ return function() comms:OnInvoke("blah", function() end) - local namespace = RobloxPluginGuiService:FindFirstChild(CrossPluginCommunication.BASE_FOLDER_NAME):FindFirstChild(TEST_NAMESPACE) + local namespace = hostService:FindFirstChild(CrossPluginCommunication.BASE_FOLDER_NAME):FindFirstChild(TEST_NAMESPACE) assert(namespace) diff --git a/BuiltInStandalonePlugins/PluginManagement/Packages/Framework/Util/Palette.spec.lua b/BuiltInStandalonePlugins/PluginManagement/Packages/Framework/Util/Palette.spec.lua index 8794c80717..02911b17c7 100644 --- a/BuiltInStandalonePlugins/PluginManagement/Packages/Framework/Util/Palette.spec.lua +++ b/BuiltInStandalonePlugins/PluginManagement/Packages/Framework/Util/Palette.spec.lua @@ -1,5 +1,16 @@ return function() - local Palette = require(script.Parent.Palette) + + local Util = script.Parent + local Flags = require(Util.Flags) + local FlagsList = Flags.new({ + FFlagRefactorDevFrameworkTheme = {"RefactorDevFrameworkTheme"}, + }) + + if FlagsList:get("FFlagRefactorDevFrameworkTheme") then + return + end + + local Palette = require(Util.Palette) it("should be a table of color StyleValues", function() expect(Palette).to.be.ok() diff --git a/BuiltInStandalonePlugins/PluginManagement/Packages/Framework/Util/Typecheck/t.lua b/BuiltInStandalonePlugins/PluginManagement/Packages/Framework/Util/Typecheck/t.lua index 44c8d50af8..045d7ac0dd 100644 --- a/BuiltInStandalonePlugins/PluginManagement/Packages/Framework/Util/Typecheck/t.lua +++ b/BuiltInStandalonePlugins/PluginManagement/Packages/Framework/Util/Typecheck/t.lua @@ -1,3 +1,5 @@ +--!nocheck + --[[ DeveloperFramework uses t from Osyris https://github.com/osyrisrblx/t diff --git a/BuiltInStandalonePlugins/PluginManagement/Packages/Framework/Util/getTestVariation.lua b/BuiltInStandalonePlugins/PluginManagement/Packages/Framework/Util/getTestVariation.lua new file mode 100644 index 0000000000..d09873db1d --- /dev/null +++ b/BuiltInStandalonePlugins/PluginManagement/Packages/Framework/Util/getTestVariation.lua @@ -0,0 +1,30 @@ +-- This function hides ABTestService away and provides a quick interface for getting a test's variation. +-- Should the requested test not exist, it will return the Control group. + +local ABTestService = game:GetService("ABTestService") + +local AB_TEST_GROUP_CONTROL = "Control" + +-- abTestName : (string) the name of test. ex) "AllUsers.Studio.ExampleTestName" +-- abTestService : (table, optional) an optional override for ABTestService +-- RETURNS : (int) the variation that the user is in. 0 = Control +return function(abTestName, abTestService) + assert(type(abTestName) == "string", "Expected abTestName to be a string") + if abTestService then + assert(type(abTestService.GetVariant) == "function", "Expected the abTestService object to have a GetVariant member function") + else + abTestService = ABTestService + end + + local variation = abTestService:GetVariant(abTestName) + if variation == AB_TEST_GROUP_CONTROL then + return 0 + else + local _, _, variationNumber = string.find(variation, "Variation(%d+)") + if not variationNumber then + return 0 + end + + return tonumber(variationNumber) + end +end diff --git a/BuiltInStandalonePlugins/PluginManagement/Packages/Framework/Util/getTestVariation.spec.lua b/BuiltInStandalonePlugins/PluginManagement/Packages/Framework/Util/getTestVariation.spec.lua new file mode 100644 index 0000000000..5febceee91 --- /dev/null +++ b/BuiltInStandalonePlugins/PluginManagement/Packages/Framework/Util/getTestVariation.spec.lua @@ -0,0 +1,41 @@ +return function() + local getTestVariation = require(script.Parent.getTestVariation) + local VARIATION_NAME_CONTROL = "Control" + + local function createTestService(result) + local DebugABService = {} + + function DebugABService:GetVariant(testName) + return result + end + + return DebugABService + end + + it("should expect some test name", function() + expect(function() + getTestVariation() + end).to.throw() + end) + + it("should return zero when in the control group", function() + local debugABTestService = createTestService(VARIATION_NAME_CONTROL) + local variation = getTestVariation("AllUsers.Studio.ShouldShowZero", debugABTestService) + expect(variation).to.equal(0) + end) + + it("should return the number of the variation when not in the control group", function() + local debugABTestService = createTestService("Variation1") + local variation = getTestVariation("AllUsers.Studio.ShouldShowNumber", debugABTestService) + expect(variation).to.equal(1) + + -- there aren't any variations past 8, but it should work with any number + debugABTestService = createTestService("Variation23") + variation = getTestVariation("AllUsers.Studio.ShouldShowNumber", debugABTestService) + expect(variation).to.equal(23) + + debugABTestService = createTestService("Variation456") + variation = getTestVariation("AllUsers.Studio.ShouldShowNumber", debugABTestService) + expect(variation).to.equal(456) + end) +end \ No newline at end of file diff --git a/BuiltInStandalonePlugins/PluginManagement/Packages/UILibrary/_internal/Utils/Symbol.spec.lua b/BuiltInStandalonePlugins/PluginManagement/Packages/UILibrary/_internal/Utils/Symbol.spec.lua index cde9be065b..f3312055c9 100644 --- a/BuiltInStandalonePlugins/PluginManagement/Packages/UILibrary/_internal/Utils/Symbol.spec.lua +++ b/BuiltInStandalonePlugins/PluginManagement/Packages/UILibrary/_internal/Utils/Symbol.spec.lua @@ -11,7 +11,7 @@ return function() it("should coerce to the given name", function() local symbol = Symbol.named("foo") - expect(tostring(symbol):find("foo")).to.be.ok() + expect(tostring(symbol):match("foo")).to.be.ok() end) it("should be unique when constructed", function() @@ -42,4 +42,4 @@ return function() expect(symbolA).never.to.equal(symbolB) end) end) -end \ No newline at end of file +end diff --git a/BuiltInStandalonePlugins/TransformDragger/Precision.server.lua b/BuiltInStandalonePlugins/TransformDragger/Precision.server.lua index 9928ac5b33..5188e0f15a 100644 --- a/BuiltInStandalonePlugins/TransformDragger/Precision.server.lua +++ b/BuiltInStandalonePlugins/TransformDragger/Precision.server.lua @@ -468,8 +468,20 @@ plugin:OnInvoke("buttonClicked", function(payload) end end) +if game:GetFastFlag("EnableClarifiedDraggerBehaviorUI") then + -- plugin:Deactivate() can't be called from main.server.lua where we connect to + -- StudioService.PromptTransformPluginCheckEnable, it doesnt have the correct + -- instance of plugin. This is why we have this intermediary event "Disable" that + -- gets caught by the correct instance and then calls deactivate properly + plugin:OnInvoke("Disable", function() + plugin:Deactivate() + end) +end + plugin.Deactivation:connect(function() - if on and Off then Off() end + if on and Off then + Off() + end end) --------------------MATH STUFFS------------------- diff --git a/BuiltInStandalonePlugins/TransformDragger/Standalone/main.server.lua b/BuiltInStandalonePlugins/TransformDragger/Standalone/main.server.lua index eb80a384f7..b981952008 100644 --- a/BuiltInStandalonePlugins/TransformDragger/Standalone/main.server.lua +++ b/BuiltInStandalonePlugins/TransformDragger/Standalone/main.server.lua @@ -1,3 +1,4 @@ +local StudioService = game:GetService("StudioService") local plugin, settings = plugin, settings @@ -11,3 +12,15 @@ end) plugin:OnInvoke("setActive", function(payloadString) toolbarbutton:SetActive(payloadString == "true") end) + +if game:GetFastFlag("EnableClarifiedDraggerBehaviorUI") then + -- Transform tool does not respect physical constraints, it should be disabled + -- when the dragger mode is set to physical + toolbarbutton.Enabled = not StudioService.DraggerSolveConstraints + StudioService.PromptTransformPluginCheckEnable:Connect(function() + if StudioService.DraggerSolveConstraints then + plugin:Invoke("Disable") + end + toolbarbutton.Enabled = not StudioService.DraggerSolveConstraints + end) +end \ No newline at end of file diff --git a/LuaPackages/AppTempCommon/LuaApp/Style/Colors.lua b/LuaPackages/AppTempCommon/LuaApp/Style/Colors.lua index d799c7b970..8019bc4b32 100644 --- a/LuaPackages/AppTempCommon/LuaApp/Style/Colors.lua +++ b/LuaPackages/AppTempCommon/LuaApp/Style/Colors.lua @@ -18,6 +18,7 @@ local Colors = { Ash = Color3.fromRGB(234, 237, 239), Chalk = Color3.fromRGB(216, 219, 222), Smoke = Color3.fromRGB(96, 97, 98), + XboxBlue = Color3.fromRGB(17, 139, 211), } return Colors \ No newline at end of file diff --git a/LuaPackages/AppTempCommon/LuaApp/Style/Themes/DarkTheme.lua b/LuaPackages/AppTempCommon/LuaApp/Style/Themes/DarkTheme.lua index 622866c860..9f18d30c19 100644 --- a/LuaPackages/AppTempCommon/LuaApp/Style/Themes/DarkTheme.lua +++ b/LuaPackages/AppTempCommon/LuaApp/Style/Themes/DarkTheme.lua @@ -157,6 +157,11 @@ local theme = { Color = Colors.Flint, Transparency = 0, }, + + SelectionCursor = { + Color = Colors.White, + Transparency = 0, + }, } return theme \ No newline at end of file diff --git a/LuaPackages/AppTempCommon/LuaApp/Style/Themes/LightTheme.lua b/LuaPackages/AppTempCommon/LuaApp/Style/Themes/LightTheme.lua index 264811104c..8d169edac5 100644 --- a/LuaPackages/AppTempCommon/LuaApp/Style/Themes/LightTheme.lua +++ b/LuaPackages/AppTempCommon/LuaApp/Style/Themes/LightTheme.lua @@ -163,6 +163,11 @@ local theme = { Color = Colors.White, Transparency = 0, }, + + SelectionCursor = { + Color = Colors.XboxBlue, + Transparency = 0, + }, } return theme \ No newline at end of file diff --git a/LuaPackages/Regulations/ScreenTime/Constants.lua b/LuaPackages/Regulations/ScreenTime/Constants.lua new file mode 100644 index 0000000000..2a85346f18 --- /dev/null +++ b/LuaPackages/Regulations/ScreenTime/Constants.lua @@ -0,0 +1,6 @@ +local Constants = { + SIGNALR_NAMESPACE = "TimedEntertainmentAllowanceNotifications", + SIGNALR_TYPE_NEW_INSTRUCTION = "NewInstruction", +} + +return Constants diff --git a/LuaPackages/Regulations/ScreenTime/GetFFlagScreenTime.lua b/LuaPackages/Regulations/ScreenTime/GetFFlagScreenTime.lua new file mode 100644 index 0000000000..23bdccdcb1 --- /dev/null +++ b/LuaPackages/Regulations/ScreenTime/GetFFlagScreenTime.lua @@ -0,0 +1,5 @@ +game:DefineFastFlag("LuaEnableScreenTime", false) + +return function() + return game:GetFastFlag("LuaEnableScreenTime") +end diff --git a/LuaPackages/Regulations/ScreenTime/HttpRequests.lua b/LuaPackages/Regulations/ScreenTime/HttpRequests.lua new file mode 100644 index 0000000000..158afe4156 --- /dev/null +++ b/LuaPackages/Regulations/ScreenTime/HttpRequests.lua @@ -0,0 +1,183 @@ +--[[ + Provides HTTP request methods for ScreenTime feature. +]] + +local CorePackages = game:GetService("CorePackages") +local Url = require(CorePackages.AppTempCommon.LuaApp.Http.Url) +local UrlBuilder = require(CorePackages.Packages.UrlBuilder).UrlBuilder +local Logging = require(CorePackages.Logging) +local ArgCheck = require(CorePackages.ArgCheck) + +local TEA_END_POINTS = { + GET_INSTRUCTIONS = "timed-entertainment-allowance/v1/instructions", + REPORT_EXECUTION = "timed-entertainment-allowance/v1/reportExecute", +} + +local RESPONSE_FORMATS = { + GET_INSTRUCTIONS = { + errorCode = "number", + instructions = "table" + }, + INSTRUCTION = { + type = "number", + instructionName = "string", + serialId = "string", + title = "string", + message = "string", + url = "string", + modalType = "number", + data = "string" + }, + REPORT_EXECUTION = { + errorCode = "number", + }, +} + +local TAG = "HttpRequests" + +local getInstructionsUrl = UrlBuilder.new({ + base = Url.APIS_URL, + path = TEA_END_POINTS.GET_INSTRUCTIONS +})() + +local reportExecutionUrl = UrlBuilder.new({ + base = Url.APIS_URL, + path = TEA_END_POINTS.REPORT_EXECUTION +})() + +--[[ + A helper function to check object table have the required fields and fields' + type specified in format table. + It will throw exceptions, so must be encapsulated by pcall. +]] +local function checkFormat(format, object) + for key, typeString in pairs(format) do + assert(object[key] ~= nil, "Missing key") + assert(type(object[key]) == typeString, "Wrong type") + end +end + +local HttpRequests = { + httpService = nil, +} + +--[[ + Create a new HttpRequests object. + + @param httpService: Pass in HttpService from game:GetService("HttpService") +]] +function HttpRequests:new(httpService) + ArgCheck.isNotNil(httpService, "httpService") + local obj = { + httpService = httpService, + } + setmetatable(obj, self) + self.__index = self + return obj +end + +--[[ + Query TEA endpoint to get the instructions to execute. + + @param callback: it should be non-nil with signature: + callback(success, unauthorized, instructions) + success (boolean): indicate whether the query is successful. + unauthorized (boolean): if success is false, indicate whether the + failure is due to authorization issue. + instructions (table): if success is true, this is the result + (associative array) of tables following + RESPONSE_FORMATS.INSTRUCTION format. +]] +function HttpRequests:getInstructions(callback) + ArgCheck.isNotNil(self.httpService, "httpService") + ArgCheck.isNotNil(callback, "callback") + local httpRequest = self.httpService:RequestInternal({ + Url = getInstructionsUrl, + Method = "GET", + }) + httpRequest:Start(function(reqSuccess, reqResponse) + local success + local err + local unauthorized = false + local instructions = {} + if not reqSuccess then + success = false + err = "Connection error" + elseif reqResponse.StatusCode == 401 then + success = false + unauthorized = true + err = "Unauthorized" + elseif reqResponse.StatusCode < 200 or reqResponse.StatusCode >= 400 then + success = false + err = "Status code: " .. reqResponse.StatusCode + else + -- reqSuccess == true and StatusCode >= 200 and StatusCode < 400 + success, err = pcall(function() + local json = self.httpService:JSONDecode(reqResponse.Body) + checkFormat(RESPONSE_FORMATS.GET_INSTRUCTIONS, json) + assert(json.errorCode == 0, "Error code is not 0") + for i, instruction in ipairs(json.instructions) do + checkFormat(RESPONSE_FORMATS.INSTRUCTION, instruction) + end + instructions = json.instructions + end) + end + if not success then + Logging.warn(TAG .. " getInstructions failed: " .. getInstructionsUrl .. ", ".. err) + end + callback(success, unauthorized, instructions) + end) +end + +--[[ + Tell TEA endpoint that an instruction has been executed. + + @param instructionName: from RESPONSE_FORMATS.INSTRUCTION + @param serialId: from RESPONSE_FORMATS.INSTRUCTION + @param callback: can be nil, signature: callback(success) + success (boolean): indicate whether the http request is successful. +]] +function HttpRequests:reportExecution(instructionName, serialId, callback) + ArgCheck.isNotNil(self.httpService, "httpService") + -- ISO 8601, Example: 2020-06-04T04:44:09Z + local formattedTime = os.date("%Y-%m-%dT%H:%M:%SZ") + local payload = self.httpService:JSONEncode({ + instructionName = instructionName, + serialId = serialId, + execTime = formattedTime, + }) + local httpRequest = self.httpService:RequestInternal({ + Url = reportExecutionUrl, + Method = "POST", + Headers = { + ["Content-Type"] = "application/json", + }, + Body = payload, + }) + httpRequest:Start(function(reqSuccess, reqResponse) + local success + local err + if not reqSuccess then + success = false + err = "Connection error" + elseif reqResponse.StatusCode < 200 and reqResponse.StatusCode >= 400 then + success = false + err = "Status code: " .. reqResponse.StatusCode + else + -- reqSuccess == true and StatusCode >= 200 and StatusCode < 400 + success, err = pcall(function() + local json = self.httpService:JSONDecode(reqResponse.Body) + checkFormat(RESPONSE_FORMATS.REPORT_EXECUTION, json) + assert(json.errorCode == 0, "Error code is not 0") + end) + end + if not success then + Logging.warn(TAG .. " reportExecution failed: " .. reportExecutionUrl .. ", ".. err) + end + if callback ~= nil then + callback(success) + end + end) +end + +return HttpRequests diff --git a/LuaPackages/Regulations/ScreenTime/HttpRequests.spec.lua b/LuaPackages/Regulations/ScreenTime/HttpRequests.spec.lua new file mode 100644 index 0000000000..be0a6eaa0b --- /dev/null +++ b/LuaPackages/Regulations/ScreenTime/HttpRequests.spec.lua @@ -0,0 +1,250 @@ +local HttpRequests = require(script.Parent.HttpRequests) + +function createMockHttpService(success, statusCode, errorCode, instructions) + local testBody = "test-body" + return { + RequestInternal = function(self, params) + return { + Start = function(self, callback) + local response = { + Body = testBody, + StatusCode = statusCode, + } + callback(success, response) + end + } + end, + JSONDecode = function(self, body) + assert(body == testBody) + return { + errorCode = errorCode, + instructions = instructions, + } + end, + JSONEncode = function(self, param) + return testBody + end, + } +end + +return function() + local testInstructions = {{ + type = 3, + instructionName = "name", + serialId = "id", + title = "title", + message = "message", + url = "url", + modalType = 0, + data = "", + }} + + describe("getInstructions()", function() + it("should correctly callback when succeeded", function() + local httpRequests = HttpRequests:new(createMockHttpService(true, 200, 0, testInstructions)) + local called = false + function callback(success, unauthorized, instructions) + expect(success).to.equal(true) + expect(unauthorized).to.equal(false) + expect(instructions).to.equal(testInstructions) + called = true; + end + httpRequests:getInstructions(callback) + expect(called).to.equal(true) + end) + + it("should throw when callback is nil", function() + local httpRequests = HttpRequests:new(createMockHttpService(false)) + success, err = pcall(function() + httpRequests:getInstructions() + end) + expect(success).to.equal(false) + end) + + it("should throw when not get from new", function() + called = false + function callback(success, unauthorized, instructions) + called = true; + end + success, err = pcall(function() + HttpRequests:getInstructions(callback) + end) + expect(success).to.equal(false) + expect(called).to.equal(false) + end) + + it("should correctly callback when connection error", function() + local httpRequests = HttpRequests:new(createMockHttpService(false)) + local called = false + function callback(success, unauthorized, instructions) + expect(success).to.equal(false) + called = true; + end + httpRequests:getInstructions(callback) + expect(called).to.equal(true) + end) + + it("should correctly callback when 401", function() + local httpRequests = HttpRequests:new(createMockHttpService(true, 401)) + local called = false + function callback(success, unauthorized, instructions) + expect(success).to.equal(false) + expect(unauthorized).to.equal(true) + called = true; + end + httpRequests:getInstructions(callback) + expect(called).to.equal(true) + end) + + it("should correctly callback when 412", function() + local httpRequests = HttpRequests:new(createMockHttpService(true, 412)) + local called = false + function callback(success, unauthorized, instructions) + expect(success).to.equal(false) + expect(unauthorized).to.equal(false) + called = true; + end + httpRequests:getInstructions(callback) + expect(called).to.equal(true) + end) + + it("should correctly callback when errorCode", function() + local httpRequests = HttpRequests:new(createMockHttpService(true, 200, 1)) + local called = false + function callback(success, unauthorized, instructions) + expect(success).to.equal(false) + expect(unauthorized).to.equal(false) + called = true; + end + httpRequests:getInstructions(callback) + expect(called).to.equal(true) + end) + + it("should correctly callback when decoding failed", function() + local httpService = createMockHttpService(true, 200, 0) + httpService.JSONDecode = function(self, body) + assert(false) + end + local httpRequests = HttpRequests:new(httpService) + local called = false + function callback(success, unauthorized, instructions) + expect(success).to.equal(false) + expect(unauthorized).to.equal(false) + called = true; + end + httpRequests:getInstructions(callback) + expect(called).to.equal(true) + end) + + it("should correctly callback when wrong response json format", function() + local httpService = createMockHttpService(true, 200, 0) + httpService.JSONDecode = function(self, body) + return { + errorCode = 0, + } + end + local httpRequests = HttpRequests:new(httpService) + local called = false + function callback(success, unauthorized, instructions) + expect(success).to.equal(false) + expect(unauthorized).to.equal(false) + called = true; + end + httpRequests:getInstructions(callback) + expect(called).to.equal(true) + end) + end) + + describe("reportExecution()", function() + it("should correctly callback when succeeded", function() + local httpRequests = HttpRequests:new(createMockHttpService(true, 200, 0)) + local called = false + function callback(success) + expect(success).to.equal(true) + called = true; + end + httpRequests:reportExecution("a", "b", callback) + expect(called).to.equal(true) + end) + + it("should throw when not get from new", function() + called = false + function callback(success) + called = true; + end + success, err = pcall(function() + HttpRequests:reportExecution("a", "b", callback) + end) + expect(success).to.equal(false) + expect(called).to.equal(false) + end) + + it("should be ok with nil callback when succeeded", function() + local httpRequests = HttpRequests:new(createMockHttpService(true, 200, 0)) + httpRequests:reportExecution("a", "b", nil) + end) + + it("should correctly callback when connection error", function() + local httpRequests = HttpRequests:new(createMockHttpService(false)) + local called = false + function callback(success) + expect(success).to.equal(false) + called = true; + end + httpRequests:reportExecution("a", "b", callback) + expect(called).to.equal(true) + end) + + it("should correctly callback when 401", function() + local httpRequests = HttpRequests:new(createMockHttpService(true, 401)) + local called = false + function callback(success) + expect(success).to.equal(false) + called = true; + end + httpRequests:reportExecution("a", "b", callback) + expect(called).to.equal(true) + end) + + it("should correctly callback when errorCode", function() + local httpRequests = HttpRequests:new(createMockHttpService(true, 200, 1)) + local called = false + function callback(success) + expect(success).to.equal(false) + called = true; + end + httpRequests:reportExecution("a", "b", callback) + expect(called).to.equal(true) + end) + + it("should correctly callback when decoding failed", function() + local httpService = createMockHttpService(true, 200, 0) + httpService.JSONDecode = function(self, body) + assert(false) + end + local httpRequests = HttpRequests:new(httpService) + local called = false + function callback(success) + expect(success).to.equal(false) + called = true; + end + httpRequests:reportExecution("a", "b", callback) + expect(called).to.equal(true) + end) + + it("should correctly callback when wrong response json format", function() + local httpService = createMockHttpService(true, 200, 0) + httpService.JSONDecode = function(self, body) + return { } + end + local httpRequests = HttpRequests:new(httpService) + local called = false + function callback(success) + expect(success).to.equal(false) + called = true; + end + httpRequests:reportExecution("a", "b", callback) + expect(called).to.equal(true) + end) + end) +end diff --git a/LuaPackages/UIBloxFlags/GetFFlagLuaAppUseNewUIBloxRoundedCorners.lua b/LuaPackages/UIBloxFlags/GetFFlagLuaAppUseNewUIBloxRoundedCorners.lua new file mode 100644 index 0000000000..aeb0aa0035 --- /dev/null +++ b/LuaPackages/UIBloxFlags/GetFFlagLuaAppUseNewUIBloxRoundedCorners.lua @@ -0,0 +1,5 @@ +game:DefineFastFlag("LuaAppUseNewUIBloxRoundedCorners", false) + +return function() + return game:GetFastFlag("LuaAppUseNewUIBloxRoundedCorners") +end \ No newline at end of file diff --git a/LuaPackages/UIBloxFlags/GetFFlagLuaAppUseUIBloxToasts.lua b/LuaPackages/UIBloxFlags/GetFFlagLuaAppUseUIBloxToasts.lua deleted file mode 100644 index 38d6ffaf13..0000000000 --- a/LuaPackages/UIBloxFlags/GetFFlagLuaAppUseUIBloxToasts.lua +++ /dev/null @@ -1,5 +0,0 @@ -game:DefineFastFlag("LuaAppUseUIBloxToasts2", false) - -return function() - return game:GetFastFlag("LuaAppUseUIBloxToasts2") -end \ No newline at end of file diff --git a/LuaPackages/UIBloxUniversalAppConfig.lua b/LuaPackages/UIBloxUniversalAppConfig.lua index 341f8171bc..96ccecd815 100644 --- a/LuaPackages/UIBloxUniversalAppConfig.lua +++ b/LuaPackages/UIBloxUniversalAppConfig.lua @@ -1,13 +1,12 @@ -- See https://confluence.rbx.com/display/MOBAPP/UIBlox+Flagging -- for more info on how to add values here local CorePackages = game:GetService("CorePackages") -local GetFFlagLuaAppUseUIBloxToasts = require(CorePackages.UIBloxFlags.GetFFlagLuaAppUseUIBloxToasts) +local GetFFlagLuaAppUseNewUIBloxRoundedCorners = require(CorePackages.UIBloxFlags.GetFFlagLuaAppUseNewUIBloxRoundedCorners) local GetFFlagLuaUIBloxModalWindowAnchorPoint = require(CorePackages.UIBloxFlags.GetFFlagLuaUIBloxModalWindowAnchorPoint) local GetFFlagLuaFixItemTilePremiumIcon = require(CorePackages.UIBloxFlags.GetFFlagLuaFixItemTilePremiumIcon) return { - fixToastResizeConfig = GetFFlagLuaAppUseUIBloxToasts(), - expandableTextAutomaticResizeConfig = true, + useNewUICornerRoundedCorners = GetFFlagLuaAppUseNewUIBloxRoundedCorners(), modalWindowAnchorPoint = GetFFlagLuaUIBloxModalWindowAnchorPoint(), fixItemTilePremiumIcon = GetFFlagLuaFixItemTilePremiumIcon(), } \ No newline at end of file diff --git a/scripts/CoreScripts/CoreScripts/InGameChat.lua b/scripts/CoreScripts/CoreScripts/InGameChat.lua index 37b9b1a8f7..bc81dde485 100644 --- a/scripts/CoreScripts/CoreScripts/InGameChat.lua +++ b/scripts/CoreScripts/CoreScripts/InGameChat.lua @@ -184,7 +184,7 @@ onBubbleChatEnabledChanged() if game:GetEngineFeature("BubbleChatSettingsApi") then Chat.BubbleChatSettingsChanged:Connect(function(settings) local ok, message = Types.IChatSettings(settings) - assert(ok, "Bad settings object passed to Chat:SetBubbleChatSettings:\n"..message) + assert(ok, "Bad settings object passed to Chat:SetBubbleChatSettings:\n"..(message or "")) chatStore:dispatch(UpdateChatSettings(settings)) end) end diff --git a/scripts/CoreScripts/CoreScripts/NotificationScript2.lua b/scripts/CoreScripts/CoreScripts/NotificationScript2.lua index 5fb32009f5..ab089fadd1 100644 --- a/scripts/CoreScripts/CoreScripts/NotificationScript2.lua +++ b/scripts/CoreScripts/CoreScripts/NotificationScript2.lua @@ -38,6 +38,7 @@ local FFlagNewAwardBadgeEndpoint = settings():GetFFlag('NewAwardBadgeEndpoint2') local FFlagFixNotificationScriptError = game:DefineFastFlag("FixNotificationScriptError", false) local GetFFlagRemoveInGameFollowingEvents = require(RobloxGui.Modules.Flags.GetFFlagRemoveInGameFollowingEvents) +local GetFixGraphicsQuality = require(RobloxGui.Modules.Flags.GetFixGraphicsQuality) local isNewGamepadMenuEnabled = require(RobloxGui.Modules.Flags.isNewGamepadMenuEnabled) local RobloxTranslator = require(RobloxGui:WaitForChild("Modules"):WaitForChild("RobloxTranslator")) @@ -739,6 +740,7 @@ local function onBadgeAwarded(message, userId, badgeId) end end +-- DEPRECATED Remove with FixGraphicsQuality function onGameSettingsChanged(property, amount) if property == "SavedQualityLevel" then local level = GameSettings.SavedQualityLevel.Value + amount @@ -784,9 +786,12 @@ if not isTenFootInterface then Players.FriendRequestEvent:connect(onFriendRequestEvent) PointsService.PointsAwarded:connect(onPointsAwarded) --GameSettings.Changed:connect(onGameSettingsChanged) - game.GraphicsQualityChangeRequest:connect(function(graphicsIncrease) --graphicsIncrease is a boolean - onGameSettingsChanged("SavedQualityLevel", graphicsIncrease == true and 1 or -1) - end) + + if not GetFixGraphicsQuality() then + game.GraphicsQualityChangeRequest:connect(function(graphicsIncrease) --graphicsIncrease is a boolean + onGameSettingsChanged("SavedQualityLevel", graphicsIncrease == true and 1 or -1) + end) + end end local allowScreenshots = not PolicyService:IsSubjectToChinaPolicies() diff --git a/scripts/CoreScripts/CoreScripts/ScreenTimeInGame.lua b/scripts/CoreScripts/CoreScripts/ScreenTimeInGame.lua index bda76fbe1f..3afcb8bdc6 100644 --- a/scripts/CoreScripts/CoreScripts/ScreenTimeInGame.lua +++ b/scripts/CoreScripts/CoreScripts/ScreenTimeInGame.lua @@ -3,9 +3,11 @@ local GuiService = game:GetService("GuiService") local CoreGui = game:GetService("CoreGui") local RobloxGui = CoreGui:WaitForChild("RobloxGui") local NotificationService = game:GetService("NotificationService") +local CorePackages = game:GetService("CorePackages") local HttpService = game:GetService("HttpService") -local HttpRbxApiService = game:GetService("HttpRbxApiService") - +local ScreenTimeHttpRequests = require(CorePackages.Regulations.ScreenTime.HttpRequests) +local ScreenTimeConstants = require(CorePackages.Regulations.ScreenTime.Constants) +local Logging = require(CorePackages.Logging) local ErrorPrompt = require(RobloxGui.Modules.ErrorPrompt) local Url = require(RobloxGui.Modules.Common.Url) @@ -20,27 +22,14 @@ local ScreenTimeState = { OpenWebView = 3, } -local function markRead(messageToDisplay) - -- The ScreenTime V2 markRead endpoint, https://apis.roblox.qq.com/timed-entertainment-allowance/v1/reportExecute - local apiPath = "/timed-entertainment-allowance/v1/reportExecute" - local fullUrl = Url.APIS_URL .. apiPath - local nowLocal = os.date("*t", os.time()) - -- Required time format 2020-06-04T04:44:09Z - local formattedTime = ("%d-%02d-%02dT%02d:%02d:%02dZ"):format(nowLocal.year, nowLocal.month, nowLocal.day, nowLocal.hour, nowLocal.min, nowLocal.sec) - local payload = HttpService:JSONEncode({ - instructionName = messageToDisplay.instructionName, - serialId = messageToDisplay.id, - execTime = formattedTime, - }) - pcall(function() - return HttpRbxApiService:PostAsyncFullUrl(fullUrl, payload) - end) -end +local TAG = "ScreenTimeInGame" + +local screenTimeHttpRequests = ScreenTimeHttpRequests:new(HttpService) --[[ Resolving message: * display (move message PendingResolve -> Displaying) - * markread - spawned as soon as displayed to resolve with server + * report execution - report as soon as displayed to resolve with server * user input - pressing "ok" (move message Displaying -> Resolved) ]] @@ -82,9 +71,7 @@ local messageQueue = { self.displayMessageCallback(messageToDisplay.message) end - spawn(function() - markRead(messageToDisplay) - end) + screenTimeHttpRequests:reportExecution(messageToDisplay.instructionName, messageToDisplay.id) end, -- currently messages will be resolved on client side by user click on the "OK" button @@ -146,12 +133,12 @@ onScreenSizeChanged() local screenTimeUpdatedConnection -local function screenTimeStatesUpdated(responseTable) +local function screenTimeStatesUpdated(instructions) local lockout = false - local instructions = {} - for _, instruction in ipairs(responseTable.instructions) do + local filteredInstructions = {} + for _, instruction in ipairs(instructions) do if instruction.type == ScreenTimeState.Warning then - table.insert(instructions, instruction) + table.insert(filteredInstructions, instruction) elseif instruction.type == ScreenTimeState.Lockout then -- If there is a lockout, we will stop getting other state and then leaveGame lockout = true @@ -170,13 +157,29 @@ local function screenTimeStatesUpdated(responseTable) end leaveGame() else - messageQueue:update(instructions) + messageQueue:update(filteredInstructions) end end +local function requestInstructions() + screenTimeHttpRequests:getInstructions(function(success, unauthorized, instructions) + if success then + screenTimeStatesUpdated(instructions) + elseif unauthorized then + -- Leave it to LuaApp + Logging.warn(TAG .. " requestInstructions failed: unauthorized") + else + Logging.warn(TAG .. " requestInstructions failed: error") + end + end) +end + screenTimeUpdatedConnection = NotificationService.RobloxEventReceived:Connect(function(eventData) - if eventData.namespace == "ScreenTimeClientNotifications" then - local responseTable = HttpService:JSONDecode(eventData.detail) - screenTimeStatesUpdated(responseTable) + if eventData.namespace == ScreenTimeConstants.SIGNALR_NAMESPACE and + eventData.detailType == ScreenTimeConstants.SIGNALR_TYPE_NEW_INSTRUCTION then + requestInstructions() end end) + +-- First request on initialization +requestInstructions() diff --git a/scripts/CoreScripts/Modules/AvatarEditorPrompts/Actions/CloseOpenPrompt.lua b/scripts/CoreScripts/Modules/AvatarEditorPrompts/Actions/CloseOpenPrompt.lua new file mode 100644 index 0000000000..d2cb298754 --- /dev/null +++ b/scripts/CoreScripts/Modules/AvatarEditorPrompts/Actions/CloseOpenPrompt.lua @@ -0,0 +1,7 @@ +local CorePackages = game:GetService("CorePackages") + +local Action = require(CorePackages.AppTempCommon.Common.Action) + +return Action(script.Name, function() + return {} +end) \ No newline at end of file diff --git a/scripts/CoreScripts/Modules/AvatarEditorPrompts/Actions/GameNameFetched.lua b/scripts/CoreScripts/Modules/AvatarEditorPrompts/Actions/GameNameFetched.lua new file mode 100644 index 0000000000..acb9e33a20 --- /dev/null +++ b/scripts/CoreScripts/Modules/AvatarEditorPrompts/Actions/GameNameFetched.lua @@ -0,0 +1,9 @@ +local CorePackages = game:GetService("CorePackages") + +local Action = require(CorePackages.AppTempCommon.Common.Action) + +return Action(script.Name, function(gameName) + return { + gameName = gameName, + } +end) \ No newline at end of file diff --git a/scripts/CoreScripts/Modules/AvatarEditorPrompts/Actions/OpenPrompt.lua b/scripts/CoreScripts/Modules/AvatarEditorPrompts/Actions/OpenPrompt.lua new file mode 100644 index 0000000000..76d7cf5169 --- /dev/null +++ b/scripts/CoreScripts/Modules/AvatarEditorPrompts/Actions/OpenPrompt.lua @@ -0,0 +1,10 @@ +local CorePackages = game:GetService("CorePackages") + +local Action = require(CorePackages.AppTempCommon.Common.Action) + +return Action(script.Name, function(promptType, promptInfo) + return { + promptType = promptType, + promptInfo = promptInfo, + } +end) \ No newline at end of file diff --git a/scripts/CoreScripts/Modules/AvatarEditorPrompts/Actions/ScreenSizeUpdated.lua b/scripts/CoreScripts/Modules/AvatarEditorPrompts/Actions/ScreenSizeUpdated.lua new file mode 100644 index 0000000000..c60b92d4b3 --- /dev/null +++ b/scripts/CoreScripts/Modules/AvatarEditorPrompts/Actions/ScreenSizeUpdated.lua @@ -0,0 +1,9 @@ +local CorePackages = game:GetService("CorePackages") + +local Action = require(CorePackages.AppTempCommon.Common.Action) + +return Action(script.Name, function(screenSize) + return { + screenSize = screenSize, + } +end) \ No newline at end of file diff --git a/scripts/CoreScripts/Modules/AvatarEditorPrompts/Components/AvatarEditorPromptsApp.lua b/scripts/CoreScripts/Modules/AvatarEditorPrompts/Components/AvatarEditorPromptsApp.lua new file mode 100644 index 0000000000..45d4358823 --- /dev/null +++ b/scripts/CoreScripts/Modules/AvatarEditorPrompts/Components/AvatarEditorPromptsApp.lua @@ -0,0 +1,81 @@ +local CorePackages = game:GetService("CorePackages") + +local Roact = require(CorePackages.Roact) +local RoactRodux = require(CorePackages.RoactRodux) +local t = require(CorePackages.Packages.t) + +local Connection = require(script.Parent.Connection) + +local Prompts = script.Parent.Prompts +local AllowInventoryReadAccessPrompt = require(Prompts.AllowInventoryReadAccessPrompt) +local SaveAvatarPrompt = require(Prompts.SaveAvatarPrompt) +local CreateOutfitPrompt = require(Prompts.CreateOutfitPrompt) +local SetFavoritePrompt = require(Prompts.SetFavoritePrompt) + +local AvatarEditorPrompts = script.Parent.Parent + +local PromptType = require(AvatarEditorPrompts.PromptType) + +local ScreenSizeUpdated = require(AvatarEditorPrompts.Actions.ScreenSizeUpdated) + +--Displays behind the InGameMenu so that developers can't block interaction with the InGameMenu by constantly prompting. +local AVATAR_PROMPTS_DISPLAY_ORDER = 0 + +local AvatarEditorPromptsApp = Roact.PureComponent:extend("AvatarEditorPromptsApp") + +AvatarEditorPromptsApp.validateProps = t.strictInterface({ + --From State + promptType = t.optional(t.userdata), + + --Dispatch + screenSizeUpdated = t.callback, +}) + +function AvatarEditorPromptsApp:init() + self.absoluteSizeChanged = function(rbx) + self.props.screenSizeUpdated(rbx.AbsoluteSize) + end +end + +function AvatarEditorPromptsApp:render() + local promptComponent + if self.props.promptType == PromptType.AllowInventoryReadAccess then + promptComponent = Roact.createElement(AllowInventoryReadAccessPrompt) + elseif self.props.promptType == PromptType.SaveAvatar then + promptComponent = Roact.createElement(SaveAvatarPrompt) + elseif self.props.promptType == PromptType.CreateOutfit then + promptComponent = Roact.createElement(CreateOutfitPrompt) + elseif self.props.promptType == PromptType.SetFavorite then + promptComponent = Roact.createElement(SetFavoritePrompt) + end + + return Roact.createElement("ScreenGui", { + IgnoreGuiInset = true, + ZIndexBehavior = Enum.ZIndexBehavior.Sibling, + AutoLocalize = false, + DisplayOrder = AVATAR_PROMPTS_DISPLAY_ORDER, + + [Roact.Change.AbsoluteSize] = self.absoluteSizeChanged, + }, { + Connection = Roact.createElement(Connection), + + Prompt = promptComponent, + }) +end + +local function mapStateToProps(state) + return { + promptType = state.promptInfo.promptType, + } +end + +local function mapDispatchToProps(dispatch) + return { + screenSizeUpdated = function(screenSize) + return dispatch(ScreenSizeUpdated(screenSize)) + end, + } +end + + +return RoactRodux.UNSTABLE_connect2(mapStateToProps, mapDispatchToProps)(AvatarEditorPromptsApp) \ No newline at end of file diff --git a/scripts/CoreScripts/Modules/AvatarEditorPrompts/Components/Connection/AvatarEditorServiceConnector.lua b/scripts/CoreScripts/Modules/AvatarEditorPrompts/Components/Connection/AvatarEditorServiceConnector.lua new file mode 100644 index 0000000000..2e828cb864 --- /dev/null +++ b/scripts/CoreScripts/Modules/AvatarEditorPrompts/Components/Connection/AvatarEditorServiceConnector.lua @@ -0,0 +1,79 @@ +local CorePackages = game:GetService("CorePackages") +local AvatarEditorService = game:GetService("AvatarEditorService") + +local Roact = require(CorePackages.Roact) +local RoactRodux = require(CorePackages.RoactRodux) +local t = require(CorePackages.Packages.t) + +local Components = script.Parent.Parent +local AvatarEditorPrompts = Components.Parent + +local OpenPrompt = require(AvatarEditorPrompts.Actions.OpenPrompt) +local PromptType = require(AvatarEditorPrompts.PromptType) + +local OpenSetFavoritePrompt = require(AvatarEditorPrompts.Thunks.OpenSetFavoritePrompt) +local OpenSaveAvatarPrompt = require(AvatarEditorPrompts.Thunks.OpenSaveAvatarPrompt) + +local ExternalEventConnection = require(CorePackages.RoactUtilities.ExternalEventConnection) + +local AvatarEditorServiceConnector = Roact.PureComponent:extend("AvatarEditorServiceConnector") + +AvatarEditorServiceConnector.validateProps = t.strictInterface({ + --Dispatch + openPrompt = t.callback, + openSetFavoritePrompt = t.callback, + openSaveAvatarPrompt = t.callback, +}) + +function AvatarEditorServiceConnector:render() + return Roact.createFragment({ + OpenPromptSaveAvatarConnection = Roact.createElement(ExternalEventConnection, { + event = AvatarEditorService.OpenPromptSaveAvatar, + callback = function(humanoidDescription, rigType) + self.props.openSaveAvatarPrompt(humanoidDescription, rigType) + end, + }), + + OpenAllowInventoryReadAccessConnection = Roact.createElement(ExternalEventConnection, { + event = AvatarEditorService.OpenAllowInventoryReadAccess, + callback = function() + self.props.openPrompt(PromptType.AllowInventoryReadAccess, {}) + end, + }), + + OpenPromptCreateOufitConnection = Roact.createElement(ExternalEventConnection, { + event = AvatarEditorService.OpenPromptCreateOufit, + callback = function(humanoidDescription, rigType) + self.props.openPrompt(PromptType.CreateOutfit, { + humanoidDescription = humanoidDescription, + rigType = rigType, + }) + end, + }), + + OpenPromptSetFavoriteConnection = Roact.createElement(ExternalEventConnection, { + event = AvatarEditorService.OpenPromptSetFavorite, + callback = function(itemId, itemType, isFavorited) + self.props.openSetFavoritePrompt(itemId, itemType, isFavorited) + end, + }), + }) +end + +local function mapDispatchToProps(dispatch) + return { + openPrompt = function(promptType, promptArgs) + return dispatch(OpenPrompt(promptType, promptArgs)) + end, + + openSetFavoritePrompt = function(itemId, itemType, shouldFavorite) + return dispatch(OpenSetFavoritePrompt(itemId, itemType, shouldFavorite)) + end, + + openSaveAvatarPrompt = function(humanoidDescription, rigType) + return dispatch(OpenSaveAvatarPrompt(humanoidDescription, rigType)) + end, + } +end + +return RoactRodux.UNSTABLE_connect2(nil, mapDispatchToProps)(AvatarEditorServiceConnector) \ No newline at end of file diff --git a/scripts/CoreScripts/Modules/AvatarEditorPrompts/Components/Connection/ContextActionsBinder.lua b/scripts/CoreScripts/Modules/AvatarEditorPrompts/Components/Connection/ContextActionsBinder.lua new file mode 100644 index 0000000000..8de98a28bd --- /dev/null +++ b/scripts/CoreScripts/Modules/AvatarEditorPrompts/Components/Connection/ContextActionsBinder.lua @@ -0,0 +1,106 @@ +local CorePackages = game:GetService("CorePackages") +local ContextActionService = game:GetService("ContextActionService") +local GuiService = game:GetService("GuiService") + +local Roact = require(CorePackages.Roact) +local RoactRodux = require(CorePackages.RoactRodux) +local t = require(CorePackages.Packages.t) + +local Connection = script.Parent +local Components = Connection.Parent +local AvatarEditorPrompts = Components.Parent + +local CloseOpenPrompt = require(AvatarEditorPrompts.Thunks.CloseOpenPrompt) + +local CLOSE_AVATAR_EDITOR_PROMPT_NAME = "CloseAvatarEditorPrompt" + +local ContextActionsBinder = Roact.PureComponent:extend("ContextActionsBinder") + +ContextActionsBinder.validateProps = t.strictInterface({ + --Map State to Props + promptOpen = t.boolean, + + --Map dispatch to props + closeOpenPrompt = t.callback, +}) + +function ContextActionsBinder:init() + self.actionsBound = false +end + +function ContextActionsBinder:bindActions() + if self.actionsBound then + return + end + + self.actionsBound = true + + ContextActionService:BindCoreAction( + CLOSE_AVATAR_EDITOR_PROMPT_NAME, + function(actionName, inputState, inputObject) + if GuiService.MenuIsOpen then + return Enum.ContextActionResult.Pass + end + + if inputState ~= Enum.UserInputState.Begin then + return Enum.ContextActionResult.Pass + end + self.props.closeOpenPrompt() + return Enum.ContextActionResult.Sink + end, + false, + Enum.KeyCode.Escape + ) +end + +function ContextActionsBinder:unbindActions() + if not self.actionsBound then + return + end + + self.actionsBound = false + + ContextActionService:UnbindCoreAction(CLOSE_AVATAR_EDITOR_PROMPT_NAME) +end + +function ContextActionsBinder:didMount() + if self.props.promptOpen then + self:bindActions() + end +end + +function ContextActionsBinder:render() + return nil +end + +function ContextActionsBinder:didUpdate(prevProps, prevState) + if self.props.promptOpen ~= prevProps.promptOpen then + if self.props.promptOpen then + self:bindActions() + else + self:unbindActions() + end + end +end + +function ContextActionsBinder:willUnmount() + if self.actionsBound then + self:unbindActions() + end +end + +local function mapStateToProps(state) + return { + promptOpen = state.promptInfo.promptType ~= nil, + } +end + +local function mapDispatchToProps(dispatch) + return { + closeOpenPrompt = function() + return dispatch(CloseOpenPrompt) + end, + } +end + +return RoactRodux.UNSTABLE_connect2(mapStateToProps, mapDispatchToProps)(ContextActionsBinder) \ No newline at end of file diff --git a/scripts/CoreScripts/Modules/AvatarEditorPrompts/Components/Connection/init.lua b/scripts/CoreScripts/Modules/AvatarEditorPrompts/Components/Connection/init.lua new file mode 100644 index 0000000000..aaa3ca4698 --- /dev/null +++ b/scripts/CoreScripts/Modules/AvatarEditorPrompts/Components/Connection/init.lua @@ -0,0 +1,17 @@ +local CorePackages = game:GetService("CorePackages") + +local Roact = require(CorePackages.Roact) + +local ContextActionsBinder = require(script.ContextActionsBinder) +local AvatarEditorServiceConnector = require(script.AvatarEditorServiceConnector) + +local Connection = Roact.PureComponent:extend("Connection") + +function Connection:render() + return Roact.createFragment({ + ContextActionsBinder = Roact.createElement(ContextActionsBinder), + AvatarEditorServiceConnector = Roact.createElement(AvatarEditorServiceConnector), + }) +end + +return Connection \ No newline at end of file diff --git a/scripts/CoreScripts/Modules/AvatarEditorPrompts/Components/HumanoidViewport.lua b/scripts/CoreScripts/Modules/AvatarEditorPrompts/Components/HumanoidViewport.lua new file mode 100644 index 0000000000..905b45b0f5 --- /dev/null +++ b/scripts/CoreScripts/Modules/AvatarEditorPrompts/Components/HumanoidViewport.lua @@ -0,0 +1,220 @@ +local CorePackages = game:GetService("CorePackages") +local Players = game:GetService("Players") + +local Roact = require(CorePackages.Roact) +local t = require(CorePackages.Packages.t) +local UIBlox = require(CorePackages.UIBlox) + +local ShimmerPanel = UIBlox.Loading.ShimmerPanel + +local INITIAL_OFFSET = 5 +local ROTATION_CFRAME = CFrame.fromEulerAnglesXYZ(math.rad(20), math.rad(15), math.rad(40)) +local THUMBNAIL_FOV = 70 +local ZOOM_FACTOR = 1.2 + +local HumanoidViewport = Roact.PureComponent:extend("HumanoidViewport") + +HumanoidViewport.validateProps = t.strictInterface({ + humanoidDescription = t.instanceOf("HumanoidDescription"), + rigType = t.enum(Enum.HumanoidRigType), +}) + +function HumanoidViewport:init() + self:setState({ + loading = true + }) + + self.cameraRef = Roact.createRef() + self.worldModelRef = Roact.createRef() + + self.cameraCFrameBinding, self.updateCameraCFrameBinding = Roact.createBinding(CFrame.new()) + self.cameraFocusBinding, self.updateCameraFocusBinding = Roact.createBinding(CFrame.new()) + + self.humanoidModel = nil + + self.mounted = false +end + +local function rotateLookVector(lookVector) + local look = lookVector + if math.abs(look.Y) > 0.95 then + look = Vector3.new(0, 0, -1) + else + look = Vector3.new(look.X, 0, look.Z) + look = look.unit + end + local lookCoord = CFrame.new(Vector3.new(0, 0, 0), look) + lookCoord = lookCoord * ROTATION_CFRAME + return lookCoord.lookVector +end + +local function getCameraOffset(fov, extentsSize) + local xSize, ySize, zSize = extentsSize.X, extentsSize.Y, extentsSize.Z + local maxSize = math.sqrt(xSize^2 + ySize^2 + zSize^2) + local fovMultiplier = 1 / math.tan(math.rad(fov) / 2) + local halfSize = maxSize / 2 + return halfSize * fovMultiplier +end + +local function zoomExtents(model, lookVector, cameraCFrame) + local modelCFrame = model:GetModelCFrame() + local position = modelCFrame.p + local extentsSize = model:GetExtentsSize() + local cameraOffset = getCameraOffset(THUMBNAIL_FOV, extentsSize) + local zoomFactor = 1 / ZOOM_FACTOR + cameraOffset = cameraOffset * zoomFactor + local cameraRotation = cameraCFrame - cameraCFrame.p + return cameraRotation + position + (lookVector * cameraOffset) +end + +function HumanoidViewport:positionCamera() + local model = self.humanoidModel + local modelCFrame = model:GetModelCFrame() + local lookVector = modelCFrame.lookVector + local humanoidRootPart = model:FindFirstChild("HumanoidRootPart") + if humanoidRootPart then + lookVector = humanoidRootPart.CFrame.lookVector + end + lookVector = rotateLookVector(lookVector) + + local cameraCFrame = CFrame.new(modelCFrame.p + (lookVector * INITIAL_OFFSET), modelCFrame.p) + cameraCFrame = zoomExtents(model, lookVector, cameraCFrame) + + self.updateCameraCFrameBinding(cameraCFrame) + self.updateCameraFocusBinding(modelCFrame) +end + +function HumanoidViewport:loadIdleAnimation(humanoidModel) + local humanoid = humanoidModel:FindFirstChildOfClass("Humanoid") + local humanoidDescription = humanoid.HumanoidDescription + + if humanoidDescription.IdleAnimation == 0 then + return + end + + local animate = humanoidModel:FindFirstChild("Animate") + if not animate then + return + end + + local idle = animate:FindFirstChild("idle") + if not idle then + return + end + + local animation = idle:FindFirstChildOfClass("Animation") + if not animation then + return + end + + local animationTrack = humanoid:LoadAnimation(animation) + animationTrack.Looped = true + animationTrack:Play() +end + +function HumanoidViewport:loadHumanoidModel() + local humanoidDescription = self.props.humanoidDescription + local rigType = self.props.rigType + + coroutine.wrap(function() + local model = Players:CreateHumanoidModelFromDescription(humanoidDescription, rigType) + + if not self.mounted then + return + end + + if self.props.humanoidDescription ~= humanoidDescription then + return + end + + if self.props.rigType ~= rigType then + return + end + + self.humanoidModel = model + if self.worldModelRef:getValue() then + self.humanoidModel.Parent = self.worldModelRef:getValue() + end + + self:positionCamera() + self:loadIdleAnimation(model) + + self:setState({ + loading = false + }) + end)() +end + +function HumanoidViewport:render() + return Roact.createElement("Frame", { + BackgroundTransparency = 1, + Size = UDim2.fromScale(1, 1), + Position = UDim2.fromScale(0.5, 0.5), + AnchorPoint = Vector2.new(0.5, 0.5), + }, { + AspectRatioConstraint = Roact.createElement("UIAspectRatioConstraint", { + AspectRatio = 1, + AspectType = Enum.AspectType.FitWithinMaxSize, + DominantAxis = Enum.DominantAxis.Width, + }), + + ShimmerFrame = self.state.loading and Roact.createElement(ShimmerPanel, { + Size = UDim2.fromScale(1, 1), + Position = UDim2.fromScale(0.5, 0.5), + AnchorPoint = Vector2.new(0.5, 0.5), + }), + + ViewportFrame = Roact.createElement("ViewportFrame", { + Visible = not self.state.loading, + BackgroundTransparency = 1, + Size = UDim2.fromScale(1, 1), + Position = UDim2.fromScale(0.5, 0.5), + AnchorPoint = Vector2.new(0.5, 0.5), + LightColor = Color3.fromRGB(240, 240, 240), + Ambient = Color3.fromRGB(240, 240, 240), + CurrentCamera = self.cameraRef, + }, { + Camera = Roact.createElement("Camera", { + CameraType = Enum.CameraType.Scriptable, + FieldOfView = THUMBNAIL_FOV, + + CFrame = self.cameraCFrameBinding, + Focus = self.cameraFocusBinding, + + [Roact.Ref] = self.cameraRef, + }), + + WorldModel = Roact.createElement("WorldModel", { + [Roact.Ref] = self.worldModelRef, + }), + }), + }) +end + +function HumanoidViewport:didMount() + self.mounted = true + + self:loadHumanoidModel() +end + +function HumanoidViewport:didUpdate(prevProps) + if self.worldModelRef:getValue() and self.humanoidModel then + self.humanoidModel.Parent = self.worldModelRef:getValue() + end + + local descriptionUpdated = self.props.humanoidDescription ~= prevProps.humanoidDescription + local rigTypeUpdated = self.props.rigType ~= prevProps.rigType + if descriptionUpdated or rigTypeUpdated then + self:setState({ + loading = true + }) + + self:loadHumanoidModel() + end +end + +function HumanoidViewport:willUnmount() + self.mounted = false +end + +return HumanoidViewport \ No newline at end of file diff --git a/scripts/CoreScripts/Modules/AvatarEditorPrompts/Components/ItemsList.lua b/scripts/CoreScripts/Modules/AvatarEditorPrompts/Components/ItemsList.lua new file mode 100644 index 0000000000..93cc42183d --- /dev/null +++ b/scripts/CoreScripts/Modules/AvatarEditorPrompts/Components/ItemsList.lua @@ -0,0 +1,99 @@ +local CorePackages = game:GetService("CorePackages") + +local Roact = require(CorePackages.Roact) +local t = require(CorePackages.Packages.t) +local UIBlox = require(CorePackages.UIBlox) +local AvatarExperienceDeps = require(CorePackages.AvatarExperienceDeps) +local Text = require(CorePackages.AppTempCommon.Common.Text) + +local RoactFitComponents = AvatarExperienceDeps.RoactFitComponents +local FitTextLabel = RoactFitComponents.FitTextLabel +local VerticalScrollView = UIBlox.App.Container.VerticalScrollView +local withStyle = UIBlox.Style.withStyle + +local PADDING_BETWEEN = 10 +local BULLET_POINT_SYMBOL = "• " + +local ItemsList = Roact.PureComponent:extend("ItemsList") + +ItemsList.validateProps = t.strictInterface({ + assetNames = t.array(t.string), +}) + +function ItemsList:init() + self:setState({ + canvasSizeY = 0 + }) + + self.onContentSizeChanged = function(rbx) + self:setState({ + canvasSizeY = rbx.AbsoluteContentSize.Y + }) + end +end + +function ItemsList:render() + return withStyle(function(stylePalette) + local fontInfo = stylePalette.Font + local theme = stylePalette.Theme + + local font = fontInfo.CaptionBody.Font + local fontSize = fontInfo.BaseSize * fontInfo.CaptionBody.RelativeSize + local list = {} + + local bulletPointWidth = Text.GetTextWidth(BULLET_POINT_SYMBOL, font, fontSize) + + list.Layout = Roact.createElement("UIListLayout", { + FillDirection = Enum.FillDirection.Vertical, + HorizontalAlignment = Enum.HorizontalAlignment.Left, + Padding = UDim.new(0, PADDING_BETWEEN), + SortOrder = Enum.SortOrder.LayoutOrder, + + [Roact.Change.AbsoluteContentSize] = self.onContentSizeChanged, + }) + + for i, assetName in ipairs(self.props.assetNames) do + list[i] = Roact.createElement(RoactFitComponents.FitFrameVertical, { + width = UDim.new(1, 0), + + FillDirection = Enum.FillDirection.Horizontal, + VerticalAlignment = Enum.VerticalAlignment.Top, + + BackgroundTransparency = 1, + LayoutOrder = i, + }, { + Bullet = Roact.createElement("TextLabel", { + BackgroundTransparency = 1, + Size = UDim2.fromOffset(bulletPointWidth, fontSize), + Text = BULLET_POINT_SYMBOL, + Font = font, + TextSize = fontSize, + TextColor3 = theme.TextDefault.Color, + TextTransparency = theme.TextDefault.Transparency, + TextXAlignment = Enum.TextXAlignment.Left, + LayoutOrder = 1, + }), + + Text = Roact.createElement(FitTextLabel, { + width = UDim.new(1, -bulletPointWidth), + + BackgroundTransparency = 1, + Text = assetName, + Font = font, + TextSize = fontSize, + TextColor3 = theme.TextDefault.Color, + TextTransparency = theme.TextDefault.Transparency, + TextXAlignment = Enum.TextXAlignment.Left, + LayoutOrder = 2, + }) + }) + end + + return Roact.createElement(VerticalScrollView, { + size = UDim2.fromScale(1, 1), + canvasSizeY = UDim.new(0, self.state.canvasSizeY), + }, list) + end) +end + +return ItemsList \ No newline at end of file diff --git a/scripts/CoreScripts/Modules/AvatarEditorPrompts/Components/Prompts/AllowInventoryReadAccessPrompt.lua b/scripts/CoreScripts/Modules/AvatarEditorPrompts/Components/Prompts/AllowInventoryReadAccessPrompt.lua new file mode 100644 index 0000000000..58c57e5418 --- /dev/null +++ b/scripts/CoreScripts/Modules/AvatarEditorPrompts/Components/Prompts/AllowInventoryReadAccessPrompt.lua @@ -0,0 +1,78 @@ +local CorePackages = game:GetService("CorePackages") +local CoreGui = game:GetService("CoreGui") + +local Roact = require(CorePackages.Roact) +local RoactRodux = require(CorePackages.RoactRodux) +local t = require(CorePackages.Packages.t) +local UIBlox = require(CorePackages.UIBlox) + +local InteractiveAlert = UIBlox.App.Dialog.Alert.InteractiveAlert +local ButtonType = UIBlox.App.Button.Enum.ButtonType + +local RobloxGui = CoreGui:WaitForChild("RobloxGui") +local RobloxTranslator = require(RobloxGui.Modules.RobloxTranslator) + +local Components = script.Parent.Parent +local AvatarEditorPrompts = Components.Parent + +local SetAllowInventoryReadAccess = require(AvatarEditorPrompts.Thunks.SetAllowInventoryReadAccess) + +local AllowInventoryReadAccessPrompt = Roact.PureComponent:extend("AllowInventoryReadAccessPrompt") + +AllowInventoryReadAccessPrompt.validateProps = t.strictInterface({ + --State + gameName = t.string, + screenSize = t.Vector2, + --Dispatch + setAvatarReadAccessAllowed = t.callback, + setAvatarReadAccessDenied = t.callback, +}) + +function AllowInventoryReadAccessPrompt:render() + return Roact.createElement(InteractiveAlert, { + title = RobloxTranslator:FormatByKey("CoreScripts.AvatarEditorPrompts.InventoryReadAccessPromptTitle"), + bodyText = RobloxTranslator:FormatByKey("CoreScripts.AvatarEditorPrompts.InventoryReadAccessPromptText", { + RBX_NAME = self.props.gameName, + }), + buttonStackInfo = { + buttons = { + { + props = { + onActivated = self.props.setAvatarReadAccessDenied, + text = RobloxTranslator:FormatByKey("CoreScripts.AvatarEditorPrompts.InventoryReadAccessPromptNo"), + }, + }, + { + buttonType = ButtonType.PrimarySystem, + props = { + onActivated = self.props.setAvatarReadAccessAllowed, + text = RobloxTranslator:FormatByKey("CoreScripts.AvatarEditorPrompts.InventoryReadAccessPromptYes"), + }, + }, + }, + }, + position = UDim2.fromScale(0.5, 0.5), + screenSize = self.props.screenSize, + }) +end + +local function mapStateToProps(state) + return { + gameName = state.gameName, + screenSize = state.screenSize, + } +end + +local function mapDispatchToProps(dispatch) + return { + setAvatarReadAccessDenied = function() + return dispatch(SetAllowInventoryReadAccess(false)) + end, + + setAvatarReadAccessAllowed = function() + return dispatch(SetAllowInventoryReadAccess(true)) + end, + } +end + +return RoactRodux.UNSTABLE_connect2(mapStateToProps, mapDispatchToProps)(AllowInventoryReadAccessPrompt) \ No newline at end of file diff --git a/scripts/CoreScripts/Modules/AvatarEditorPrompts/Components/Prompts/AllowInventoryReadAccessPrompt.spec.lua b/scripts/CoreScripts/Modules/AvatarEditorPrompts/Components/Prompts/AllowInventoryReadAccessPrompt.spec.lua new file mode 100644 index 0000000000..34939dc406 --- /dev/null +++ b/scripts/CoreScripts/Modules/AvatarEditorPrompts/Components/Prompts/AllowInventoryReadAccessPrompt.spec.lua @@ -0,0 +1,46 @@ +return function() + local CorePackages = game:GetService("CorePackages") + + local AppDarkTheme = require(CorePackages.AppTempCommon.LuaApp.Style.Themes.DarkTheme) + local AppFont = require(CorePackages.AppTempCommon.LuaApp.Style.Fonts.Gotham) + + local Roact = require(CorePackages.Roact) + local Rodux = require(CorePackages.Rodux) + local RoactRodux = require(CorePackages.RoactRodux) + local UIBlox = require(CorePackages.UIBlox) + + local AvatarEditorPrompts = script.Parent.Parent.Parent + local Reducer = require(AvatarEditorPrompts.Reducer) + local PromptType = require(AvatarEditorPrompts.PromptType) + local OpenPrompt = require(AvatarEditorPrompts.Actions.OpenPrompt) + + local appStyle = { + Theme = AppDarkTheme, + Font = AppFont, + } + + describe("AllowInventoryReadAccessPrompt", function() + it("should create and destroy without errors", function() + local AllowInventoryReadAccessPrompt = require(script.Parent.AllowInventoryReadAccessPrompt) + + local store = Rodux.Store.new(Reducer, nil, { + Rodux.thunkMiddleware, + }) + + store:dispatch(OpenPrompt(PromptType.AllowInventoryReadAccess, {})) + + local element = Roact.createElement(RoactRodux.StoreProvider, { + store = store, + }, { + ThemeProvider = Roact.createElement(UIBlox.Style.Provider, { + style = appStyle, + }, { + AllowInventoryReadAccessPrompt = Roact.createElement(AllowInventoryReadAccessPrompt) + }) + }) + + local instance = Roact.mount(element) + Roact.unmount(instance) + end) + end) +end diff --git a/scripts/CoreScripts/Modules/AvatarEditorPrompts/Components/Prompts/CreateOutfitPrompt.lua b/scripts/CoreScripts/Modules/AvatarEditorPrompts/Components/Prompts/CreateOutfitPrompt.lua new file mode 100644 index 0000000000..46dab8a0c0 --- /dev/null +++ b/scripts/CoreScripts/Modules/AvatarEditorPrompts/Components/Prompts/CreateOutfitPrompt.lua @@ -0,0 +1,201 @@ +local CorePackages = game:GetService("CorePackages") +local CoreGui = game:GetService("CoreGui") + +local Roact = require(CorePackages.Roact) +local RoactRodux = require(CorePackages.RoactRodux) +local t = require(CorePackages.Packages.t) +local UIBlox = require(CorePackages.UIBlox) + +local InteractiveAlert = UIBlox.App.Dialog.Alert.InteractiveAlert +local ButtonType = UIBlox.App.Button.Enum.ButtonType +local ImageSetLabel = UIBlox.Core.ImageSet.Label +local withStyle = UIBlox.Style.withStyle + +local Images = UIBlox.App.ImageSet.Images + +local RobloxGui = CoreGui:WaitForChild("RobloxGui") +local RobloxTranslator = require(RobloxGui.Modules.RobloxTranslator) + +local Components = script.Parent.Parent +local AvatarEditorPrompts = Components.Parent + +local HumanoidViewport = require(Components.HumanoidViewport) + +local SignalCreateOutfitPermissionDenied = require(AvatarEditorPrompts.Thunks.SignalCreateOutfitPermissionDenied) +local PerformCreateOutfit = require(AvatarEditorPrompts.Thunks.PerformCreateOutfit) + +local VIEWPORT_SIDE_PADDING = 10 +local SCREEN_SIZE_PADDING = 30 +local NAME_TEXTBOX_HEIGHT = 35 + +local TEXTBOX_STROKE = Images["component_assets/circle_17_stroke_1"] +local STROKE_SLICE_CENTER = Rect.new(8, 8, 8, 8) + +local CreateOutfitPrompt = Roact.PureComponent:extend("CreateOutfitPrompt") + +CreateOutfitPrompt.validateProps = t.strictInterface({ + --State + screenSize = t.Vector2, + humanoidDescription = t.instanceOf("HumanoidDescription"), + rigType = t.enum(Enum.HumanoidRigType), + --Dispatch + performCreateOutfit = t.callback, + signalCreateOutfitPermissionDenied = t.callback, +}) + +function CreateOutfitPrompt:init() + self:setState({ + outfitName = "", + }) + + self.middleContentRef = Roact.createRef() + self.contentSize, self.updateContentSize = Roact.createBinding(UDim2.new(1, 0, 0, 200)) + + self.onAlertSizeChanged = function(rbx) + local alertSize = rbx.AbsoluteSize + + if not self.middleContentRef:getValue() then + return + end + + local currentHeight = self.middleContentRef:getValue().AbsoluteSize.Y + local alertNoContentHeight = alertSize.Y - currentHeight + local maxAllowedContentHeight = self.props.screenSize.Y - (SCREEN_SIZE_PADDING * 2) - alertNoContentHeight + + local viewportMaxSize = self.middleContentRef:getValue().AbsoluteSize.X - ( VIEWPORT_SIDE_PADDING * 2) + local totalMaxHeight = viewportMaxSize + NAME_TEXTBOX_HEIGHT + + if maxAllowedContentHeight > totalMaxHeight then + maxAllowedContentHeight = totalMaxHeight + end + + if currentHeight ~= maxAllowedContentHeight then + self.updateContentSize(UDim2.new(1, 0, 0, maxAllowedContentHeight)) + end + end + + self.confirmCreateOutfit = function() + self.props.performCreateOutfit(self.state.outfitName) + end + + self.textUpdated = function(rbx) + self:setState({ + outfitName = rbx.Text, + }) + end + + self.renderAlertMiddleContent = function() + return withStyle(function(styles) + local font = styles.Font + local theme = styles.Theme + + return Roact.createElement("Frame", { + BackgroundTransparency = 1, + Size = self.contentSize, + + [Roact.Ref] = self.middleContentRef, + }, { + HumanoidViewportFrame = Roact.createElement("Frame", { + BackgroundTransparency = 1, + Size = UDim2.new(1, 0, 1, -40), + }, { + HumanoidViewport = Roact.createElement(HumanoidViewport, { + humanoidDescription = self.props.humanoidDescription, + rigType = self.props.rigType, + }) + }), + + TextboxContainer = Roact.createElement("Frame", { + BackgroundTransparency = 1, + Size = UDim2.new(1, 0, 0, NAME_TEXTBOX_HEIGHT), + Position = UDim2.fromScale(0, 1), + AnchorPoint = Vector2.new(0, 1), + }, { + TextboxBorder = Roact.createElement(ImageSetLabel, { + BackgroundTransparency = 1, + Image = TEXTBOX_STROKE, + ImageColor3 = theme.UIDefault.Color, + ImageTransparency = theme.UIDefault.Transparency, + LayoutOrder = 3, + ScaleType = Enum.ScaleType.Slice, + Size = UDim2.new(1, 0, 1, -5), + Position = UDim2.fromScale(0, 1), + AnchorPoint = Vector2.new(0, 1), + SliceCenter = STROKE_SLICE_CENTER, + }, { + Textbox = Roact.createElement("TextBox", { + BackgroundTransparency = 1, + ClearTextOnFocus = false, + Font = font.Header2.Font, + FontSize = font.BaseSize * font.CaptionBody.RelativeSize, + PlaceholderColor3 = theme.TextDefault.Color, + PlaceholderText = RobloxTranslator:FormatByKey("CoreScripts.AvatarEditorPrompts.OutfitNamePlaceholder"), + Position = UDim2.new(0, 6, 0, 0), + Size = UDim2.new(1, -12, 1, 0), + TextColor3 = theme.TextEmphasis.Color, + TextSize = 16, + TextTruncate = Enum.TextTruncate.AtEnd, + Text = self.state.outfitName, + TextWrapped = true, + TextXAlignment = Enum.TextXAlignment.Left, + OverlayNativeInput = true, + [Roact.Change.Text] = self.textUpdated, + }) + }), + }) + }) + end) + end +end + +function CreateOutfitPrompt:render() + return Roact.createElement(InteractiveAlert, { + title = RobloxTranslator:FormatByKey("CoreScripts.AvatarEditorPrompts.CreateOutfitPromptTitle"), + bodyText = RobloxTranslator:FormatByKey("CoreScripts.AvatarEditorPrompts.CreateOutfitPromptText"), + buttonStackInfo = { + buttons = { + { + props = { + onActivated = self.props.signalCreateOutfitPermissionDenied, + text = RobloxTranslator:FormatByKey("CoreScripts.AvatarEditorPrompts.CreateOutfitPromptNo"), + }, + }, + { + buttonType = ButtonType.PrimarySystem, + props = { + isDisabled = self.state.outfitName == "", + onActivated = self.confirmCreateOutfit, + text = RobloxTranslator:FormatByKey("CoreScripts.AvatarEditorPrompts.CreateOutfitPromptYes"), + }, + }, + }, + }, + position = UDim2.fromScale(0.5, 0.5), + screenSize = self.props.screenSize, + middleContent = self.renderAlertMiddleContent, + onAbsoluteSizeChanged = self.onAlertSizeChanged, + isMiddleContentFocusable = false, + }) +end + +local function mapStateToProps(state) + return { + screenSize = state.screenSize, + humanoidDescription = state.promptInfo.humanoidDescription, + rigType = state.promptInfo.rigType, + } +end + +local function mapDispatchToProps(dispatch) + return { + signalCreateOutfitPermissionDenied = function() + return dispatch(SignalCreateOutfitPermissionDenied) + end, + + performCreateOutfit = function(outfitName) + return dispatch(PerformCreateOutfit(outfitName)) + end, + } +end + +return RoactRodux.UNSTABLE_connect2(mapStateToProps, mapDispatchToProps)(CreateOutfitPrompt) \ No newline at end of file diff --git a/scripts/CoreScripts/Modules/AvatarEditorPrompts/Components/Prompts/CreateOutfitPrompt.spec.lua b/scripts/CoreScripts/Modules/AvatarEditorPrompts/Components/Prompts/CreateOutfitPrompt.spec.lua new file mode 100644 index 0000000000..a4c78e4f6b --- /dev/null +++ b/scripts/CoreScripts/Modules/AvatarEditorPrompts/Components/Prompts/CreateOutfitPrompt.spec.lua @@ -0,0 +1,51 @@ +return function() + local CorePackages = game:GetService("CorePackages") + + local AppDarkTheme = require(CorePackages.AppTempCommon.LuaApp.Style.Themes.DarkTheme) + local AppFont = require(CorePackages.AppTempCommon.LuaApp.Style.Fonts.Gotham) + + local Roact = require(CorePackages.Roact) + local Rodux = require(CorePackages.Rodux) + local RoactRodux = require(CorePackages.RoactRodux) + local UIBlox = require(CorePackages.UIBlox) + + local AvatarEditorPrompts = script.Parent.Parent.Parent + local Reducer = require(AvatarEditorPrompts.Reducer) + local PromptType = require(AvatarEditorPrompts.PromptType) + local OpenPrompt = require(AvatarEditorPrompts.Actions.OpenPrompt) + + local appStyle = { + Theme = AppDarkTheme, + Font = AppFont, + } + + describe("CreateOutfitPrompt", function() + it("should create and destroy without errors", function() + local CreateOutfitPrompt = require(script.Parent.CreateOutfitPrompt) + + local store = Rodux.Store.new(Reducer, nil, { + Rodux.thunkMiddleware, + }) + + local humanoidDescription = Instance.new("HumanoidDescription") + + store:dispatch(OpenPrompt(PromptType.CreateOutfit, { + humanoidDescription = humanoidDescription, + rigType = Enum.HumanoidRigType.R15, + })) + + local element = Roact.createElement(RoactRodux.StoreProvider, { + store = store, + }, { + ThemeProvider = Roact.createElement(UIBlox.Style.Provider, { + style = appStyle, + }, { + CreateOutfitPrompt = Roact.createElement(CreateOutfitPrompt) + }) + }) + + local instance = Roact.mount(element) + Roact.unmount(instance) + end) + end) +end diff --git a/scripts/CoreScripts/Modules/AvatarEditorPrompts/Components/Prompts/SaveAvatarPrompt.lua b/scripts/CoreScripts/Modules/AvatarEditorPrompts/Components/Prompts/SaveAvatarPrompt.lua new file mode 100644 index 0000000000..3bac7f8581 --- /dev/null +++ b/scripts/CoreScripts/Modules/AvatarEditorPrompts/Components/Prompts/SaveAvatarPrompt.lua @@ -0,0 +1,162 @@ +local CorePackages = game:GetService("CorePackages") +local CoreGui = game:GetService("CoreGui") + +local Roact = require(CorePackages.Roact) +local RoactRodux = require(CorePackages.RoactRodux) +local t = require(CorePackages.Packages.t) +local UIBlox = require(CorePackages.UIBlox) + +local InteractiveAlert = UIBlox.App.Dialog.Alert.InteractiveAlert +local ButtonType = UIBlox.App.Button.Enum.ButtonType + +local RobloxGui = CoreGui:WaitForChild("RobloxGui") +local RobloxTranslator = require(RobloxGui.Modules.RobloxTranslator) + +local Components = script.Parent.Parent +local AvatarEditorPrompts = Components.Parent + +local HumanoidViewport = require(Components.HumanoidViewport) +local ItemsList = require(Components.ItemsList) + +local SignalSaveAvatarPermissionDenied = require(AvatarEditorPrompts.Thunks.SignalSaveAvatarPermissionDenied) +local PerformSaveAvatar = require(AvatarEditorPrompts.Thunks.PerformSaveAvatar) + +local SCREEN_SIZE_PADDING = 30 +local VIEWPORT_MAX_TOP_PADDING = 40 +local VIEWPORT_SIDE_PADDING = 10 + +local SaveAvatarPrompt = Roact.PureComponent:extend("SaveAvatarPrompt") + +SaveAvatarPrompt.validateProps = t.strictInterface({ + --State + gameName = t.string, + screenSize = t.Vector2, + humanoidDescription = t.instanceOf("HumanoidDescription"), + assetNames = t.array(t.string), + rigType = t.enum(Enum.HumanoidRigType), + --Dispatch + performSaveAvatar = t.callback, + signalSaveAvatarPermissionDenied = t.callback, +}) + +function SaveAvatarPrompt:init() + self.middleContentRef = Roact.createRef() + self.contentSize, self.updateContentSize = Roact.createBinding(UDim2.new(1, 0, 0, 200)) + + self.onAlertSizeChanged = function(rbx) + local alertSize = rbx.AbsoluteSize + + if not self.middleContentRef:getValue() then + return + end + + local currentHeight = self.middleContentRef:getValue().AbsoluteSize.Y + local alertNoContentHeight = alertSize.Y - currentHeight + local maxAllowedContentHeight = self.props.screenSize.Y - (SCREEN_SIZE_PADDING * 2) - alertNoContentHeight + + local halfWidth = self.middleContentRef:getValue().AbsoluteSize.X / 2 + local viewportMaxSize = halfWidth - ( VIEWPORT_SIDE_PADDING * 2) + (VIEWPORT_MAX_TOP_PADDING * 2) + + if maxAllowedContentHeight > viewportMaxSize then + maxAllowedContentHeight = viewportMaxSize + end + + if currentHeight ~= maxAllowedContentHeight then + self.updateContentSize(UDim2.new(1, 0, 0, maxAllowedContentHeight)) + end + end + + self.renderAlertMiddleContent = function() + return Roact.createElement("Frame", { + BackgroundTransparency = 1, + Size = self.contentSize, + + [Roact.Ref] = self.middleContentRef, + }, { + ItemsListFrame = Roact.createElement("Frame", { + BackgroundTransparency = 1, + Size = UDim2.fromScale(0.5, 1), + }, { + ItemsList = Roact.createElement(ItemsList, { + assetNames = self.props.assetNames, + }), + }), + + HumanoidViewportFrame = Roact.createElement("Frame", { + BackgroundTransparency = 1, + Size = UDim2.fromScale(0.5, 1), + Position = UDim2.fromScale(0.5, 0), + LayoutOrder = 2, + }, { + UIPadding = Roact.createElement("UIPadding", { + PaddingLeft = UDim.new(0, VIEWPORT_SIDE_PADDING), + PaddingRight = UDim.new(0, VIEWPORT_SIDE_PADDING), + }), + + HumanoidViewport = Roact.createElement(HumanoidViewport, { + humanoidDescription = self.props.humanoidDescription, + rigType = self.props.rigType, + }), + }), + + UISizeConstraint = Roact.createElement("UISizeConstraint", { + MaxSize = self.contentMaxSize, + }), + }) + end +end + +function SaveAvatarPrompt:render() + return Roact.createElement(InteractiveAlert, { + title = RobloxTranslator:FormatByKey("CoreScripts.AvatarEditorPrompts.SaveAvatarPromptTitle"), + bodyText = RobloxTranslator:FormatByKey("CoreScripts.AvatarEditorPrompts.SaveAvatarPromptText", { + RBX_NAME = self.props.gameName, + }), + buttonStackInfo = { + buttons = { + { + props = { + onActivated = self.props.signalSaveAvatarPermissionDenied, + text = RobloxTranslator:FormatByKey("CoreScripts.AvatarEditorPrompts.SaveAvatarPromptNo"), + }, + }, + { + buttonType = ButtonType.PrimarySystem, + props = { + onActivated = self.props.performSaveAvatar, + text = RobloxTranslator:FormatByKey("CoreScripts.AvatarEditorPrompts.SaveAvatarPromptYes"), + }, + }, + }, + }, + position = UDim2.fromScale(0.5, 0.5), + screenSize = self.props.screenSize, + middleContent = self.renderAlertMiddleContent, + onAbsoluteSizeChanged = self.onAlertSizeChanged, + isMiddleContentFocusable = false, + }) +end + +local function mapStateToProps(state) + return { + gameName = state.gameName, + screenSize = state.screenSize, + humanoidDescription = state.promptInfo.humanoidDescription, + rigType = state.promptInfo.rigType, + assetNames = state.promptInfo.assetNames, + } +end + +local function mapDispatchToProps(dispatch) + return { + signalSaveAvatarPermissionDenied = function() + return dispatch(SignalSaveAvatarPermissionDenied) + end, + + performSaveAvatar = function() + return dispatch(PerformSaveAvatar) + end, + } +end + +return RoactRodux.UNSTABLE_connect2(mapStateToProps, mapDispatchToProps)(SaveAvatarPrompt) \ No newline at end of file diff --git a/scripts/CoreScripts/Modules/AvatarEditorPrompts/Components/Prompts/SaveAvatarPrompt.spec.lua b/scripts/CoreScripts/Modules/AvatarEditorPrompts/Components/Prompts/SaveAvatarPrompt.spec.lua new file mode 100644 index 0000000000..fa0bcf3ad6 --- /dev/null +++ b/scripts/CoreScripts/Modules/AvatarEditorPrompts/Components/Prompts/SaveAvatarPrompt.spec.lua @@ -0,0 +1,55 @@ +return function() + local CorePackages = game:GetService("CorePackages") + + local AppDarkTheme = require(CorePackages.AppTempCommon.LuaApp.Style.Themes.DarkTheme) + local AppFont = require(CorePackages.AppTempCommon.LuaApp.Style.Fonts.Gotham) + + local Roact = require(CorePackages.Roact) + local Rodux = require(CorePackages.Rodux) + local RoactRodux = require(CorePackages.RoactRodux) + local UIBlox = require(CorePackages.UIBlox) + + local AvatarEditorPrompts = script.Parent.Parent.Parent + local Reducer = require(AvatarEditorPrompts.Reducer) + local PromptType = require(AvatarEditorPrompts.PromptType) + local OpenPrompt = require(AvatarEditorPrompts.Actions.OpenPrompt) + + local appStyle = { + Theme = AppDarkTheme, + Font = AppFont, + } + + describe("SaveAvatarPrompt", function() + it("should create and destroy without errors", function() + local SaveAvatarPrompt = require(script.Parent.SaveAvatarPrompt) + + local store = Rodux.Store.new(Reducer, nil, { + Rodux.thunkMiddleware, + }) + + local humanoidDescription = Instance.new("HumanoidDescription") + + store:dispatch(OpenPrompt(PromptType.SaveAvatar, { + humanoidDescription = humanoidDescription, + rigType = Enum.HumanoidRigType.R15, + assetNames = { + "Test Asset 1", + "Test Asset 2" + }, + })) + + local element = Roact.createElement(RoactRodux.StoreProvider, { + store = store, + }, { + ThemeProvider = Roact.createElement(UIBlox.Style.Provider, { + style = appStyle, + }, { + SaveAvatarPrompt = Roact.createElement(SaveAvatarPrompt) + }) + }) + + local instance = Roact.mount(element) + Roact.unmount(instance) + end) + end) +end diff --git a/scripts/CoreScripts/Modules/AvatarEditorPrompts/Components/Prompts/SetFavoritePrompt.lua b/scripts/CoreScripts/Modules/AvatarEditorPrompts/Components/Prompts/SetFavoritePrompt.lua new file mode 100644 index 0000000000..8eeb236583 --- /dev/null +++ b/scripts/CoreScripts/Modules/AvatarEditorPrompts/Components/Prompts/SetFavoritePrompt.lua @@ -0,0 +1,132 @@ +local CorePackages = game:GetService("CorePackages") +local CoreGui = game:GetService("CoreGui") + +local Roact = require(CorePackages.Roact) +local RoactRodux = require(CorePackages.RoactRodux) +local t = require(CorePackages.Packages.t) +local UIBlox = require(CorePackages.UIBlox) + +local RobloxGui = CoreGui:WaitForChild("RobloxGui") +local RobloxTranslator = require(RobloxGui.Modules.RobloxTranslator) + +local InteractiveAlert = UIBlox.App.Dialog.Alert.InteractiveAlert +local ButtonType = UIBlox.App.Button.Enum.ButtonType +local LoadableImage = UIBlox.App.Loading.LoadableImage + +local Components = script.Parent.Parent +local AvatarEditorPrompts = Components.Parent + +local PerformSetFavorite = require(AvatarEditorPrompts.Thunks.PerformSetFavorite) +local SignalSetFavoritePermissionDenied = require(AvatarEditorPrompts.Thunks.SignalSetFavoritePermissionDenied) + +local SetFavoritePrompt = Roact.PureComponent:extend("SetFavoritePrompt") + +SetFavoritePrompt.validateProps = t.strictInterface({ + --State + itemId = t.integer, + --TODO: Fix after rebuilding robloxdev-cli + --itemType = t.enum(Enum.AvatarItemType), + itemName = t.string, + shouldFavorite = t.boolean, + screenSize = t.Vector2, + --Dispatch + performSetFavorite = t.callback, + signalSetFavoritePermissionDenied = t.callback, +}) + +function SetFavoritePrompt:init() + self.renderAlertMiddleContent = function() + local thumbnailType = "Asset" + --TODO: Fix after rebuilding robloxdev-cli + --[[ + if self.props.itemType == Enum.AvatarItemType.Asset then + thumbnailType = "Asset" + elseif self.props.itemType == Enum.AvatarItemType.Bundle then + thumbnailType = "BundleThumbnail" + end + ]] + + local imageUrl = "rbxthumb://type=" ..thumbnailType.. "&id=" ..self.props.itemId.. "&w=150&h=150" + + return Roact.createElement("Frame", { + BackgroundTransparency = 1, + Size = UDim2.new(1, 0, 0, 150), + }, { + Thumnail = Roact.createElement(LoadableImage, { + Position = UDim2.fromScale(0.5, 0), + AnchorPoint = Vector2.new(0.5, 0), + Size = UDim2.fromOffset(150, 150), + BackgroundTransparency = 1, + Image = imageUrl, + useShimmerAnimationWhileLoading = true, + showFailedStateWhenLoadingFailed = true, + }), + }) + end +end + +function SetFavoritePrompt:render() + local title + local text + if self.props.shouldFavorite then + title = RobloxTranslator:FormatByKey("CoreScripts.AvatarEditorPrompts.FavouriteItemPromptTitle") + text = RobloxTranslator:FormatByKey("CoreScripts.AvatarEditorPrompts.FavouriteItemPromptText", { + RBX_NAME = self.props.itemName, + }) + else + title = RobloxTranslator:FormatByKey("CoreScripts.AvatarEditorPrompts.UnfavouriteItemPromptTitle") + text = RobloxTranslator:FormatByKey("CoreScripts.AvatarEditorPrompts.UnfavouriteItemPromptText", { + RBX_NAME = self.props.itemName, + }) + end + + return Roact.createElement(InteractiveAlert, { + title = title, + bodyText = text, + buttonStackInfo = { + buttons = { + { + props = { + onActivated = self.props.signalSetFavoritePermissionDenied, + text = RobloxTranslator:FormatByKey("CoreScripts.AvatarEditorPrompts.FavouriteItemPromptNo"), + }, + }, + { + buttonType = ButtonType.PrimarySystem, + props = { + onActivated = self.props.performSetFavorite, + text = RobloxTranslator:FormatByKey("CoreScripts.AvatarEditorPrompts.FavouriteItemPromptYes"), + }, + }, + }, + }, + position = UDim2.fromScale(0.5, 0.5), + screenSize = self.props.screenSize, + middleContent = self.renderAlertMiddleContent, + isMiddleContentFocusable = false, + }) +end + +local function mapStateToProps(state) + return { + itemId = state.promptInfo.itemId, + itemType = state.promptInfo.itemType, + itemName = state.promptInfo.itemName, + shouldFavorite = state.promptInfo.shouldFavorite, + screenSize = state.screenSize, + } +end + +local function mapDispatchToProps(dispatch) + return { + performSetFavorite = function() + return dispatch(PerformSetFavorite) + end, + + signalSetFavoritePermissionDenied = function() + return dispatch(SignalSetFavoritePermissionDenied) + end, + } +end + +return RoactRodux.UNSTABLE_connect2(mapStateToProps, mapDispatchToProps)(SetFavoritePrompt) \ No newline at end of file diff --git a/scripts/CoreScripts/Modules/AvatarEditorPrompts/Components/Prompts/SetFavoritePrompt.spec.lua b/scripts/CoreScripts/Modules/AvatarEditorPrompts/Components/Prompts/SetFavoritePrompt.spec.lua new file mode 100644 index 0000000000..55e8003370 --- /dev/null +++ b/scripts/CoreScripts/Modules/AvatarEditorPrompts/Components/Prompts/SetFavoritePrompt.spec.lua @@ -0,0 +1,52 @@ +return function() + local CorePackages = game:GetService("CorePackages") + + local AppDarkTheme = require(CorePackages.AppTempCommon.LuaApp.Style.Themes.DarkTheme) + local AppFont = require(CorePackages.AppTempCommon.LuaApp.Style.Fonts.Gotham) + + local Roact = require(CorePackages.Roact) + local Rodux = require(CorePackages.Rodux) + local RoactRodux = require(CorePackages.RoactRodux) + local UIBlox = require(CorePackages.UIBlox) + + local AvatarEditorPrompts = script.Parent.Parent.Parent + local Reducer = require(AvatarEditorPrompts.Reducer) + local PromptType = require(AvatarEditorPrompts.PromptType) + local OpenPrompt = require(AvatarEditorPrompts.Actions.OpenPrompt) + + local appStyle = { + Theme = AppDarkTheme, + Font = AppFont, + } + + describe("SetFavoritePrompt", function() + --TODO: Fix after rebuilding robloxdev-cli + itFIXME("should create and destroy without errors", function() + local SetFavoritePrompt = require(script.Parent.SetFavoritePrompt) + + local store = Rodux.Store.new(Reducer, nil, { + Rodux.thunkMiddleware, + }) + + store:dispatch(OpenPrompt(PromptType.SetFavorite, { + itemId = 1337, + itemType = Enum.AvatarItemType.Bundle, + itemName = "Cool Bundle", + shouldFavorite = true, + })) + + local element = Roact.createElement(RoactRodux.StoreProvider, { + store = store, + }, { + ThemeProvider = Roact.createElement(UIBlox.Style.Provider, { + style = appStyle, + }, { + SetFavoritePrompt = Roact.createElement(SetFavoritePrompt) + }) + }) + + local instance = Roact.mount(element) + Roact.unmount(instance) + end) + end) +end diff --git a/scripts/CoreScripts/Modules/AvatarEditorPrompts/GetAssetNamesFromDescription.lua b/scripts/CoreScripts/Modules/AvatarEditorPrompts/GetAssetNamesFromDescription.lua new file mode 100644 index 0000000000..98fcef0b83 --- /dev/null +++ b/scripts/CoreScripts/Modules/AvatarEditorPrompts/GetAssetNamesFromDescription.lua @@ -0,0 +1,75 @@ +local CorePackages = game:GetService("CorePackages") +local MarketplaceService = game:GetService("MarketplaceService") + +local Promise = require(CorePackages.Promise) + +local HumanoidDescriptionAssetProperties = { + "BackAccessory", + "ClimbAnimation", + "Face", + "FaceAccessory", + "FallAnimation", + "FrontAccessory", + "GraphicTShirt", + "HairAccessory", + "HatAccessory", + "Head", + "IdleAnimation", + "JumpAnimation", + "LeftArm", + "LeftLeg", + "NeckAccessory", + "Pants", + "RightArm", + "RightLeg", + "RunAnimation", + "Shirt", + "ShouldersAccessory", + "SwimAnimation", + "Torso", + "WaistAccessory", + "WalkAnimation", +} + +local function GetAssetInfo(assetId) + return Promise.new(function(resolve, reject) + local success, result = pcall(function() + return MarketplaceService:GetProductInfo(assetId, Enum.InfoType.Asset) + end) + + if success then + resolve(result.Name) + else + reject() + end + end) +end + +return function(humanoidDescription) + local assetIdList = {} + + for _, propName in ipairs(HumanoidDescriptionAssetProperties) do + local prop = humanoidDescription[propName] + if typeof(prop) == "number" and prop > 0 then + table.insert(assetIdList, prop) + elseif typeof(prop) == "string" and prop ~= "" then + for match in prop:gmatch("([%d]+),?") do + table.insert(assetIdList, tonumber(match)) + end + end + end + + local emotesIds = humanoidDescription:GetEquippedEmotes() + for _, emoteId in pairs(emotesIds) do + if emoteId and emoteId > 0 then + table.insert(assetIdList, emoteId) + end + end + + local promises = {} + for _, assetId in ipairs(assetIdList) do + table.insert(promises, GetAssetInfo(assetId)) + end + + return Promise.all(promises) +end \ No newline at end of file diff --git a/scripts/CoreScripts/Modules/AvatarEditorPrompts/PromptType.lua b/scripts/CoreScripts/Modules/AvatarEditorPrompts/PromptType.lua new file mode 100644 index 0000000000..ba8e2253ce --- /dev/null +++ b/scripts/CoreScripts/Modules/AvatarEditorPrompts/PromptType.lua @@ -0,0 +1,9 @@ +local CorePackages = game:GetService("CorePackages") +local enumerate = require(CorePackages.enumerate) + +return enumerate("PromptType", { + "AllowInventoryReadAccess", + "SaveAvatar", + "CreateOutfit", + "SetFavorite", +}) \ No newline at end of file diff --git a/scripts/CoreScripts/Modules/AvatarEditorPrompts/Reducer/GameName.lua b/scripts/CoreScripts/Modules/AvatarEditorPrompts/Reducer/GameName.lua new file mode 100644 index 0000000000..924655b7e6 --- /dev/null +++ b/scripts/CoreScripts/Modules/AvatarEditorPrompts/Reducer/GameName.lua @@ -0,0 +1,19 @@ +local CorePackages = game:GetService("CorePackages") +local CoreGui = game:GetService("CoreGui") + +local Rodux = require(CorePackages.Rodux) + +local RobloxGui = CoreGui:WaitForChild("RobloxGui") +local RobloxTranslator = require(RobloxGui.Modules.RobloxTranslator) + +local Actions = script.Parent.Parent.Actions + +local GameNameFetched = require(Actions.GameNameFetched) + +local DefaultPlaceHolderName = RobloxTranslator:FormatByKey("CoreScripts.AvatarEditorPrompts.GameNamePlaceHolder") + +return Rodux.createReducer(DefaultPlaceHolderName, { + [GameNameFetched.name] = function(state, action) + return action.gameName + end, +}) \ No newline at end of file diff --git a/scripts/CoreScripts/Modules/AvatarEditorPrompts/Reducer/PromptInfo.lua b/scripts/CoreScripts/Modules/AvatarEditorPrompts/Reducer/PromptInfo.lua new file mode 100644 index 0000000000..bc391b763e --- /dev/null +++ b/scripts/CoreScripts/Modules/AvatarEditorPrompts/Reducer/PromptInfo.lua @@ -0,0 +1,57 @@ +local CorePackages = game:GetService("CorePackages") + +local Rodux = require(CorePackages.Rodux) +local Cryo = require(CorePackages.Cryo) + +local Reducer = script.Parent +local AvatarEditorPrompts = Reducer.Parent + +local CloseOpenPrompt = require(AvatarEditorPrompts.Actions.CloseOpenPrompt) +local OpenPrompt = require(AvatarEditorPrompts.Actions.OpenPrompt) + +local initialInfo = { + promptType = nil, + --PromptSaveAvatar and PromptCreateOutfit + humanoidDescription = nil, + rigType = nil, + --PromptSetFavorite + itemId = nil, + itemType = nil, + itemName = nil, + isFavorited = nil, + + queue = {}, + infoQueue = {} +} + +local PromptInfo = Rodux.createReducer(initialInfo, { + [CloseOpenPrompt.name] = function(state, action) + if Cryo.isEmpty(state.queue) then + return { + queue = state.queue, + infoQueue = state.infoQueue, + } + end + + return Cryo.Dictionary.join(state.infoQueue[1], { + promptType = state.queue[1], + queue = Cryo.List.removeIndex(state.queue, 1), + infoQueue = Cryo.List.removeIndex(state.infoQueue, 1), + }) + end, + + [OpenPrompt.name] = function(state, action) + if state.promptType == nil then + return Cryo.Dictionary.join(state, { + promptType = action.promptType, + }, action.promptInfo) + end + + return Cryo.Dictionary.join(state, { + queue = Cryo.List.join(state.queue, {action.promptType}), + infoQueue = Cryo.List.join(state.infoQueue, {action.promptInfo}) + }) + end, +}) + +return PromptInfo diff --git a/scripts/CoreScripts/Modules/AvatarEditorPrompts/Reducer/PromptInfo.spec.lua b/scripts/CoreScripts/Modules/AvatarEditorPrompts/Reducer/PromptInfo.spec.lua new file mode 100644 index 0000000000..b225140624 --- /dev/null +++ b/scripts/CoreScripts/Modules/AvatarEditorPrompts/Reducer/PromptInfo.spec.lua @@ -0,0 +1,190 @@ +return function() + local PromptInfo = require(script.Parent.PromptInfo) + + local AvatarEditorPrompts = script.Parent.Parent + local Actions = AvatarEditorPrompts.Actions + local CloseOpenPrompt = require(Actions.CloseOpenPrompt) + local OpenPrompt = require(Actions.OpenPrompt) + + local PromptType = require(AvatarEditorPrompts.PromptType) + + local function countValues(t) + local c = 0 + for _, _ in pairs(t) do + c = c + 1 + end + return c + end + + it("should have the correct default values", function() + local defaultState = PromptInfo(nil, {}) + expect(type(defaultState)).to.equal("table") + expect(type(defaultState.queue)).to.equal("table") + expect(type(defaultState.infoQueue)).to.equal("table") + expect(countValues(defaultState)).to.equal(2) + expect(countValues(defaultState.queue)).to.equal(0) + expect(countValues(defaultState.infoQueue)).to.equal(0) + end) + + describe("OpenPrompt", function() + it("should correctly open PromptType.SaveAvatar", function() + local humanoidDescription = Instance.new("HumanoidDescription") + + local oldState = PromptInfo(nil, {}) + local newState = PromptInfo(oldState, OpenPrompt( + PromptType.SaveAvatar, + { + humanoidDescription = humanoidDescription, + rigType = Enum.HumanoidRigType.R15, + } + )) + expect(oldState).to.never.equal(newState) + expect(countValues(newState.queue)).to.equal(0) + expect(countValues(newState.infoQueue)).to.equal(0) + + expect(newState.promptType).to.equal(PromptType.SaveAvatar) + expect(newState.humanoidDescription).to.equal(humanoidDescription) + expect(newState.rigType).to.equal(humanoidDescription) + end) + + it("should correctly open PromptType.CreateOutfit", function() + local humanoidDescription = Instance.new("HumanoidDescription") + + local oldState = PromptInfo(nil, {}) + local newState = PromptInfo(oldState, OpenPrompt( + PromptType.CreateOutfit, + { + humanoidDescription = humanoidDescription, + rigType = Enum.HumanoidRigType.R15, + } + )) + expect(oldState).to.never.equal(newState) + expect(countValues(newState.queue)).to.equal(0) + expect(countValues(newState.infoQueue)).to.equal(0) + + expect(newState.promptType).to.equal(PromptType.CreateOutfit) + expect(newState.humanoidDescription).to.equal(humanoidDescription) + expect(newState.rigType).to.equal(humanoidDescription) + end) + + it("should correctly open PromptType.AllowInventoryReadAccess", function() + local oldState = PromptInfo(nil, {}) + local newState = PromptInfo(oldState, OpenPrompt( + PromptType.AllowInventoryReadAccess, + {} + )) + expect(oldState).to.never.equal(newState) + expect(newState).to.equal(3) + expect(countValues(newState.queue)).to.equal(0) + expect(countValues(newState.infoQueue)).to.equal(0) + + expect(newState.promptType).to.equal(PromptType.AllowInventoryReadAccess) + end) + + it("should correctly open PromptType.SetFavorite", function() + local oldState = PromptInfo(nil, {}) + local newState = PromptInfo(oldState, OpenPrompt( + PromptType.SetFavorite, + { + itemId = 1337, + itemType = Enum.AvatarItemType.Bundle, + itemName = "Cool Bundle", + isFavorited = true, + } + )) + expect(oldState).to.never.equal(newState) + expect(countValues(newState.queue)).to.equal(0) + expect(countValues(newState.infoQueue)).to.equal(0) + + expect(newState.promptType).to.equal(PromptType.SetFavorite) + expect(newState.itemId).to.equal(1337) + expect(newState.itemType).to.equal(Enum.AvatarItemType.Bundle) + expect(newState.itemName).to.equal("Cool Bundle") + expect(newState.isFavorited).to.equal(true) + end) + + it("should add a prompt to the queue if a prompt is already open", function() + local oldState = PromptInfo(nil, {}) + oldState = PromptInfo(oldState, OpenPrompt( + PromptType.SetFavorite, + { + itemId = 1337, + itemType = Enum.AvatarItemType.Bundle, + itemName = "Cool Bundle", + isFavorited = true, + } + )) + + local humanoidDescription = Instance.new("HumanoidDescription") + + local newState = PromptInfo(oldState, OpenPrompt( + PromptType.CreateOutfit, + { + humanoidDescription = humanoidDescription, + rigType = Enum.HumanoidRigType.R15, + } + )) + expect(oldState).to.never.equal(newState) + expect(countValues(newState.queue)).to.equal(1) + expect(countValues(newState.infoQueue)).to.equal(1) + expect(newState.queue[1]).to.equal(PromptType.CreateOutfit) + expect(newState.infoQueue[1].humanoidDescription).to.equal(humanoidDescription) + expect(newState.infoQueue[1].rigType).to.equal(Enum.HumanoidRigType.R15) + end) + end) + + describe("CloseOpenPrompt", function() + it("should revert back to the default values if the queue is empty", function() + local oldState = PromptInfo(nil, {}) + local newState = PromptInfo(oldState, OpenPrompt( + PromptType.SaveAvatar, + { + humanoidDescription = Instance.new("HumanoidDescription"), + rigType = Enum.HumanoidRigType.R15, + } + )) + expect(oldState).to.never.equal(newState) + newState = PromptInfo(newState, CloseOpenPrompt()) + expect(type(newState.queue)).to.equal("table") + expect(type(newState.infoQueue)).to.equal("table") + expect(countValues(newState)).to.equal(2) + expect(countValues(newState.queue)).to.equal(0) + expect(countValues(newState.infoQueue)).to.equal(0) + end) + + it("should switch to the next prompt info in the queue if the queue isn't empty", function() + local oldState = PromptInfo(nil, {}) + oldState = PromptInfo(oldState, OpenPrompt( + PromptType.SaveAvatar, + { + humanoidDescription = Instance.new("HumanoidDescription"), + rigType = Enum.HumanoidRigType.R15, + } + )) + local newState = PromptInfo(oldState, OpenPrompt( + PromptType.SetFavorite, + { + itemId = 1337, + itemType = Enum.AvatarItemType.Bundle, + itemName = "Cool Bundle", + isFavorited = true, + } + )) + expect(oldState).to.never.equal(newState) + expect(countValues(newState.queue)).to.equal(1) + expect(countValues(newState.infoQueue)).to.equal(1) + + newState = PromptInfo(newState, CloseOpenPrompt()) + expect(type(newState.queue)).to.equal("table") + expect(type(newState.infoQueue)).to.equal("table") + expect(countValues(newState.queue)).to.equal(0) + expect(countValues(newState.infoQueue)).to.equal(0) + + expect(newState.promptType).to.equal(PromptType.SetFavorite) + expect(newState.itemId).to.equal(1337) + expect(newState.itemType).to.equal(Enum.AvatarItemType.Bundle) + expect(newState.itemName).to.equal("Cool Bundle") + expect(newState.isFavorited).to.equal(true) + end) + end) +end \ No newline at end of file diff --git a/scripts/CoreScripts/Modules/AvatarEditorPrompts/Reducer/ScreenSize.lua b/scripts/CoreScripts/Modules/AvatarEditorPrompts/Reducer/ScreenSize.lua new file mode 100644 index 0000000000..51b4652e79 --- /dev/null +++ b/scripts/CoreScripts/Modules/AvatarEditorPrompts/Reducer/ScreenSize.lua @@ -0,0 +1,16 @@ +local CorePackages = game:GetService("CorePackages") + +local Rodux = require(CorePackages.Rodux) + +local Reducer = script.Parent +local AvatarEditorPrompts = Reducer.Parent + +local ScreenSizeUpdated = require(AvatarEditorPrompts.Actions.ScreenSizeUpdated) + +local ScreenSize = Rodux.createReducer(Vector2.new(0, 0), { + [ScreenSizeUpdated.name] = function(state, action) + return action.screenSize + end, +}) + +return ScreenSize diff --git a/scripts/CoreScripts/Modules/AvatarEditorPrompts/Reducer/ScreenSize.spec.lua b/scripts/CoreScripts/Modules/AvatarEditorPrompts/Reducer/ScreenSize.spec.lua new file mode 100644 index 0000000000..32b4bcf8b8 --- /dev/null +++ b/scripts/CoreScripts/Modules/AvatarEditorPrompts/Reducer/ScreenSize.spec.lua @@ -0,0 +1,21 @@ +return function() + local ScreenSize = require(script.Parent.ScreenSize) + + local AvatarEditorPrompts = script.Parent.Parent + local Actions = AvatarEditorPrompts.Actions + local ScreenSizeUpdated = require(Actions.ScreenSizeUpdated) + + it("should have the correct default value", function() + local defaultState = ScreenSize(nil, {}) + expect(defaultState).to.equal(Vector2.new(0, 0)) + end) + + describe("ScreenSizeUpdated", function() + it("should change the value screenSize", function() + local oldState = ScreenSize(nil, {}) + local newState = ScreenSize(oldState, ScreenSizeUpdated(Vector2.new(1500, 1200))) + expect(oldState).to.never.equal(newState) + expect(newState).to.equal(Vector2.new(1500, 1200)) + end) + end) +end \ No newline at end of file diff --git a/scripts/CoreScripts/Modules/AvatarEditorPrompts/Reducer/init.lua b/scripts/CoreScripts/Modules/AvatarEditorPrompts/Reducer/init.lua new file mode 100644 index 0000000000..1fffbe24b8 --- /dev/null +++ b/scripts/CoreScripts/Modules/AvatarEditorPrompts/Reducer/init.lua @@ -0,0 +1,15 @@ +local CorePackages = game:GetService("CorePackages") + +local Rodux = require(CorePackages.Rodux) + +local PromptInfo = require(script.PromptInfo) +local ScreenSize = require(script.ScreenSize) +local GameName = require(script.GameName) + +local Reducer = Rodux.combineReducers({ + promptInfo = PromptInfo, + screenSize = ScreenSize, + gameName = GameName, +}) + +return Reducer \ No newline at end of file diff --git a/scripts/CoreScripts/Modules/AvatarEditorPrompts/RoactGlobalConfig.lua b/scripts/CoreScripts/Modules/AvatarEditorPrompts/RoactGlobalConfig.lua new file mode 100644 index 0000000000..4875611840 --- /dev/null +++ b/scripts/CoreScripts/Modules/AvatarEditorPrompts/RoactGlobalConfig.lua @@ -0,0 +1,4 @@ +return { + propValidation = false, + elementTracing = false, +} \ No newline at end of file diff --git a/scripts/CoreScripts/Modules/AvatarEditorPrompts/Thunks/CloseOpenPrompt.lua b/scripts/CoreScripts/Modules/AvatarEditorPrompts/Thunks/CloseOpenPrompt.lua new file mode 100644 index 0000000000..bf2eb47bf1 --- /dev/null +++ b/scripts/CoreScripts/Modules/AvatarEditorPrompts/Thunks/CloseOpenPrompt.lua @@ -0,0 +1,22 @@ +local AvatarEditorService = game:GetService("AvatarEditorService") + +local AvatarEditorPrompts = script.Parent.Parent +local CloseOpenPrompt = require(AvatarEditorPrompts.Actions.CloseOpenPrompt) + +local PromptType = require(AvatarEditorPrompts.PromptType) + +return function(store) + local openPromptType = store:getState().promptInfo.promptType + + if openPromptType == PromptType.AllowInventoryReadAccess then + AvatarEditorService:SetAllowInventoryReadAccess(false) + elseif openPromptType == PromptType.SaveAvatar then + AvatarEditorService:SignalSaveAvatarPermissionDenied() + elseif openPromptType == PromptType.CreateOutfit then + AvatarEditorService:SignalCreateOutfitPermissionDenied() + elseif openPromptType == PromptType.SetFavorite then + AvatarEditorService:SignalSetFavoritePermissionDenied() + end + + store:dispatch(CloseOpenPrompt()) +end \ No newline at end of file diff --git a/scripts/CoreScripts/Modules/AvatarEditorPrompts/Thunks/GetGameName.lua b/scripts/CoreScripts/Modules/AvatarEditorPrompts/Thunks/GetGameName.lua new file mode 100644 index 0000000000..5cc5b5dc69 --- /dev/null +++ b/scripts/CoreScripts/Modules/AvatarEditorPrompts/Thunks/GetGameName.lua @@ -0,0 +1,40 @@ +local CoreGui = game:GetService("CoreGui") +local CorePackages = game:GetService("CorePackages") +local HttpRbxApiService = game:GetService("HttpRbxApiService") +local LocalizationService = game:GetService("LocalizationService") + +local RobloxGui = CoreGui:WaitForChild("RobloxGui") + +local httpRequest = require(CorePackages.AppTempCommon.Temp.httpRequest) + +local httpImpl = httpRequest(HttpRbxApiService) + +local Thunks = script.Parent +local AvatarEditorPrompts = Thunks.Parent +local GameNameFetched = require(AvatarEditorPrompts.Actions.GameNameFetched) + +local GetGameNameAndDescription = require(RobloxGui.Modules.Common.GetGameNameAndDescription) + +return function(store) + coroutine.wrap(function() + if game.GameId == 0 then + return + end + + GetGameNameAndDescription(httpImpl, game.GameId):andThen(function( + gameNameLocaleMap, gameDescriptionsLocaleMap, sourceLocale) + + local localeGameName = gameNameLocaleMap[LocalizationService.RobloxLocaleId] + if localeGameName then + return store:dispatch(GameNameFetched(localeGameName)) + end + + local sourceGameName = gameNameLocaleMap[sourceLocale] + if sourceGameName then + return store:dispatch(GameNameFetched(sourceGameName)) + end + end):catch(function() + warn("Unable to get game name for Avatar Editor Prompts") + end) + end)() +end \ No newline at end of file diff --git a/scripts/CoreScripts/Modules/AvatarEditorPrompts/Thunks/OpenSaveAvatarPrompt.lua b/scripts/CoreScripts/Modules/AvatarEditorPrompts/Thunks/OpenSaveAvatarPrompt.lua new file mode 100644 index 0000000000..f4840763a2 --- /dev/null +++ b/scripts/CoreScripts/Modules/AvatarEditorPrompts/Thunks/OpenSaveAvatarPrompt.lua @@ -0,0 +1,28 @@ +local AvatarEditorService = game:GetService("AvatarEditorService") +local CorePackages = game:GetService("CorePackages") + +local Promise = require(CorePackages.Promise) + +local AvatarEditorPrompts = script.Parent.Parent +local OpenPrompt = require(AvatarEditorPrompts.Actions.OpenPrompt) +local GetAssetNamesFromDescription = require(AvatarEditorPrompts.GetAssetNamesFromDescription) + +local PromptType = require(AvatarEditorPrompts.PromptType) + +return function(humanoidDescription, rigType) + return function(store) + return GetAssetNamesFromDescription(humanoidDescription):andThen(function(assetNames) + store:dispatch(OpenPrompt(PromptType.SaveAvatar, { + humanoidDescription = humanoidDescription, + rigType = rigType, + assetNames = assetNames, + })) + + return Promise.resolve(assetNames) + end, function() + AvatarEditorService:SignalSaveAvatarFailed() + + return Promise.reject() + end) + end +end \ No newline at end of file diff --git a/scripts/CoreScripts/Modules/AvatarEditorPrompts/Thunks/OpenSetFavoritePrompt.lua b/scripts/CoreScripts/Modules/AvatarEditorPrompts/Thunks/OpenSetFavoritePrompt.lua new file mode 100644 index 0000000000..dc06335a14 --- /dev/null +++ b/scripts/CoreScripts/Modules/AvatarEditorPrompts/Thunks/OpenSetFavoritePrompt.lua @@ -0,0 +1,45 @@ +local AvatarEditorService = game:GetService("AvatarEditorService") +local CorePackages = game:GetService("CorePackages") +local MarketplaceService = game:GetService("MarketplaceService") + +local Promise = require(CorePackages.Promise) + +local AvatarEditorPrompts = script.Parent.Parent +local OpenPrompt = require(AvatarEditorPrompts.Actions.OpenPrompt) + +local PromptType = require(AvatarEditorPrompts.PromptType) + +return function(itemId, itemType, shouldFavorite) + return function(store) + return Promise.new(function(resolve, reject) + local infoType = nil + --TODO: Fix after rebuilding robloxdev-cli + --[[ + if itemType == Enum.AvatarItemType.Asset then + infoType = Enum.InfoType.Asset + else + infoType = Enum.InfoType.Bundle + end + --]] + + local success, result = pcall(function() + return MarketplaceService:GetProductInfo(itemId, infoType) + end) + + if success then + store:dispatch(OpenPrompt(PromptType.SetFavorite, { + itemId = itemId, + itemName = result.Name, + itemType = itemType, + shouldFavorite = shouldFavorite, + })) + + resolve() + else + AvatarEditorService:SignalSetFavoriteFailed() + + reject() + end + end) + end +end \ No newline at end of file diff --git a/scripts/CoreScripts/Modules/AvatarEditorPrompts/Thunks/PerformCreateOutfit.lua b/scripts/CoreScripts/Modules/AvatarEditorPrompts/Thunks/PerformCreateOutfit.lua new file mode 100644 index 0000000000..00fd19398f --- /dev/null +++ b/scripts/CoreScripts/Modules/AvatarEditorPrompts/Thunks/PerformCreateOutfit.lua @@ -0,0 +1,12 @@ +local AvatarEditorService = game:GetService("AvatarEditorService") + +local AvatarEditorPrompts = script.Parent.Parent +local CloseOpenPrompt = require(AvatarEditorPrompts.Actions.CloseOpenPrompt) + +return function(outfitName) + return function(store) + AvatarEditorService:PerformCreateOutfit(outfitName) + + store:dispatch(CloseOpenPrompt()) + end +end \ No newline at end of file diff --git a/scripts/CoreScripts/Modules/AvatarEditorPrompts/Thunks/PerformSaveAvatar.lua b/scripts/CoreScripts/Modules/AvatarEditorPrompts/Thunks/PerformSaveAvatar.lua new file mode 100644 index 0000000000..686913a767 --- /dev/null +++ b/scripts/CoreScripts/Modules/AvatarEditorPrompts/Thunks/PerformSaveAvatar.lua @@ -0,0 +1,10 @@ +local AvatarEditorService = game:GetService("AvatarEditorService") + +local AvatarEditorPrompts = script.Parent.Parent +local CloseOpenPrompt = require(AvatarEditorPrompts.Actions.CloseOpenPrompt) + +return function(store) + AvatarEditorService:PerformSaveAvatar() + + store:dispatch(CloseOpenPrompt()) +end \ No newline at end of file diff --git a/scripts/CoreScripts/Modules/AvatarEditorPrompts/Thunks/PerformSetFavorite.lua b/scripts/CoreScripts/Modules/AvatarEditorPrompts/Thunks/PerformSetFavorite.lua new file mode 100644 index 0000000000..d3a60199a7 --- /dev/null +++ b/scripts/CoreScripts/Modules/AvatarEditorPrompts/Thunks/PerformSetFavorite.lua @@ -0,0 +1,10 @@ +local AvatarEditorService = game:GetService("AvatarEditorService") + +local AvatarEditorPrompts = script.Parent.Parent +local CloseOpenPrompt = require(AvatarEditorPrompts.Actions.CloseOpenPrompt) + +return function(store) + AvatarEditorService:PerformSetFavorite() + + store:dispatch(CloseOpenPrompt()) +end \ No newline at end of file diff --git a/scripts/CoreScripts/Modules/AvatarEditorPrompts/Thunks/SetAllowInventoryReadAccess.lua b/scripts/CoreScripts/Modules/AvatarEditorPrompts/Thunks/SetAllowInventoryReadAccess.lua new file mode 100644 index 0000000000..a81df41cba --- /dev/null +++ b/scripts/CoreScripts/Modules/AvatarEditorPrompts/Thunks/SetAllowInventoryReadAccess.lua @@ -0,0 +1,12 @@ +local AvatarEditorService = game:GetService("AvatarEditorService") + +local AvatarEditorPrompts = script.Parent.Parent +local CloseOpenPrompt = require(AvatarEditorPrompts.Actions.CloseOpenPrompt) + +return function(readAccessAllowed) + return function(store) + AvatarEditorService:SetAllowInventoryReadAccess(readAccessAllowed) + + store:dispatch(CloseOpenPrompt()) + end +end \ No newline at end of file diff --git a/scripts/CoreScripts/Modules/AvatarEditorPrompts/Thunks/SignalCreateOutfitPermissionDenied.lua b/scripts/CoreScripts/Modules/AvatarEditorPrompts/Thunks/SignalCreateOutfitPermissionDenied.lua new file mode 100644 index 0000000000..ae881b80e6 --- /dev/null +++ b/scripts/CoreScripts/Modules/AvatarEditorPrompts/Thunks/SignalCreateOutfitPermissionDenied.lua @@ -0,0 +1,10 @@ +local AvatarEditorService = game:GetService("AvatarEditorService") + +local AvatarEditorPrompts = script.Parent.Parent +local CloseOpenPrompt = require(AvatarEditorPrompts.Actions.CloseOpenPrompt) + +return function(store) + AvatarEditorService:SignalCreateOutfitPermissionDenied() + + store:dispatch(CloseOpenPrompt()) +end \ No newline at end of file diff --git a/scripts/CoreScripts/Modules/AvatarEditorPrompts/Thunks/SignalSaveAvatarPermissionDenied.lua b/scripts/CoreScripts/Modules/AvatarEditorPrompts/Thunks/SignalSaveAvatarPermissionDenied.lua new file mode 100644 index 0000000000..7907985bd3 --- /dev/null +++ b/scripts/CoreScripts/Modules/AvatarEditorPrompts/Thunks/SignalSaveAvatarPermissionDenied.lua @@ -0,0 +1,10 @@ +local AvatarEditorService = game:GetService("AvatarEditorService") + +local AvatarEditorPrompts = script.Parent.Parent +local CloseOpenPrompt = require(AvatarEditorPrompts.Actions.CloseOpenPrompt) + +return function(store) + AvatarEditorService:SignalSaveAvatarPermissionDenied() + + store:dispatch(CloseOpenPrompt()) +end \ No newline at end of file diff --git a/scripts/CoreScripts/Modules/AvatarEditorPrompts/Thunks/SignalSetFavoritePermissionDenied.lua b/scripts/CoreScripts/Modules/AvatarEditorPrompts/Thunks/SignalSetFavoritePermissionDenied.lua new file mode 100644 index 0000000000..a56c4b552b --- /dev/null +++ b/scripts/CoreScripts/Modules/AvatarEditorPrompts/Thunks/SignalSetFavoritePermissionDenied.lua @@ -0,0 +1,10 @@ +local AvatarEditorService = game:GetService("AvatarEditorService") + +local AvatarEditorPrompts = script.Parent.Parent +local CloseOpenPrompt = require(AvatarEditorPrompts.Actions.CloseOpenPrompt) + +return function(store) + AvatarEditorService:SignalSetFavoritePermissionDenied() + + store:dispatch(CloseOpenPrompt()) +end \ No newline at end of file diff --git a/scripts/CoreScripts/Modules/AvatarEditorPrompts/init.lua b/scripts/CoreScripts/Modules/AvatarEditorPrompts/init.lua new file mode 100644 index 0000000000..b515a9987c --- /dev/null +++ b/scripts/CoreScripts/Modules/AvatarEditorPrompts/init.lua @@ -0,0 +1,62 @@ +local CorePackages = game:GetService("CorePackages") +local CoreGui = game:GetService("CoreGui") + +local AppDarkTheme = require(CorePackages.AppTempCommon.LuaApp.Style.Themes.DarkTheme) +local AppFont = require(CorePackages.AppTempCommon.LuaApp.Style.Fonts.Gotham) + +local Roact = require(CorePackages.Roact) +local Rodux = require(CorePackages.Rodux) +local RoactRodux = require(CorePackages.RoactRodux) +local UIBlox = require(CorePackages.UIBlox) + +local AvatarEditorPromptsApp = require(script.Components.AvatarEditorPromptsApp) +local Reducer = require(script.Reducer) + +local GetGameName = require(script.Thunks.GetGameName) + +local RoactGlobalConfig = require(script.RoactGlobalConfig) + +local AvatarEditorPrompts = {} +AvatarEditorPrompts.__index = AvatarEditorPrompts + +function AvatarEditorPrompts.new() + local self = setmetatable({}, AvatarEditorPrompts) + + if RoactGlobalConfig.propValidation then + Roact.setGlobalConfig({ + propValidation = true, + }) + end + if RoactGlobalConfig.elementTracing then + Roact.setGlobalConfig({ + elementTracing = true, + }) + end + + self.store = Rodux.Store.new(Reducer, nil, { + Rodux.thunkMiddleware, + }) + + self.store:dispatch(GetGameName) + + local appStyle = { + Theme = AppDarkTheme, + Font = AppFont, + } + + self.root = Roact.createElement(RoactRodux.StoreProvider, { + store = self.store, + }, { + ThemeProvider = Roact.createElement(UIBlox.Style.Provider, { + style = appStyle, + }, { + AvatarEditorPromptsApp = Roact.createElement(AvatarEditorPromptsApp) + }) + }) + + self.element = Roact.mount(self.root, CoreGui, "AvatarEditorPrompts") + + return self +end + +return AvatarEditorPrompts.new() diff --git a/scripts/CoreScripts/Modules/AvatarEditorPrompts/init.spec.lua b/scripts/CoreScripts/Modules/AvatarEditorPrompts/init.spec.lua new file mode 100644 index 0000000000..176b901f77 --- /dev/null +++ b/scripts/CoreScripts/Modules/AvatarEditorPrompts/init.spec.lua @@ -0,0 +1,6 @@ +return function() + it("should require without errors", function() + local AvatarEditorPrompts = require(script.Parent) + expect(AvatarEditorPrompts).to.be.ok() + end) +end diff --git a/scripts/CoreScripts/Modules/Flags/GetFixGraphicsQuality.lua b/scripts/CoreScripts/Modules/Flags/GetFixGraphicsQuality.lua new file mode 100644 index 0000000000..e23478d6ee --- /dev/null +++ b/scripts/CoreScripts/Modules/Flags/GetFixGraphicsQuality.lua @@ -0,0 +1,5 @@ +game:DefineFastFlag("FixGraphicsQuality", false) + +return function() + return game:GetFastFlag("FixGraphicsQuality") and game:GetEngineFeature("MatchingGraphicsQualityLevels") +end diff --git a/scripts/CoreScripts/Modules/InGameChat/BubbleChat/ChatSettings.lua b/scripts/CoreScripts/Modules/InGameChat/BubbleChat/ChatSettings.lua index 00d348abba..6e58aa6f30 100644 --- a/scripts/CoreScripts/Modules/InGameChat/BubbleChat/ChatSettings.lua +++ b/scripts/CoreScripts/Modules/InGameChat/BubbleChat/ChatSettings.lua @@ -1,15 +1,15 @@ --[[ These are developer-facing settings for BubbleChat that can be configured - through the BubbleChatSettings API. + through the SetBubbleChatSettings API. Every value in this table is the default, which can be overriden by developers at runtime. Usage: - Chat:SetBubbleChatSettings({ + game.Chat:SetBubbleChatSettings({ BubbleDuration = 10, - Maxbubbles = 5, + MaxBubbles = 5, }) ]] @@ -21,13 +21,27 @@ return { -- immediately when a new message comes in. MaxBubbles = 3, - -- Styling for the bubbles. Gives the developer some themeing options - BackgroundColor3 = Color3.fromRGB(255, 255, 255), + -- Styling for the bubbles. These settings will change various visual aspects. + BackgroundColor3 = Color3.fromRGB(250, 250, 250), + TextColor3 = Color3.fromRGB(57, 59, 61), + TextSize = 16, + Font = Enum.Font.GothamSemibold, + Transparency = .1, + CornerRadius = UDim.new(0, 12), + TailVisible = true, + Padding = 8, -- in pixels + MaxWidth = 300, --in pixels + + -- Extra space between the head and the billboard (useful if you want to + -- leave some space for other character billboard UIs) + VerticalStudsOffset = 0, + + -- Space in pixels between two bubbles + BubblesSpacing = 6, -- The distance (from the camera) that bubbles turn into a single bubble - -- with elipses (...) to indicate chatter. + -- with ellipses (...) to indicate chatter. MinimizeDistance = 40, - -- The max distance (from the camera) that bubbles are shown at MaxDistance = 100, } diff --git a/scripts/CoreScripts/Modules/InGameChat/BubbleChat/Components/BubbleChatBillboard.lua b/scripts/CoreScripts/Modules/InGameChat/BubbleChat/Components/BubbleChatBillboard.lua index 94482e0f64..e5e95d65a3 100644 --- a/scripts/CoreScripts/Modules/InGameChat/BubbleChat/Components/BubbleChatBillboard.lua +++ b/scripts/CoreScripts/Modules/InGameChat/BubbleChat/Components/BubbleChatBillboard.lua @@ -20,11 +20,12 @@ local BubbleChatBillboard = Roact.Component:extend("BubbleChatBillboard") BubbleChatBillboard.validateProps = t.strictInterface({ userId = t.string, + onFadeOut = t.optional(t.callback), -- RoactRodux chatSettings = Types.IChatSettings, messages = t.map(t.string, Types.IMessage), - messageIds = t.array(t.string) + messageIds = t.optional(t.array(t.string)), -- messageIds == nil during the last bubble's fade out animation }) function BubbleChatBillboard:init() @@ -33,6 +34,11 @@ function BubbleChatBillboard:init() isInsideRenderDistance = false, isInsideMaximizeDistance = false } + self.onLastBubbleFadeOut = function() + if self.props.onFadeOut then + self.props.onFadeOut(self.props.userId) + end + end end function BubbleChatBillboard:didMount() @@ -179,9 +185,10 @@ end -- If the billboard is to be attached to a character, return it, -- otherwise return the part specified in lastMessage.adornee function BubbleChatBillboard:getAdornee() - local lastMessageId = self.props.messageIds[#self.props.messageIds] - if not lastMessageId then return end + local messageIds = self.props.messageIds + if not messageIds or #messageIds == 0 then return end + local lastMessageId = messageIds[#messageIds] local lastMessage = self.props.messages[lastMessageId] assert(Types.IMessage(lastMessage)) @@ -198,22 +205,15 @@ function BubbleChatBillboard:getAdorneePart() end function BubbleChatBillboard:render() - local chatSettings = self.props.chatSettings - local messageIds = self.props.messageIds - local lastMessageId = messageIds[#messageIds] - local lastMessage = self.props.messages[lastMessageId] local adornee = self.state.adornee local adorneePart = self:getAdorneePart() + local isLocalPlayer = self.props.userId == tostring(Players.LocalPlayer.UserId) + local settings = self.props.chatSettings if not adorneePart then return end - -- Don't render the Billboard if the user has not sent any messages recently - if os.time() - lastMessage.timestamp > chatSettings.BubbleDuration then - return - end - -- Don't render the billboard at all if out of range. We could use -- the MaxDistance property on the billboard, but that keeps -- instances around. This approach means nothing exists in the DM @@ -226,21 +226,35 @@ function BubbleChatBillboard:render() Adornee = adorneePart, Size = UDim2.fromOffset(500, 200), SizeOffset = Vector2.new(0, 0.5), - -- For other players, increase offset by 1 to prevent overlaps with the name display, same behavior as old bubble chat - StudsOffset = Vector3.new(0, self.props.userId == tostring(Players.LocalPlayer.UserId) and 0 or 1, 0), + -- For other players, increase vertical offset by 1 to prevent overlaps with the name display + -- For the local player, increase Z offset to prevent the character from overlapping his bubbles when jumping/emoting + -- This behavior is the same as the old bubble chat + StudsOffset = Vector3.new(0, (isLocalPlayer and 0 or 1) + settings.VerticalStudsOffset, isLocalPlayer and 2 or 0.1), StudsOffsetWorldSpace = self:getVerticalOffset(adornee), ResetOnSpawn = false, }, { - DistantBubble = not self.state.isInsideMaximizeDistance and Roact.createElement(ChatBubbleDistant), + DistantBubble = not self.state.isInsideMaximizeDistance and Roact.createElement(ChatBubbleDistant, { + fadingOut = not self.props.messageIds or #self.props.messageIds == 0, + onFadeOut = self.onLastBubbleFadeOut, + }), BubbleChatList = self.state.isInsideMaximizeDistance and Roact.createElement(BubbleChatList, { userId = self.props.userId, isVisible = self.state.isInsideMaximizeDistance, + onLastBubbleFadeOut = self.onLastBubbleFadeOut, }) }) end +function BubbleChatBillboard:didUpdate() + -- If self.state.isInsideRenderDistance, the responsibility to call self.onLastBubbleFadeOut will be on either + -- DistantBubble or BubbleChatList (after their fade out animation) + if (not self.props.messageIds or #self.props.messageIds == 0) and not self.state.isInsideRenderDistance then + self.onLastBubbleFadeOut() + end +end + function BubbleChatBillboard:shouldUpdate(nextProps) -- Only update the messages if the user's list of message IDs changed. -- diff --git a/scripts/CoreScripts/Modules/InGameChat/BubbleChat/Components/BubbleChatBillboards.lua b/scripts/CoreScripts/Modules/InGameChat/BubbleChat/Components/BubbleChatBillboards.lua index 843911647b..e7196a53d6 100644 --- a/scripts/CoreScripts/Modules/InGameChat/BubbleChat/Components/BubbleChatBillboards.lua +++ b/scripts/CoreScripts/Modules/InGameChat/BubbleChat/Components/BubbleChatBillboards.lua @@ -8,6 +8,7 @@ local CorePackages = game:GetService("CorePackages") local Roact = require(CorePackages.Packages.Roact) local RoactRodux = require(CorePackages.Packages.RoactRodux) local t = require(CorePackages.Packages.t) +local Cryo = require(CorePackages.Packages.Cryo) local BubbleChatBillboard = require(script.Parent.BubbleChatBillboard) local ChatBillboards = Roact.Component:extend("ChatBillboards") @@ -16,12 +17,33 @@ ChatBillboards.validateProps = t.strictInterface({ userMessages = t.map(t.string, t.array(t.string)) }) +function ChatBillboards.getDerivedStateFromProps(nextProps, lastState) + return { + -- We need to keep in memory userMessages' keys to allow the fade out animations to play, otherwise the child + -- billboards would be unmounted right away. It is their responsibility to clean up by triggering + -- the function self.onBillboardFadeOut + userMessages = Cryo.Dictionary.join(lastState.userMessages or {}, nextProps.userMessages) + } +end + +function ChatBillboards:init() + self:setState({ + userMessages = {} + }) + self.onBillboardFadeOut = function(userId) + self:setState({ + userMessages = Cryo.Dictionary.join(self.state.userMessages, { [userId] = Cryo.None }) + }) + end +end + function ChatBillboards:render() local billboards = {} - for userId, messageIds in pairs(self.props.userMessages) do + for userId, _ in pairs(self.state.userMessages) do billboards["BubbleChat_" .. userId] = Roact.createElement(BubbleChatBillboard, { userId = userId, + onFadeOut = self.onBillboardFadeOut, }) end diff --git a/scripts/CoreScripts/Modules/InGameChat/BubbleChat/Components/BubbleChatList.lua b/scripts/CoreScripts/Modules/InGameChat/BubbleChat/Components/BubbleChatList.lua index 047ffadf6a..cfeea285aa 100644 --- a/scripts/CoreScripts/Modules/InGameChat/BubbleChat/Components/BubbleChatList.lua +++ b/scripts/CoreScripts/Modules/InGameChat/BubbleChat/Components/BubbleChatList.lua @@ -13,6 +13,7 @@ BubbleChatList.validateProps = t.strictInterface({ userId = t.string, isVisible = t.optional(t.boolean), theme = t.optional(t.string), + onLastBubbleFadeOut = t.optional(t.callback), -- RoactRodux chatSettings = Types.IChatSettings, @@ -31,7 +32,7 @@ function BubbleChatList.getDerivedStateFromProps(nextProps, lastState) for _, bubble in ipairs(lastState.bubbles) do -- A message being in lastState but not nextProps means it's been removed from the store -- => keep it in the state and fade it out! - if not nextProps.messageIds[bubble.message.id] then + if not Cryo.List.find(nextProps.messageIds, bubble.message.id) then table.insert(bubbles, { message = bubble.message, fadingOut = true @@ -52,29 +53,43 @@ function BubbleChatList.getDerivedStateFromProps(nextProps, lastState) } end -function BubbleChatList:init() +function BubbleChatList:init(props) + -- It's possible for this component to be initialized with no message if we switch between maximized/minimized + -- view during the fade out animation + if (not props.messageIds or #props.messageIds == 0) and props.onLastBubbleFadeOut then + props.onLastBubbleFadeOut() + end + self.onBubbleFadeOut = function(messageId) - self:setState({ - bubbles = Cryo.List.filter(self.state.bubbles, function(otherBubble) - return otherBubble.message.id ~= messageId - end) - }) + local bubbles = Cryo.List.filter(self.state.bubbles, function(otherBubble) + return otherBubble.message.id ~= messageId + end) + if #bubbles == 0 and self.props.onLastBubbleFadeOut then + self.props.onLastBubbleFadeOut() + else + -- Doing this when #bubbles == 0 causes Roact to panic, probably because the bubbles are being unmounted twice + -- (once due to setState below and once due to this list component being unmounted), hence the above check + self:setState({ + bubbles = bubbles + }) + end end end function BubbleChatList:render() local children = {} + local settings = self.props.chatSettings children.Layout = Roact.createElement("UIListLayout", { SortOrder = Enum.SortOrder.LayoutOrder, HorizontalAlignment = Enum.HorizontalAlignment.Center, VerticalAlignment = Enum.VerticalAlignment.Bottom, - Padding = UDim.new(0, 8), + Padding = UDim.new(0, settings.BubblesSpacing), }) -- This padding pushes up the UI a bit so the first message's -- caret shows up. - children.CaretPadding = Roact.createElement("UIPadding", { + children.CaretPadding = settings.TailVisible and Roact.createElement("UIPadding", { PaddingBottom = UDim.new(0, 8), }) diff --git a/scripts/CoreScripts/Modules/InGameChat/BubbleChat/Components/ChatBubble.lua b/scripts/CoreScripts/Modules/InGameChat/BubbleChat/Components/ChatBubble.lua index 837601c4c0..f8ed42a36e 100644 --- a/scripts/CoreScripts/Modules/InGameChat/BubbleChat/Components/ChatBubble.lua +++ b/scripts/CoreScripts/Modules/InGameChat/BubbleChat/Components/ChatBubble.lua @@ -5,9 +5,7 @@ local Otter = require(CorePackages.Packages.Otter) local Roact = require(CorePackages.Packages.Roact) local RoactRodux = require(CorePackages.Packages.RoactRodux) local t = require(CorePackages.Packages.t) -local Constants = require(script.Parent.Parent.Constants) local Types = require(script.Parent.Parent.Types) -local Themes = require(script.Parent.Parent.Themes) local SPRING_CONFIG = { dampingRatio = 1, @@ -21,21 +19,15 @@ ChatBubble.validateProps = t.strictInterface({ fadingOut = t.optional(t.boolean), onFadeOut = t.optional(t.callback), - maxWidth = t.optional(t.number), LayoutOrder = t.optional(t.number), isMostRecent = t.optional(t.boolean), theme = t.optional(t.string), - TextSize = t.optional(t.number), - Font = t.optional(t.enum(Enum.Font)), chatSettings = Types.IChatSettings, }) ChatBubble.defaultProps = { theme = "Light", - TextSize = 16, - Font = Enum.Font.Gotham, - maxWidth = 300, isMostRecent = true, } @@ -58,13 +50,14 @@ function ChatBubble:init() end function ChatBubble:getTextBounds() - local padding = Vector2.new(Constants.UI_PADDING * 4, Constants.UI_PADDING * 2) + local settings = self.props.chatSettings + local padding = Vector2.new(settings.Padding * 4, settings.Padding * 2) local bounds = TextService:GetTextSize( self.props.message.text, - self.props.TextSize, - self.props.Font, - Vector2.new(self.props.maxWidth, 10000) + settings.TextSize, + settings.Font, + Vector2.new(settings.MaxWidth, 10000) ) return bounds + padding @@ -72,6 +65,7 @@ end function ChatBubble:render() local bounds = self:getTextBounds() + local settings = self.props.chatSettings return Roact.createElement("Frame", { LayoutOrder = self.props.LayoutOrder, @@ -88,7 +82,7 @@ function ChatBubble:render() Frame = Roact.createElement("Frame", { LayoutOrder = 1, - BackgroundColor3 = self.props.chatSettings.BackgroundColor3, + BackgroundColor3 = settings.BackgroundColor3, AnchorPoint = Vector2.new(0.5, 0), Size = UDim2.fromScale(1, 1), BorderSizePixel = 0, @@ -97,7 +91,7 @@ function ChatBubble:render() ClipsDescendants = true, }, { UICorner = Roact.createElement("UICorner", { - CornerRadius = UDim.new(0, 12), + CornerRadius = settings.CornerRadius, }), Text = Roact.createElement("TextLabel", { @@ -106,27 +100,27 @@ function ChatBubble:render() AnchorPoint = Vector2.new(0.5, 0.5), Position = UDim2.fromScale(0.5, 0.5), BackgroundTransparency = 1, - Font = Enum.Font.GothamSemibold, - TextColor3 = Themes.FontColor[self.props.theme], - TextSize = self.props.TextSize, + Font = settings.Font, + TextColor3 = settings.TextColor3, + TextSize = settings.TextSize, TextTransparency = self.transparency, TextWrapped = true, }, { Padding = Roact.createElement("UIPadding", { - PaddingTop = UDim.new(0, Constants.UI_PADDING), - PaddingRight = UDim.new(0, Constants.UI_PADDING), - PaddingBottom = UDim.new(0, Constants.UI_PADDING), - PaddingLeft = UDim.new(0, Constants.UI_PADDING), + PaddingTop = UDim.new(0, settings.Padding), + PaddingRight = UDim.new(0, settings.Padding), + PaddingBottom = UDim.new(0, settings.Padding), + PaddingLeft = UDim.new(0, settings.Padding), }) }) }), - Carat = self.props.isMostRecent and Roact.createElement("ImageLabel", { + Carat = self.props.isMostRecent and settings.TailVisible and Roact.createElement("ImageLabel", { LayoutOrder = 2, BackgroundTransparency = 1, Size = UDim2.fromOffset(9, 6), Image = "rbxasset://textures/ui/InGameChat/Caret.png", - ImageColor3 = self.props.chatSettings.BackgroundColor3, + ImageColor3 = settings.BackgroundColor3, ImageTransparency = self.transparency, }), }) @@ -167,7 +161,7 @@ function ChatBubble:didMount() self.widthMotor:setGoal(Otter.spring(bounds.X, SPRING_CONFIG)) end - self.transparencyMotor:setGoal(Otter.spring(Constants.BUBBLE_BASE_TRANSPARENCY, SPRING_CONFIG)) + self.transparencyMotor:setGoal(Otter.spring(self.props.chatSettings.Transparency, SPRING_CONFIG)) end function ChatBubble:willUnmount() diff --git a/scripts/CoreScripts/Modules/InGameChat/BubbleChat/Components/ChatBubbleDistant.lua b/scripts/CoreScripts/Modules/InGameChat/BubbleChat/Components/ChatBubbleDistant.lua index 199ac9a24a..ec1273cff1 100644 --- a/scripts/CoreScripts/Modules/InGameChat/BubbleChat/Components/ChatBubbleDistant.lua +++ b/scripts/CoreScripts/Modules/InGameChat/BubbleChat/Components/ChatBubbleDistant.lua @@ -24,6 +24,8 @@ local SPRING_CONFIG = { ChatBubbleDistant.validateProps = t.strictInterface({ width = t.optional(t.number), height = t.optional(t.number), + fadingOut = t.optional(t.boolean), + onFadeOut = t.optional(t.callback), chatSettings = Types.IChatSettings, }) @@ -45,9 +47,17 @@ function ChatBubbleDistant:init(props) self.transparency, self.updateTransparency = Roact.createBinding(1) self.transparencyMotor = Otter.createSingleMotor(1) self.transparencyMotor:onStep(self.updateTransparency) + + -- It's possible for this component to be initialized with fadingOut = true if we switch between maximized/minimized + -- view during the fade out animation + if props.fadingOut then + self:fadeOut() + end end function ChatBubbleDistant:render() + local settings = self.props.chatSettings + return Roact.createElement("Frame", { AnchorPoint = Vector2.new(0.5, 1), Size = UDim2.new(0, 43, 0, 32), @@ -57,25 +67,26 @@ function ChatBubbleDistant:render() Scale = Roact.createElement("UIScale", { Scale = 0.75, }), - Carat = Roact.createElement("ImageLabel", { + Carat = settings.TailVisible and Roact.createElement("ImageLabel", { AnchorPoint = Vector2.new(0.5, 0), BackgroundTransparency = 1, Position = UDim2.new(0.5, 0, 1, -1), --UICorner generates a 1 pixel gap (UISYS-625), this fixes it by moving the carrot up by 1 pixel Size = UDim2.fromOffset(12, 8), Image = "rbxasset://textures/ui/InGameChat/Caret.png", - ImageColor3 = self.props.chatSettings.BackgroundColor3, + ImageColor3 = settings.BackgroundColor3, ImageTransparency = self.transparency, }), RoundedFrame = Roact.createElement("Frame", { Size = self.frameSize, - BackgroundColor3 = self.props.chatSettings.BackgroundColor3, + BackgroundColor3 = settings.BackgroundColor3, BackgroundTransparency = self.transparency, + BorderSizePixel = 0, AnchorPoint = Vector2.new(0.5, 0), Position = UDim2.new(0.5, 0, 0, 0), ClipsDescendants = true, }, { UICorner = Roact.createElement("UICorner", { - CornerRadius = UDim.new(0, 12), + CornerRadius = settings.CornerRadius, }), Contents = Roact.createElement("Frame", { @@ -83,15 +94,17 @@ function ChatBubbleDistant:render() BackgroundTransparency = 1, }, { Padding = Roact.createElement("UIPadding", { - PaddingTop = UDim.new(0, Constants.UI_PADDING), - PaddingRight = UDim.new(0, Constants.UI_PADDING), - PaddingBottom = UDim.new(0, Constants.UI_PADDING), - PaddingLeft = UDim.new(0, Constants.UI_PADDING), + PaddingTop = UDim.new(0, settings.Padding), + PaddingRight = UDim.new(0, settings.Padding), + PaddingBottom = UDim.new(0, settings.Padding), + PaddingLeft = UDim.new(0, settings.Padding), }), Icon = Roact.createElement("TextLabel", { BackgroundTransparency = 1, Text = "…", + TextColor3 = settings.TextColor3, + TextTransparency = self.transparency, Font = Enum.Font.GothamBlack, TextScaled = true, Size = UDim2.fromScale(1, 1), @@ -101,9 +114,31 @@ function ChatBubbleDistant:render() }) end +function ChatBubbleDistant:fadeOut() + if not self.isFadingOut then + self.isFadingOut = true + + self.transparencyMotor:onComplete(function() + if self.props.onFadeOut then + self.props.onFadeOut() + end + end) + + self.transparencyMotor:setGoal(Otter.spring(1, SPRING_CONFIG)) + end +end + +function ChatBubbleDistant:didUpdate() + if self.props.fadingOut then + self:fadeOut() + end +end + function ChatBubbleDistant:didMount() - self.transparencyMotor:setGoal(Otter.spring(Constants.BUBBLE_BASE_TRANSPARENCY, SPRING_CONFIG)) - self.widthMotor:setGoal(Otter.spring(self.props.width, SPRING_CONFIG)) + if not self.props.fadingOut then + self.transparencyMotor:setGoal(Otter.spring(self.props.chatSettings.Transparency, SPRING_CONFIG)) + self.widthMotor:setGoal(Otter.spring(self.props.width, SPRING_CONFIG)) + end end function ChatBubbleDistant:willUnmount() diff --git a/scripts/CoreScripts/Modules/InGameChat/BubbleChat/Components/__stories__/ChatBubble.story.lua b/scripts/CoreScripts/Modules/InGameChat/BubbleChat/Components/__stories__/ChatBubble.story.lua index 7a6952fe1d..70d0f17c70 100644 --- a/scripts/CoreScripts/Modules/InGameChat/BubbleChat/Components/__stories__/ChatBubble.story.lua +++ b/scripts/CoreScripts/Modules/InGameChat/BubbleChat/Components/__stories__/ChatBubble.story.lua @@ -4,7 +4,7 @@ local Roact = require(CorePackages.Packages.Roact) local createMockMessage = require(script.Parent.Parent.Parent.Helpers.createMockMessage) local ChatBubble = require(script.Parent.Parent.ChatBubble) -local Constants = require(script.Parent.Parent.Parent.Constants) +local settings = require(script.Parent.Parent.Parent.ChatSettings) return function(target) local root = Roact.createFragment({ @@ -17,10 +17,10 @@ return function(target) }), Padding = Roact.createElement("UIPadding", { - PaddingTop = UDim.new(0, Constants.UI_PADDING), - PaddingRight = UDim.new(0, Constants.UI_PADDING), - PaddingBottom = UDim.new(0, Constants.UI_PADDING), - PaddingLeft = UDim.new(0, Constants.UI_PADDING), + PaddingTop = UDim.new(0, settings.Padding), + PaddingRight = UDim.new(0, settings.Padding), + PaddingBottom = UDim.new(0, settings.Padding), + PaddingLeft = UDim.new(0, settings.Padding), }), ShortMessage = Roact.createElement(ChatBubble, { diff --git a/scripts/CoreScripts/Modules/InGameChat/BubbleChat/Components/__stories__/Themes.story.lua b/scripts/CoreScripts/Modules/InGameChat/BubbleChat/Components/__stories__/Themes.story.lua index 58a28a041b..fd4cb0571c 100644 --- a/scripts/CoreScripts/Modules/InGameChat/BubbleChat/Components/__stories__/Themes.story.lua +++ b/scripts/CoreScripts/Modules/InGameChat/BubbleChat/Components/__stories__/Themes.story.lua @@ -3,7 +3,7 @@ local CorePackages = game:GetService("CorePackages") local Roact = require(CorePackages.Packages.Roact) local createMockMessage = require(script.Parent.Parent.Parent.Helpers.createMockMessage) -local Constants = require(script.Parent.Parent.Parent.Constants) +local settings = require(script.Parent.Parent.Parent.ChatSettings) local ChatBubble = require(script.Parent.Parent.ChatBubble) return function(target) @@ -17,10 +17,10 @@ return function(target) }), Padding = Roact.createElement("UIPadding", { - PaddingTop = UDim.new(0, Constants.UI_PADDING), - PaddingRight = UDim.new(0, Constants.UI_PADDING), - PaddingBottom = UDim.new(0, Constants.UI_PADDING), - PaddingLeft = UDim.new(0, Constants.UI_PADDING), + PaddingTop = UDim.new(0, settings.Padding), + PaddingRight = UDim.new(0, settings.Padding), + PaddingBottom = UDim.new(0, settings.Padding), + PaddingLeft = UDim.new(0, settings.Padding), }), ShortMessage = Roact.createElement(ChatBubble, { diff --git a/scripts/CoreScripts/Modules/InGameChat/BubbleChat/Constants.lua b/scripts/CoreScripts/Modules/InGameChat/BubbleChat/Constants.lua index 2a1fc27649..18e51c0953 100644 --- a/scripts/CoreScripts/Modules/InGameChat/BubbleChat/Constants.lua +++ b/scripts/CoreScripts/Modules/InGameChat/BubbleChat/Constants.lua @@ -7,12 +7,6 @@ return { -- we're duplicating the value. MAX_MESSAGE_LENGTH = 200, - -- Pixel value used to pad UI elements - UI_PADDING = 8, - -- The amount of studs the camera has to move before a rerender occurs. CAMERA_CHANGED_EPSILON = 5, - - BUBBLE_BASE_TRANSPARENCY = 0.1, - BUBBLE_FADED_TRANSPARENCY = 0.3, } diff --git a/scripts/CoreScripts/Modules/InGameChat/BubbleChat/Types.lua b/scripts/CoreScripts/Modules/InGameChat/BubbleChat/Types.lua index f02a76b625..63a268f421 100644 --- a/scripts/CoreScripts/Modules/InGameChat/BubbleChat/Types.lua +++ b/scripts/CoreScripts/Modules/InGameChat/BubbleChat/Types.lua @@ -34,6 +34,18 @@ Types.IChatSettings = t.strictInterface({ MaxBubbles = t.optional(t.number), BackgroundColor3 = t.optional(t.Color3), + TextColor3 = t.optional(t.Color3), + TextSize = t.optional(t.number), + Font = t.optional(t.enum(Enum.Font)), + Transparency = t.optional(t.number), + CornerRadius = t.optional(t.UDim), + TailVisible = t.optional(t.boolean), + Padding = t.optional(t.number), + MaxWidth = t.optional(t.number), + + VerticalStudsOffset = t.optional(t.number), + + BubblesSpacing = t.optional(t.number), MinimizeDistance = t.optional(t.number), MaxDistance = t.optional(t.number), diff --git a/scripts/CoreScripts/Modules/InGameMenu/Components/GameSettingsPage/GraphicsQualityEntry.lua b/scripts/CoreScripts/Modules/InGameMenu/Components/GameSettingsPage/GraphicsQualityEntry.lua index d6999e4903..b101032e56 100644 --- a/scripts/CoreScripts/Modules/InGameMenu/Components/GameSettingsPage/GraphicsQualityEntry.lua +++ b/scripts/CoreScripts/Modules/InGameMenu/Components/GameSettingsPage/GraphicsQualityEntry.lua @@ -11,7 +11,8 @@ local t = InGameMenuDependencies.t local UserGameSettings = UserSettings():GetService("UserGameSettings") local RenderSettings = settings().Rendering -local SavedQualityLevelChanged = UserGameSettings:GetPropertyChangedSignal("SavedQualityLevel") + +local SavedQualityLevelChanged = UserGameSettings:GetPropertyChangedSignal("SavedQualityLevel") -- <- DEPRECATED: remove with FixGraphicsQuality local InGameMenu = script.Parent.Parent.Parent @@ -19,38 +20,110 @@ local ToggleEntry = require(script.Parent.ToggleEntry) local ExternalEventConnection = require(InGameMenu.Utility.ExternalEventConnection) local SliderEntry = require(script.Parent.SliderEntry) +local CoreGui = game:GetService("CoreGui") +local RobloxGui = CoreGui:WaitForChild("RobloxGui") +local GetFixGraphicsQuality = require(RobloxGui.Modules.Flags.GetFixGraphicsQuality) + +local SendNotification +local RobloxTranslator +local GraphicsQualityLevelChanged + +if GetFixGraphicsQuality() then + RobloxTranslator = require(RobloxGui:WaitForChild("Modules"):WaitForChild("RobloxTranslator")) + GraphicsQualityLevelChanged = UserGameSettings:GetPropertyChangedSignal("GraphicsQualityLevel") +end + local SendAnalytics = require(InGameMenu.Utility.SendAnalytics) local Constants = require(InGameMenu.Resources.Constants) -local SAVED_QUALITY_LEVELS = #Enum.SavedQualitySetting:GetEnumItems() - 1 +local SAVED_QUALITY_LEVELS = #Enum.SavedQualitySetting:GetEnumItems() - 1 -- <- DEPRECATED: remove with FixGraphicsQuality +local GRAPHICS_QUALITY_LEVELS = 10 -local function mapInteger(x, xMin, xMax, yMin, yMax) +if GetFixGraphicsQuality() then + GRAPHICS_QUALITY_LEVELS = RenderSettings:GetMaxQualityLevel() - 1 + -- Don't be fooled by the word "max". It's not the maximum level, it's a strict upper bound + -- so if GetMaxQualityLevel() returns 22, that means the biggest the level can be is 21 +end + +local function mapInteger(x, xMin, xMax, yMin, yMax) -- <- DEPRECATED: remove with FixGraphicsQuality return math.clamp( math.floor(yMin + (x - xMin)*(yMax - yMin)/(xMax - xMin)), yMin, yMax) end +--[[ + Generates the popup in the lower-right corner when the user changes the + graphics quality level with F-keys. +]] +local function sendNotificationForGraphicsQualityLevelChange(message, level) + if SendNotification == nil then + SendNotification = RobloxGui:WaitForChild("SendNotificationInfo") + end + SendNotification:Fire({ + GroupName = "Graphics", + Title = "Graphics Quality", + Text = message, + DetailText = message, + Image = "", + Duration = 2 + }) +end + +--[[ + Takes two arguments: newValue, delta. If delta is non-zero, constructs a message for the pop-up + notification (based on whether delta is positive or negative) to inform the user that the level + incrememnted / decremented to the new value. +]] +local function notifyForLevelChange(newValue, delta) + if delta > 0 then + sendNotificationForGraphicsQualityLevelChange( + RobloxTranslator:FormatByKey("NotificationScrip2.onCurrentGraphicsQualityLevelChanged.Increased", {RBX_NUMBER = tostring(newValue)}), + newValue) + elseif delta < 0 then + sendNotificationForGraphicsQualityLevelChange( + RobloxTranslator:FormatByKey("NotificationScrip2.onCurrentGraphicsQualityLevelChanged.Decreased", {RBX_NUMBER = tostring(newValue)}), + newValue) + end +end + local GraphicsQualityEntry = Roact.PureComponent:extend("GraphicsQualityEntry") GraphicsQualityEntry.validateProps = t.strictInterface({ LayoutOrder = t.integer, }) function GraphicsQualityEntry:init() - local isAutomatic = UserGameSettings.SavedQualityLevel == Enum.SavedQualitySetting.Automatic - self:setState({ - quality = isAutomatic and 5 or UserGameSettings.SavedQualityLevel.Value, - automatic = isAutomatic, - }) - - self.onQualityChanged = function() - local newSavedQuality = UserGameSettings.SavedQualityLevel - local changedToAutomatic = newSavedQuality == Enum.SavedQualitySetting.Automatic - + local isAutomatic + if GetFixGraphicsQuality() then + local quality = UserGameSettings.GraphicsQualityLevel + isAutomatic = quality == 0 self:setState({ - quality = not changedToAutomatic and newSavedQuality.Value or nil, - automatic = newSavedQuality == Enum.SavedQualitySetting.Automatic, + quality = isAutomatic and 5 or UserGameSettings.GraphicsQualityLevel, + automatic = isAutomatic, }) + self.onQualityChanged = function() + local quality = UserGameSettings.GraphicsQualityLevel + self:setState({ + quality = quality ~= 0 and quality or nil, + automatic = quality == 0, + }) + end + else + isAutomatic = UserGameSettings.SavedQualityLevel == Enum.SavedQualitySetting.Automatic + self:setState({ + quality = isAutomatic and 5 or UserGameSettings.SavedQualityLevel.Value, + automatic = isAutomatic, + }) + + self.onQualityChanged = function() + local newSavedQuality = UserGameSettings.SavedQualityLevel + local changedToAutomatic = newSavedQuality == Enum.SavedQualitySetting.Automatic + + self:setState({ + quality = not changedToAutomatic and newSavedQuality.Value or nil, + automatic = newSavedQuality == Enum.SavedQualitySetting.Automatic, + }) + end end if isAutomatic then @@ -58,22 +131,57 @@ function GraphicsQualityEntry:init() else self:setManualQualityLevel(self.state.quality) end + + if GetFixGraphicsQuality() then + --[[ + Gets called when the user hits F10 / Shift-F10 to adjust the graphcs quality level. + + Calls setManualQualityLevel to set the new level. Note: setManualQualityLevel checks whether the new value + it's given is in bounds. The returned newValue and delta indicate the change that actually takes place + We use newValue and delta to determine what notification to show. + ]] + game.GraphicsQualityChangeRequest:connect( + function(isIncrease) + local current = UserGameSettings.GraphicsQualityLevel + if current ~= 0 then + local newValue, delta = self:setManualQualityLevel(current + (isIncrease and 1 or -1)) + notifyForLevelChange(newValue, delta) + end + end + ) + end end function GraphicsQualityEntry:setAutomaticQualityLevel() - UserGameSettings.SavedQualityLevel = Enum.SavedQualitySetting.Automatic - RenderSettings.QualityLevel = 0 + if GetFixGraphicsQuality() then + UserGameSettings.GraphicsQualityLevel = 0 + RenderSettings.QualityLevel = 0 + else + UserGameSettings.SavedQualityLevel = Enum.SavedQualitySetting.Automatic + RenderSettings.QualityLevel = 0 + end end function GraphicsQualityEntry:setManualQualityLevel(manualQualityLevel) - local renderQualityLevel = mapInteger( - manualQualityLevel, - 1, SAVED_QUALITY_LEVELS, - -- Quality levels are zero-based; GetMaxQualityLevel reports 22, even - -- though 21 is the maximum. - 1, RenderSettings:GetMaxQualityLevel() - 1) - UserGameSettings.SavedQualityLevel = manualQualityLevel - RenderSettings.QualityLevel = renderQualityLevel + if GetFixGraphicsQuality() then + local newValue = math.clamp(manualQualityLevel, 1, GRAPHICS_QUALITY_LEVELS) + local oldValue = UserGameSettings.GraphicsQualityLevel + + UserGameSettings.GraphicsQualityLevel = newValue + RenderSettings.QualityLevel = newValue + + return newValue, newValue - oldValue + else + local renderQualityLevel = mapInteger( + manualQualityLevel, + 1, SAVED_QUALITY_LEVELS, + -- Quality levels are zero-based; GetMaxQualityLevel reports 22, even + -- though 21 is the maximum. + 1, RenderSettings:GetMaxQualityLevel() - 1) + UserGameSettings.SavedQualityLevel = manualQualityLevel + RenderSettings.QualityLevel = renderQualityLevel + return nil + end end function GraphicsQualityEntry:render() @@ -102,7 +210,7 @@ function GraphicsQualityEntry:render() LayoutOrder = 2, labelKey = "CoreScripts.InGameMenu.GameSettings.ManualGraphicsQuality", min = 1, - max = 10, + max = GRAPHICS_QUALITY_LEVELS, stepInterval = 1, value = self.state.quality, disabled = self.state.automatic, @@ -112,10 +220,10 @@ function GraphicsQualityEntry:render() end, }), QualityListener = Roact.createElement(ExternalEventConnection, { - event = SavedQualityLevelChanged, + event = GetFixGraphicsQuality() and GraphicsQualityLevelChanged or SavedQualityLevelChanged, callback = self.onQualityChanged, }) }) end -return GraphicsQualityEntry \ No newline at end of file +return GraphicsQualityEntry diff --git a/scripts/CoreScripts/Modules/InGameMenu/Components/GameSettingsPage/GraphicsQualityEntry.spec.lua b/scripts/CoreScripts/Modules/InGameMenu/Components/GameSettingsPage/GraphicsQualityEntry.spec.lua index 27ab47f5d8..9b57f6c109 100644 --- a/scripts/CoreScripts/Modules/InGameMenu/Components/GameSettingsPage/GraphicsQualityEntry.spec.lua +++ b/scripts/CoreScripts/Modules/InGameMenu/Components/GameSettingsPage/GraphicsQualityEntry.spec.lua @@ -1,6 +1,8 @@ return function() + local CoreGui = game:GetService("CoreGui") local CorePackages = game:GetService("CorePackages") + local InGameMenuDependencies = require(CorePackages.InGameMenuDependencies) local Roact = InGameMenuDependencies.Roact local Rodux = InGameMenuDependencies.Rodux @@ -15,11 +17,18 @@ return function() local AppDarkTheme = require(CorePackages.AppTempCommon.LuaApp.Style.Themes.DarkTheme) local AppFont = require(CorePackages.AppTempCommon.LuaApp.Style.Fonts.Gotham) + local appStyle = { Theme = AppDarkTheme, Font = AppFont, } + local RobloxGui = CoreGui:WaitForChild("RobloxGui") + local GetFixGraphicsQuality = require(RobloxGui.Modules.Flags.GetFixGraphicsQuality) + local SendNotificationInfo = Instance.new("BindableEvent") + SendNotificationInfo.Name = "SendNotificationInfo" + SendNotificationInfo.Parent = RobloxGui + local GraphicsQualityEntry = require(script.Parent.GraphicsQualityEntry) local UserGameSettings = UserSettings():GetService("UserGameSettings") @@ -68,15 +77,28 @@ return function() }), }) - UserGameSettings.SavedQualityLevel = Enum.SavedQualitySetting.QualityLevel2 + if GetFixGraphicsQuality() then + UserGameSettings.GraphicsQualityLevel = 11 - local instance = Roact.mount(element) - expect(RenderSettings.QualityLevel).to.equal(Enum.QualityLevel.Level03) - Roact.unmount(instance) + local instance = Roact.mount(element) + expect(RenderSettings.QualityLevel).to.equal(Enum.QualityLevel.Level11) + Roact.unmount(instance) - UserGameSettings.SavedQualityLevel = Enum.SavedQualitySetting.QualityLevel10 - instance = Roact.mount(element) - expect(RenderSettings.QualityLevel).to.equal(Enum.QualityLevel.Level21) - Roact.unmount(instance) + UserGameSettings.GraphicsQualityLevel = 21 + instance = Roact.mount(element) + expect(RenderSettings.QualityLevel).to.equal(Enum.QualityLevel.Level21) + Roact.unmount(instance) + else + UserGameSettings.SavedQualityLevel = Enum.SavedQualitySetting.QualityLevel2 + + local instance = Roact.mount(element) + expect(RenderSettings.QualityLevel).to.equal(Enum.QualityLevel.Level03) + Roact.unmount(instance) + + UserGameSettings.SavedQualityLevel = Enum.SavedQualitySetting.QualityLevel10 + instance = Roact.mount(element) + expect(RenderSettings.QualityLevel).to.equal(Enum.QualityLevel.Level21) + Roact.unmount(instance) + end end) -end \ No newline at end of file +end diff --git a/scripts/CoreScripts/Modules/Server/ClientChat/BubbleChat/BubbleChat.lua b/scripts/CoreScripts/Modules/Server/ClientChat/BubbleChat/BubbleChat.lua index 4a9d1f7f69..d91c7822ed 100644 --- a/scripts/CoreScripts/Modules/Server/ClientChat/BubbleChat/BubbleChat.lua +++ b/scripts/CoreScripts/Modules/Server/ClientChat/BubbleChat/BubbleChat.lua @@ -38,6 +38,13 @@ local UserRoactBubbleChatBeta do UserRoactBubbleChatBeta = success and value end +local UserPreventOldBubbleChatOverlap do + local success, value = pcall(function() + return UserSettings():IsUserFeatureEnabled("UserPreventOldBubbleChatOverlap") + end) + UserPreventOldBubbleChatOverlap = success and value +end + local function getMessageLength(message) return utf8.len(utf8.nfcnormalize(message)) end @@ -631,6 +638,11 @@ function this:OnPlayerChatMessage(sourcePlayer, message, targetPlayer) end function this:OnGameChatMessage(origin, message, color) + -- Prevents conflicts with the new bubble chat if it is enabled + if UserRoactBubbleChatBeta or (UserPreventOldBubbleChatOverlap and ChatService.BubbleChatEnabled) then + return + end + local localPlayer = PlayersService.LocalPlayer local fromOthers = localPlayer ~= nil and (localPlayer.Character ~= origin) @@ -652,7 +664,7 @@ function this:OnGameChatMessage(origin, message, color) end function this:BubbleChatEnabled() - if UserRoactBubbleChatBeta then + if UserRoactBubbleChatBeta or (UserPreventOldBubbleChatOverlap and ChatService.BubbleChatEnabled) then return false end local clientChatModules = ChatService:FindFirstChild("ClientChatModules") diff --git a/scripts/CoreScripts/Modules/Settings/Pages/GameSettings.lua b/scripts/CoreScripts/Modules/Settings/Pages/GameSettings.lua index 63cfb050c2..6ffd53331e 100644 --- a/scripts/CoreScripts/Modules/Settings/Pages/GameSettings.lua +++ b/scripts/CoreScripts/Modules/Settings/Pages/GameSettings.lua @@ -22,7 +22,10 @@ local Settings = UserSettings() local GameSettings = Settings.GameSettings -------------- CONSTANTS -------------- +-- DEPRECATED Remove with FixGraphicsQuality local GRAPHICS_QUALITY_LEVELS = 10 + +-- DEPRECATED Remove with FixGraphicsQuality local GRAPHICS_QUALITY_TO_INT = { ["Enum.SavedQualitySetting.Automatic"] = 0, ["Enum.SavedQualitySetting.QualityLevel1"] = 1, @@ -76,6 +79,17 @@ local LocalPlayer = Players.LocalPlayer local platform = UserInputService:GetPlatform() local PolicyService = require(RobloxGui.Modules.Common.PolicyService) +local GetFixGraphicsQuality = require(RobloxGui.Modules.Flags.GetFixGraphicsQuality) +local RenderSettings +local SendNotification +local RobloxTranslator + +if GetFixGraphicsQuality() then + RenderSettings = settings().Rendering + SendNotification = RobloxGui:WaitForChild("SendNotificationInfo") + RobloxTranslator = require(RobloxGui:WaitForChild("Modules"):WaitForChild("RobloxTranslator")) +end + local UnlSuccess, UnlResult = pcall( function() @@ -218,8 +232,15 @@ local function Initialize() ------------------ Gfx Enabler Selection GUI Setup ------------------ local graphicsEnablerStart = 1 - if GameSettings.SavedQualityLevel ~= Enum.SavedQualitySetting.Automatic then - graphicsEnablerStart = 2 + + if GetFixGraphicsQuality() then + if GameSettings.GraphicsQualityLevel ~= 0 then + graphicsEnablerStart = 2 + end + else + if GameSettings.SavedQualityLevel ~= Enum.SavedQualitySetting.Automatic then + graphicsEnablerStart = 2 + end end this.GraphicsEnablerFrame, this.GraphicsEnablerLabel, this.GraphicsQualityEnabler = @@ -227,8 +248,28 @@ local function Initialize() this.GraphicsEnablerFrame.LayoutOrder = 7 ------------------ Gfx Slider GUI Setup ------------------ - this.GraphicsQualityFrame, this.GraphicsQualityLabel, this.GraphicsQualitySlider = - utility:AddNewRow(this, "Graphics Quality", "Slider", GRAPHICS_QUALITY_LEVELS, 1) + + local numGraphicsQualityLevels + if GetFixGraphicsQuality() then + numGraphicsQualityLevels = RenderSettings:GetMaxQualityLevel() - 1 + -- Don't be fooled by the word "max". It's not the maximum level, it's a strict upper bound + -- so if GetMaxQualityLevel() returns 22, that means the biggest the level can be is 21 + + + --[[ + Cache the most recent non-zero graphics level in this member variable. If the user + switches to Auto mode, we use it to set the graphics level to whatever the user had set + the last time they were in manual mode. + ]] + this.mostRecentGraphicsQualityValue = numGraphicsQualityLevels + + this.GraphicsQualityFrame, this.GraphicsQualityLabel, this.GraphicsQualitySlider = + utility:AddNewRow(this, "Graphics Quality", "Slider", numGraphicsQualityLevels, 1) + else + this.GraphicsQualityFrame, this.GraphicsQualityLabel, this.GraphicsQualitySlider = + utility:AddNewRow(this, "Graphics Quality", "Slider", GRAPHICS_QUALITY_LEVELS, 1) + end + this.GraphicsQualityFrame.LayoutOrder = 8 this.GraphicsQualitySlider:SetMinStep(1) @@ -236,6 +277,7 @@ local function Initialize() ------------------------- Connection Setup ---------------------------- settings().Rendering.EnableFRM = true + -- DEPRECATED Remove with FixGraphicsQuality function SetGraphicsQuality(newValue, automaticSettingAllowed) local percentage = newValue / GRAPHICS_QUALITY_LEVELS local newQualityLevel = math.floor((settings().Rendering:GetMaxQualityLevel() - 1) * percentage) @@ -254,6 +296,7 @@ local function Initialize() settings().Rendering.QualityLevel = newQualityLevel end + -- DEPRECATED Remove with FixGraphicsQuality local function setGraphicsToAuto() this.GraphicsQualitySlider:SetZIndex(1) this.GraphicsQualityLabel.ZIndex = 1 @@ -262,6 +305,7 @@ local function Initialize() SetGraphicsQuality(Enum.QualityLevel.Automatic.Value, true) end + -- DEPRECATED. Remove with FixGraphicsQuality local function setGraphicsToManual(level) this.GraphicsQualitySlider:SetZIndex(2) this.GraphicsQualityLabel.ZIndex = 2 @@ -275,70 +319,235 @@ local function Initialize() end end - game.GraphicsQualityChangeRequest:connect( - function(isIncrease) - -- was using settings().Rendering.Quality level, which was wrongly saying it was automatic. - if GameSettings.SavedQualityLevel == Enum.SavedQualitySetting.Automatic then - return - end - local currentGraphicsSliderValue = this.GraphicsQualitySlider:GetValue() - if isIncrease then - currentGraphicsSliderValue = currentGraphicsSliderValue + 1 - else - currentGraphicsSliderValue = currentGraphicsSliderValue - 1 - end + --[[ + Perform a UI change corrseponding to the user setting Auto mode. + ]] + local function disableGraphicsQualitySliderForAutoMode() + this.GraphicsQualitySlider:SetZIndex(1) + this.GraphicsQualityLabel.ZIndex = 1 + this.GraphicsQualitySlider:SetInteractable(false) + end + + --[[ + Sets the value visible in the slider GUI object to the given number (not an enum) + If the slider was hidden because we were in Auto mode before, this function also makes + the slider visible and interactable. + + Also, it doesn't bother setting the value if unchanged. + ]] + local function setGraphicsQualitySliderLevel(level) + this.GraphicsQualitySlider:SetZIndex(2) + this.GraphicsQualityLabel.ZIndex = 2 + this.GraphicsQualitySlider:SetInteractable(true) - this.GraphicsQualitySlider:SetValue(currentGraphicsSliderValue) + if this.GraphicsQualitySlider:GetValue() ~= level then + this.GraphicsQualitySlider:SetValue(level) end - ) + end - local initializedGfxLvl = false - this.GraphicsQualitySlider.ValueChanged:connect( - function(newValue) - SetGraphicsQuality(newValue) - if initializedGfxLvl == true then - reportSettingsForAnalytics() + --[[ + Perform a settings change corresponding to the user setting Auto mode. + ]] + local function setGraphicsQualityToAuto() + GameSettings.GraphicsQualityLevel = 0 + RenderSettings.QualityLevel = 0 + disableGraphicsQualitySliderForAutoMode() + end + + --[[ + Sets the rendering quality in three places: + GameSettings, settings() and the GUI. + + GameSettings gets saved to persistent local storage. + settings() affects the graphics engine. + To set the GUI, we call setGraphicsQualitySliderLevel, which importantly does nothing if the value is already + set on the GUI. (Otherwise, we might get into an infinite loop.) + + Returns two values: + newValue - note that the first thing this function does is clamp to the appropriate range, + and newValue is the clampped value. + delta - the difference between the new value and the old value. + ]] + local function setGraphicsQualityLevel(inputValue) + local newValue = math.clamp(inputValue, 1, numGraphicsQualityLevels) + local oldValue = GameSettings.GraphicsQualityLevel + + GameSettings.GraphicsQualityLevel = newValue + RenderSettings.QualityLevel = newValue + setGraphicsQualitySliderLevel(newValue) + + this.mostRecentGraphicsQualityValue = newValue + + --[[ + The caller might want to know what effect calling this setter actually had, so we return: + newValue - the actual new value (after clamping) and + delta - the difference between the new value and the old one. + + So for instance, if the level was already at the 21, 21 is the maximum, and we + tried to increment it to 22, this would returna newValue of 21 and a delta of 0 + ]] + return newValue, newValue - oldValue + end + + --[[ + On startup, set the graphics quality level (according to the engine and also according to UI) to + the most logical thing. If the fast-int is set that overrides it, set to the overridden value, + if not, use the value stored in settings: GameSettings.GraphicsQualityLevel + ]] + local function initGraphicsQualityLevel() + if FIntRomarkStartWithGraphicQualityLevel >= 0 then + if FIntRomarkStartWithGraphicQualityLevel == 0 then + setGraphicsQualityToAuto() + this.mostRecentGraphicsQualityValue = numGraphicsQualityLevels + else + setGraphicsQualityLevel(FIntRomarkStartWithGraphicQualityLevel) end - initializedGfxLvl = true + elseif GameSettings.GraphicsQualityLevel == 0 then + setGraphicsQualityToAuto() + this.mostRecentGraphicsQualityValue = numGraphicsQualityLevels + else + setGraphicsQualityLevel(GameSettings.GraphicsQualityLevel) end - ) + end - this.GraphicsQualityEnabler.IndexChanged:connect( - function(newIndex) - if newIndex == 1 then - setGraphicsToAuto() - elseif newIndex == 2 then - setGraphicsToManual(this.GraphicsQualitySlider:GetValue()) + if GetFixGraphicsQuality() then + this.GraphicsQualitySlider.ValueChanged:connect(setGraphicsQualityLevel) + --[[ + Gets called when the user hits the button in settings that switches between Manual and Auto + we forward that call to the appropriate function above. + ]] + this.GraphicsQualityEnabler.IndexChanged:connect( + function(newIndex) + if newIndex == 1 then + setGraphicsQualityToAuto() + elseif newIndex == 2 then + setGraphicsQualityLevel(this.mostRecentGraphicsQualityValue) + end + reportSettingsForAnalytics() end - reportSettingsForAnalytics() + ) + + --[[ + Generates the little popup in the lower right hand corner when the user changes the + graphics quality level with the F-keys. + ]] + local function sendNotificationForGraphicsQualityLevelChange(message, level) + SendNotification:Fire({ + GroupName = "Graphics", + Title = "Graphics Quality", + Text = message, + DetailText = message, + Image = "", + Duration = 2 + }) end - ) - -- initialize the slider position - if FIntRomarkStartWithGraphicQualityLevel >= 0 then - if FIntRomarkStartWithGraphicQualityLevel == 0 then - this.GraphicsQualityEnabler:SetSelectionIndex(1) - setGraphicsToAuto() - else - this.GraphicsQualityEnabler:SetSelectionIndex(2) - setGraphicsToManual(FIntRomarkStartWithGraphicQualityLevel) + --[[ + Takes two arguments: newValue, delta. If delta is non-zero, constructs a message for the pop-up + notification (based on whether delta is positive or negative) to inform the user that the level + incrememnted / decremented to the new value. + ]] + local function notifyForLevelChange(newValue, delta) + if delta > 0 then + sendNotificationForGraphicsQualityLevelChange( + RobloxTranslator:FormatByKey("NotificationScrip2.onCurrentGraphicsQualityLevelChanged.Increased", {RBX_NUMBER = tostring(newValue)}), + newValue) + elseif delta < 0 then + sendNotificationForGraphicsQualityLevelChange( + RobloxTranslator:FormatByKey("NotificationScrip2.onCurrentGraphicsQualityLevelChanged.Decreased", {RBX_NUMBER = tostring(newValue)}), + newValue) + end end - elseif GameSettings.SavedQualityLevel == Enum.SavedQualitySetting.Automatic then - this.GraphicsQualitySlider:SetValue(5) - setGraphicsToAuto() + + --[[ + Gets called when the user hits F10 / Shift-F10 to adjust the graphcs quality level. + + Based on whether the argument indicates an increase (or decrease), calls setGraphicsQualityLevel to increment (or decrement) + the level. The setter can decide that it doesn't want to increment (if the new value would be out-of-range) + the setter returns two values newValue and delta indicating what change actually happened. We then pass thos two values + to notifyForLevelChange to construct the notification. + ]] + game.GraphicsQualityChangeRequest:connect( + function(isIncrease) + local current = GameSettings.GraphicsQualityLevel + if current ~= 0 then + local targetValue = current + (isIncrease and 1 or -1) + local newValue, delta = setGraphicsQualityLevel(targetValue) + notifyForLevelChange(newValue, delta) + end + end + ) + + initGraphicsQualityLevel() else - local graphicsLevel = tostring(GameSettings.SavedQualityLevel) - if GRAPHICS_QUALITY_TO_INT[graphicsLevel] then - graphicsLevel = GRAPHICS_QUALITY_TO_INT[graphicsLevel] - else - graphicsLevel = GRAPHICS_QUALITY_LEVELS - end - SetGraphicsQuality(graphicsLevel) - spawn( - function() - this.GraphicsQualitySlider:SetValue(graphicsLevel) + -- DEPRECATED Remove with FixGraphicsQuality + local initializedGfxLvl = false + this.GraphicsQualitySlider.ValueChanged:connect( + function(newValue) + SetGraphicsQuality(newValue) + if initializedGfxLvl == true then + reportSettingsForAnalytics() + end + initializedGfxLvl = true + end + ) + + -- DEPRECATED Remove with FixGraphicsQuality + this.GraphicsQualityEnabler.IndexChanged:connect( + function(newIndex) + if newIndex == 1 then + setGraphicsToAuto() + elseif newIndex == 2 then + setGraphicsToManual(this.mostRecentGraphicsQualityValue) + end + reportSettingsForAnalytics() end ) + + -- DEPRECATED Remove with FixGraphicsQuality + game.GraphicsQualityChangeRequest:connect( + function(isIncrease) + -- was using settings().Rendering.Quality level, which was wrongly saying it was automatic. + if GameSettings.SavedQualityLevel ~= Enum.SavedQualitySetting.Automatic then + local currentGraphicsSliderValue = this.GraphicsQualitySlider:GetValue() + if isIncrease then + currentGraphicsSliderValue = currentGraphicsSliderValue + 1 + else + currentGraphicsSliderValue = currentGraphicsSliderValue - 1 + end + + this.GraphicsQualitySlider:SetValue(currentGraphicsSliderValue) + end + end + ) + + -- DEPRECATED Remove with FixGraphicsQuality + -- initialize the slider position + if FIntRomarkStartWithGraphicQualityLevel >= 0 then + if FIntRomarkStartWithGraphicQualityLevel == 0 then + this.GraphicsQualityEnabler:SetSelectionIndex(1) + setGraphicsToAuto() + else + this.GraphicsQualityEnabler:SetSelectionIndex(2) + setGraphicsToManual(FIntRomarkStartWithGraphicQualityLevel) + end + elseif GameSettings.SavedQualityLevel == Enum.SavedQualitySetting.Automatic then + this.GraphicsQualitySlider:SetValue(5) + setGraphicsToAuto() + else + local graphicsLevel = tostring(GameSettings.SavedQualityLevel) + if GRAPHICS_QUALITY_TO_INT[graphicsLevel] then + graphicsLevel = GRAPHICS_QUALITY_TO_INT[graphicsLevel] + else + graphicsLevel = GRAPHICS_QUALITY_LEVELS + end + SetGraphicsQuality(graphicsLevel) + spawn( + function() + this.GraphicsQualitySlider:SetValue(graphicsLevel) + end + ) + end end end -- of createGraphicsOptions @@ -588,9 +797,8 @@ local function Initialize() end end - ------------------------------------------------------ - ------------------ - ------------------ Camera Mode ----------------------- + ----------------------------------------------------------- + ----------------------- Camera Mode ----------------------- function setCameraModeVisible(visible) if this.CameraMode then diff --git a/scripts/CoreScripts/Modules/Settings/Pages/ShareGame/Components/ConversationList.lua b/scripts/CoreScripts/Modules/Settings/Pages/ShareGame/Components/ConversationList.lua index 1aee842e8e..ae1b179396 100644 --- a/scripts/CoreScripts/Modules/Settings/Pages/ShareGame/Components/ConversationList.lua +++ b/scripts/CoreScripts/Modules/Settings/Pages/ShareGame/Components/ConversationList.lua @@ -183,12 +183,14 @@ local function handleBinding(self) if isSelectionGroupEnabled() then local conversationList = self.scrollingRef:getValue() if conversationList then - if GuiService.SelectedCoreObject == nil then - GuiService:AddSelectionParent("invitePrompt", conversationList) - for _, object in ipairs(conversationList:GetChildren()) do - if object:IsA("GuiObject") and object.LayoutOrder == 1 then - GuiService.SelectedCoreObject = object - break + if conversationList:FindFirstAncestorOfClass("ScreenGui").Enabled then + if GuiService.SelectedCoreObject == nil then + GuiService:AddSelectionParent("invitePrompt", conversationList) + for _, object in ipairs(conversationList:GetChildren()) do + if object:IsA("GuiObject") and object.LayoutOrder == 1 then + GuiService.SelectedCoreObject = object + break + end end end end diff --git a/scripts/CoreScripts/Modules/Settings/Pages/ShareGame/isSelectionGroupEnabled.lua b/scripts/CoreScripts/Modules/Settings/Pages/ShareGame/isSelectionGroupEnabled.lua index 4a1804cc93..6c41c7ecdf 100644 --- a/scripts/CoreScripts/Modules/Settings/Pages/ShareGame/isSelectionGroupEnabled.lua +++ b/scripts/CoreScripts/Modules/Settings/Pages/ShareGame/isSelectionGroupEnabled.lua @@ -1,5 +1,5 @@ -game:DefineFastFlag("LuaInviteSelectionGroupEnabled", false) +game:DefineFastFlag("LuaInviteSelectionGroupEnabledV2", false) return function() - return game:GetFastFlag("LuaInviteSelectionGroupEnabled") + return game:GetFastFlag("LuaInviteSelectionGroupEnabledV2") end diff --git a/scripts/CoreScripts/StarterScript.lua b/scripts/CoreScripts/StarterScript.lua index 846db0f10c..dc21fde76a 100644 --- a/scripts/CoreScripts/StarterScript.lua +++ b/scripts/CoreScripts/StarterScript.lua @@ -34,7 +34,10 @@ local FFlagUseRoactGlobalConfigInCoreScripts = require(RobloxGui.Modules.Flags.F local FFlagConnectErrorHandlerInLoadingScript = require(RobloxGui.Modules.Flags.FFlagConnectErrorHandlerInLoadingScript) local GetFFlagRoactBubbleChat = require(RobloxGui.Modules.Common.Flags.GetFFlagRoactBubbleChat) +local EngineFeatureAvatarEditorService = game:GetEngineFeature("AvatarEditorService") + local isNewGamepadMenuEnabled = require(RobloxGui.Modules.Flags.isNewGamepadMenuEnabled) +local GetFFlagScreenTime = require(CorePackages.Regulations.ScreenTime.GetFFlagScreenTime) -- The Rotriever index, as well as the in-game menu code itself, relies on -- the init.lua convention, so we have to run initify over the module. @@ -109,7 +112,9 @@ ScriptContext:AddCoreScriptLocal("CoreScripts/MainBotChatScript2", RobloxGui) coroutine.wrap(function() -- this is the first place we call, which can yield so wrap in coroutine if PolicyService:IsSubjectToChinaPolicies() then ScriptContext:AddCoreScriptLocal("CoreScripts/AntiAddictionPrompt", RobloxGui) - ScriptContext:AddCoreScriptLocal("CoreScripts/ScreenTimeInGame", RobloxGui) + if GetFFlagScreenTime() then + ScriptContext:AddCoreScriptLocal("CoreScripts/ScreenTimeInGame", RobloxGui) + end end end)() @@ -156,6 +161,11 @@ coroutine.wrap(safeRequire)(RobloxGui.Modules.BackpackScript) -- Emotes Menu coroutine.wrap(safeRequire)(RobloxGui.Modules.EmotesMenu.EmotesMenuMaster) +if EngineFeatureAvatarEditorService then + initify(CoreGuiModules.AvatarEditorPrompts) + coroutine.wrap(safeRequire)(CoreGuiModules.AvatarEditorPrompts) +end + ScriptContext:AddCoreScriptLocal("CoreScripts/VehicleHud", RobloxGui) if not isNewGamepadMenuEnabled() then diff --git a/scripts/PlayerScripts/StarterPlayerScripts_NewStructure/PlayerModule.module/CameraModule/CameraInput.lua b/scripts/PlayerScripts/StarterPlayerScripts_NewStructure/PlayerModule.module/CameraModule/CameraInput.lua index 913d64ca07..4827fc9e9c 100644 --- a/scripts/PlayerScripts/StarterPlayerScripts_NewStructure/PlayerModule.module/CameraModule/CameraInput.lua +++ b/scripts/PlayerScripts/StarterPlayerScripts_NewStructure/PlayerModule.module/CameraModule/CameraInput.lua @@ -135,7 +135,6 @@ do Wheel = 0, -- PointerAction Pan = Vector2.new(), -- PointerAction Pinch = 0, -- PointerAction - MouseButton2 = 0, } local touchState = { Move = Vector2.new(), @@ -408,8 +407,11 @@ do function CameraInput.resetInputForFrameEnd() mouseState.Movement = Vector2.new() touchState.Move = Vector2.new() - mouseState.Wheel = 0 touchState.Pinch = 0 + + mouseState.Wheel = 0 -- PointerAction + mouseState.Pan = Vector2.new() -- PointerAction + mouseState.Pinch = 0 -- PointerAction end UserInputService.WindowFocused:Connect(resetInputDevices) diff --git a/scripts/PlayerScripts/StarterPlayerScripts_NewStructure/PlayerModule.module/CameraModule/LegacyCamera.lua b/scripts/PlayerScripts/StarterPlayerScripts_NewStructure/PlayerModule.module/CameraModule/LegacyCamera.lua index 053db6076b..7d806782bc 100644 --- a/scripts/PlayerScripts/StarterPlayerScripts_NewStructure/PlayerModule.module/CameraModule/LegacyCamera.lua +++ b/scripts/PlayerScripts/StarterPlayerScripts_NewStructure/PlayerModule.module/CameraModule/LegacyCamera.lua @@ -102,7 +102,7 @@ function LegacyCamera:Update(dt) local newLookVector if FFlagUserCameraInputRefactor then - newLookVector = self:CalculateNewLookVectorFromArg(nil, CameraInput.getRotation()) + newLookVector = self:CalculateNewLookVectorFromArg(nil, CameraInput.getRotation()*Vector2.new(0, 1)) else newLookVector = self:CalculateNewLookVector() self.rotateInput = ZERO_VECTOR2