This commit is contained in:
lx
2026-07-06 14:01:11 -04:00
commit c4f97d729d
16468 changed files with 935321 additions and 0 deletions
@@ -0,0 +1,10 @@
{
"lint": {
"SameLineStatement": "fatal",
"LocalUnused": "fatal",
"FunctionUnused": "fatal",
"ImportUnused": "fatal",
"LocalShadow": "fatal",
"ImplicitReturn": "fatal"
}
}
@@ -0,0 +1,7 @@
local CorePackages = game:GetService("CorePackages")
local Action = require(CorePackages.AppTempCommon.Common.Action)
return Action("CANCEL_LEAVING_GAME", function()
return {}
end)
@@ -0,0 +1,7 @@
local CorePackages = game:GetService("CorePackages")
local Action = require(CorePackages.AppTempCommon.Common.Action)
return Action("CLOSE_NATIVE_PROMPT", function()
return {}
end)
@@ -0,0 +1,7 @@
local CorePackages = game:GetService("CorePackages")
local Action = require(CorePackages.AppTempCommon.Common.Action)
return Action("CLOSE_REPORT_DIALOG", function()
return {}
end)
@@ -0,0 +1,7 @@
local CorePackages = game:GetService("CorePackages")
local Action = require(CorePackages.AppTempCommon.Common.Action)
return Action("CLOSE_REPORT_SENT_DIALOG", function()
return {}
end)
@@ -0,0 +1,7 @@
local CorePackages = game:GetService("CorePackages")
local Action = require(CorePackages.AppTempCommon.Common.Action)
return Action("OPEN_NATIVE_CLOSE_PROMPT", function()
return {}
end)
@@ -0,0 +1,10 @@
local CorePackages = game:GetService("CorePackages")
local Action = require(CorePackages.AppTempCommon.Common.Action)
return Action("OPEN_REPORT_DIALOG", function(userId, userName)
return {
userId = userId,
userName = userName,
}
end)
@@ -0,0 +1,7 @@
local CorePackages = game:GetService("CorePackages")
local Action = require(CorePackages.AppTempCommon.Common.Action)
return Action("OPEN_REPORT_SENT_DIALOG", function()
return {}
end)
@@ -0,0 +1,9 @@
local Modules = game:GetService("CorePackages").AppTempCommon
local Action = require(Modules.Common.Action)
return Action("RECEIVED_USER_INVITE_STATUS", function(userId, inviteStatus)
return {
userId = tostring(userId),
inviteStatus = inviteStatus,
}
end)
@@ -0,0 +1,9 @@
local CorePackages = game:GetService("CorePackages")
local Action = require(CorePackages.AppTempCommon.Common.Action)
return Action("SET_CONTROL_LAYOUT", function(controlLayout)
return {
controlLayout = controlLayout
}
end)
@@ -0,0 +1,9 @@
local CorePackages = game:GetService("CorePackages")
local Action = require(CorePackages.AppTempCommon.Common.Action)
return Action("SET_CURRENT_PAGE", function(pageKey)
return {
newPage = pageKey,
}
end)
@@ -0,0 +1,11 @@
--Remove with isNewGamepadMenuEnabled
local CorePackages = game:GetService("CorePackages")
local Action = require(CorePackages.AppTempCommon.Common.Action)
return Action("SET_GAME_DESCRIPTIONS", function(newDescriptions)
return {
newDescriptions = newDescriptions,
}
end)
@@ -0,0 +1,10 @@
local CorePackages = game:GetService("CorePackages")
local Action = require(CorePackages.AppTempCommon.Common.Action)
return Action("SET_FRIEND_STATUS", function(userId, friendStatus)
return {
userId = userId,
friendStatus = friendStatus,
}
end)
@@ -0,0 +1,11 @@
local CorePackages = game:GetService("CorePackages")
local Action = require(CorePackages.AppTempCommon.Common.Action)
return Action("SET_GAME_NAME_AND_DESCRIPTIONS", function(gameNameLocaleMap, gameDescriptionsLocaleMap, sourceLocale)
return {
gameNames = gameNameLocaleMap,
gameDescriptions = gameDescriptionsLocaleMap,
sourceLocale = sourceLocale,
}
end)
@@ -0,0 +1,9 @@
local CorePackages = game:GetService("CorePackages")
local Action = require(CorePackages.AppTempCommon.Common.Action)
return Action("SET_INPUT_TYPE", function(inputType)
return {
inputType = inputType,
}
end)
@@ -0,0 +1,9 @@
local CorePackages = game:GetService("CorePackages")
local Action = require(CorePackages.AppTempCommon.Common.Action)
return Action("SET_INSPECT_MENU_ENABLED", function(enabled)
return {
enabled = enabled,
}
end)
@@ -0,0 +1,11 @@
--Remove with isNewGamepadMenuEnabled
local CorePackages = game:GetService("CorePackages")
local Action = require(CorePackages.AppTempCommon.Common.Action)
return Action("SET_LANGUAGE_CODE_MAP", function(languageCodeMap)
return {
languageCodeMap = languageCodeMap,
}
end)
@@ -0,0 +1,9 @@
local CorePackages = game:GetService("CorePackages")
local Action = require(CorePackages.AppTempCommon.Common.Action)
return Action("SET_LOCALE_ID", function(localeId)
return {
newLocaleId = localeId,
}
end)
@@ -0,0 +1,9 @@
local CorePackages = game:GetService("CorePackages")
local Action = require(CorePackages.AppTempCommon.Common.Action)
return Action("SET_MENU_ICON_TOOLTIP_OPEN", function(status)
return {
menuIconTooltipOpen = status,
}
end)
@@ -0,0 +1,9 @@
local CorePackages = game:GetService("CorePackages")
local Action = require(CorePackages.AppTempCommon.Common.Action)
return Action("SET_MENU_OPEN", function(isMenuOpen)
return {
isMenuOpen = isMenuOpen,
}
end)
@@ -0,0 +1,11 @@
--Remove with isNewGamepadMenuEnabled
local CorePackages = game:GetService("CorePackages")
local Action = require(CorePackages.AppTempCommon.Common.Action)
return Action("SET_GAME_NAMES", function(newNames)
return {
newNames = newNames,
}
end)
@@ -0,0 +1,10 @@
local CorePackages = game:GetService("CorePackages")
local Action = require(CorePackages.AppTempCommon.Common.Action)
return Action("SET_PLACE_INFO", function(placeName, placeDescription)
return {
placeName = placeName,
placeDescription = placeDescription,
}
end)
@@ -0,0 +1,10 @@
local CorePackages = game:GetService("CorePackages")
local Action = require(CorePackages.AppTempCommon.Common.Action)
return Action("SET_RESPAWN_BEHAVIOR", function(respawnEnabled, respawnCallback)
return {
respawnEnabled = respawnEnabled,
customCallback = respawnCallback,
}
end)
@@ -0,0 +1,9 @@
local CorePackages = game:GetService("CorePackages")
local Action = require(CorePackages.AppTempCommon.Common.Action)
return Action("SET_RESPAWNING", function(isRespawning)
return {
respawning = isRespawning,
}
end)
@@ -0,0 +1,9 @@
local CorePackages = game:GetService("CorePackages")
local Action = require(CorePackages.AppTempCommon.Common.Action)
return Action("SET_SCREEN_SIZE", function(screenSize)
return {
newScreenSize = screenSize,
}
end)
@@ -0,0 +1,11 @@
--Remove with isNewGamepadMenuEnabled
local CorePackages = game:GetService("CorePackages")
local Action = require(CorePackages.AppTempCommon.Common.Action)
return Action("SET_SOURCE_LANGUAGE_CODE", function(languageCode)
return {
languageCode = languageCode,
}
end)
@@ -0,0 +1,9 @@
local CorePackages = game:GetService("CorePackages")
local Action = require(CorePackages.AppTempCommon.Common.Action)
return Action("SET_VIDEO_RECORDING", function (recording)
return {
recording = recording,
}
end)
@@ -0,0 +1,7 @@
local CorePackages = game:GetService("CorePackages")
local Action = require(CorePackages.AppTempCommon.Common.Action)
return Action("START_LEAVING_GAME", function()
return {}
end)
@@ -0,0 +1,67 @@
--[[
The topbar button that opens the in-game menu.
]]
local CorePackages = game:GetService("CorePackages")
local CoreGui = game:GetService("CoreGui")
local RobloxGui = CoreGui:WaitForChild("RobloxGui")
local InGameMenuDependencies = require(CorePackages.InGameMenuDependencies)
local Roact = InGameMenuDependencies.Roact
local RoactRodux = InGameMenuDependencies.RoactRodux
local ViewportOverlay = require(script.Parent.ViewportOverlay)
local SideNavigation = require(script.Parent.SideNavigation)
local LeaveGameDialog = require(script.Parent.LeaveGameDialog)
local PageContainer = require(script.Parent.PageContainer)
local RespawnDialog = require(script.Parent.RespawnDialog)
local ReportDialog = require(script.Parent.ReportPage.ReportDialog)
local ReportSentDialog = require(script.Parent.ReportPage.ReportSentDialog)
local ControlLayoutSetter = require(script.Parent.ControlsPage.ControlLayoutSetter)
local Connection = require(script.Parent.Connection)
local EducationalPopup = require(script.Parent.EducationalPopup)
local MenuIconTooltip = require(script.Parent.MenuIconTooltip)
local GetFFlagEducationalPopupOnNativeClose = require(RobloxGui.Modules.Flags.GetFFlagEducationalPopupOnNativeClose)
local FFlagLuaMenuPerfImprovements = require(script.Parent.Parent.Flags.FFlagLuaMenuPerfImprovements)
local GetFFlagInGameMenuIconTooltip = require(RobloxGui.Modules.Flags.GetFFlagInGameMenuIconTooltip)
local function App(props)
if FFlagLuaMenuPerfImprovements then
return Roact.createFragment({
Content = props.visible and Roact.createFragment({
Overlay = Roact.createElement(ViewportOverlay),
SideNavigation = Roact.createElement(SideNavigation),
LeaveGameDialog = Roact.createElement(LeaveGameDialog),
PageContainer = Roact.createElement(PageContainer),
RespawnDialog = Roact.createElement(RespawnDialog),
ReportDialog = Roact.createElement(ReportDialog),
ReportSentDialog = Roact.createElement(ReportSentDialog),
ControlLayoutSetter = Roact.createElement(ControlLayoutSetter),
EducationalPopup = GetFFlagEducationalPopupOnNativeClose() and Roact.createElement(EducationalPopup) or nil,
MenuIconTooltip = GetFFlagInGameMenuIconTooltip() and Roact.createElement(MenuIconTooltip) or nil,
}) or nil,
Connection = Roact.createElement(Connection),
})
else
return Roact.createFragment({
Overlay = Roact.createElement(ViewportOverlay),
SideNavigation = Roact.createElement(SideNavigation),
LeaveGameDialog = Roact.createElement(LeaveGameDialog),
PageContainer = Roact.createElement(PageContainer),
RespawnDialog = Roact.createElement(RespawnDialog),
ReportDialog = Roact.createElement(ReportDialog),
ReportSentDialog = Roact.createElement(ReportSentDialog),
ControlLayoutSetter = Roact.createElement(ControlLayoutSetter),
Connection = Roact.createElement(Connection),
EducationalPopup = GetFFlagEducationalPopupOnNativeClose() and Roact.createElement(EducationalPopup) or nil,
MenuIconTooltip = GetFFlagInGameMenuIconTooltip() and Roact.createElement(MenuIconTooltip) or nil,
})
end
end
local function mapStateToProps(state, props)
return { visible = state.isMenuOpen }
end
return RoactRodux.connect(mapStateToProps, nil)(App)
@@ -0,0 +1,48 @@
return function()
local CorePackages = game:GetService("CorePackages")
local InGameMenuDependencies = require(CorePackages.InGameMenuDependencies)
local Roact = InGameMenuDependencies.Roact
local RoactRodux = InGameMenuDependencies.RoactRodux
local Rodux = InGameMenuDependencies.Rodux
local UIBlox = InGameMenuDependencies.UIBlox
local InGameMenu = script.Parent.Parent
local reducer = require(InGameMenu.reducer)
local Localization = require(InGameMenu.Localization.Localization)
local LocalizationProvider = require(InGameMenu.Localization.LocalizationProvider)
local InGameMenuPolicy = require(InGameMenu.InGameMenuPolicy)
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 App = require(script.Parent.App)
it("should create and destroy without errors", function()
local element = Roact.createElement(RoactRodux.StoreProvider, {
store = Rodux.Store.new(reducer)
}, {
InGameMenuPolicy = Roact.createElement(InGameMenuPolicy.Provider, {
policy = { InGameMenuPolicy.Mapper },
}, {
ThemeProvider = Roact.createElement(UIBlox.Core.Style.Provider, {
style = appStyle,
}, {
LocalizationProvider = Roact.createElement(LocalizationProvider, {
localization = Localization.new("en-us"),
}, {
App = Roact.createElement(App),
}),
}),
}),
})
local instance = Roact.mount(element)
Roact.unmount(instance)
end)
end
@@ -0,0 +1,47 @@
--[[
Displays an image from the asset dictionary, slicing it if the asset
requires that.
]]
local CorePackages = game:GetService("CorePackages")
local InGameMenuDependencies = require(CorePackages.InGameMenuDependencies)
local Roact = InGameMenuDependencies.Roact
local Cryo = InGameMenuDependencies.Cryo
local UIBlox = InGameMenuDependencies.UIBlox
local InGameMenu = script.Parent.Parent
local ImageSetLabel = UIBlox.Core.ImageSet.Label
local ImageSetButton = UIBlox.Core.ImageSet.Button
local Assets = require(InGameMenu.Resources.Assets)
local function makeAssetImageComponent(component)
return function(props)
local imageKey = props.imageKey
local imageData = Assets.Images[imageKey]
local imageProps
if typeof(imageData) == "string" then
imageProps = {
Image = imageData,
}
else
imageProps = imageData
end
local mergedProps = Cryo.Dictionary.join(props, {
imageKey = Cryo.None,
}, imageProps, {
BackgroundTransparency = 1,
})
return Roact.createElement(component, mergedProps)
end
end
return {
Label = makeAssetImageComponent(ImageSetLabel),
Button = makeAssetImageComponent(ImageSetButton),
}
@@ -0,0 +1,70 @@
local CorePackages = game:GetService("CorePackages")
local InGameMenuDependencies = require(CorePackages.InGameMenuDependencies)
local Roact = InGameMenuDependencies.Roact
local t = InGameMenuDependencies.t
local BarOnTopScrollingFrame = Roact.PureComponent:extend("BarOnTopScrollingFrame")
BarOnTopScrollingFrame.validateProps = t.strictInterface({
Size = t.UDim2,
Position = t.optional(t.UDim2),
CanvasSize = t.UDim2,
scrollBarOffset = t.integer,
ScrollingEnabled = t.optional(t.boolean),
[Roact.Children] = t.table,
})
BarOnTopScrollingFrame.defaultProps = {
ScrollingEnabled = true,
}
function BarOnTopScrollingFrame:init()
self.state = {
canvasPosition = Vector2.new(0, 0),
}
end
function BarOnTopScrollingFrame:render()
return Roact.createElement("Frame", {
Size = self.props.Size,
Position = self.props.Position,
BackgroundTransparency = 1,
ClipsDescendants = true,
}, {
DisplayScrollingFrame = Roact.createElement("ScrollingFrame", {
BackgroundTransparency = 1,
Size = UDim2.new(1, -self.props.scrollBarOffset, 1, 0),
CanvasSize = self.props.CanvasSize - UDim2.new(0, self.props.scrollBarOffset, 0, 0),
ScrollBarThickness = 8,
BorderSizePixel = 0,
ZIndex = 2,
ScrollingEnabled = self.props.ScrollingEnabled,
[Roact.Change.CanvasPosition] = function(rbx)
self:setState({
CanvasPosition = rbx.CanvasPosition
})
end,
}),
ContentsScrollingFrame = Roact.createElement("ScrollingFrame", {
BackgroundTransparency = 1,
Size = UDim2.new(1, -self.props.scrollBarOffset, 1, 0),
CanvasSize = self.props.CanvasSize - UDim2.new(0, self.props.scrollBarOffset, 0, 0),
CanvasPosition = self.state.CanvasPosition,
ScrollBarThickness = 0,
BorderSizePixel = 0,
ClipsDescendants = false,
ScrollingEnabled = false,
}, {
Roact.createElement("Frame", {
BackgroundTransparency = 1,
Size = UDim2.new(1, self.props.scrollBarOffset, 1, 0),
}, self.props[Roact.Children]),
}),
})
end
return BarOnTopScrollingFrame
@@ -0,0 +1,37 @@
local CoreGui = game:GetService("CoreGui")
local CorePackages = game:GetService("CorePackages")
local RunService = game:GetService("RunService")
local InGameMenuDependencies = require(CorePackages.InGameMenuDependencies)
local Roact = InGameMenuDependencies.Roact
local t = InGameMenuDependencies.t
local BlurredModalPortal = Roact.PureComponent:extend("BlurredModalPortal")
BlurredModalPortal.validateProps = t.strictInterface({
Enabled = t.boolean,
[Roact.Children] = t.optional(t.table)
})
function BlurredModalPortal:render()
return Roact.createElement(Roact.Portal, {
target = CoreGui
}, {
InGameMenuModalBlur = Roact.createElement("ScreenGui", {
OnTopOfCoreBlur = true,
Enabled = self.props.Enabled,
IgnoreGuiInset = true,
}, self.props[Roact.Children])
})
end
function BlurredModalPortal:didUpdate(prevProps)
local wasEnabled = prevProps.Enabled
local enabled = self.props.Enabled
if wasEnabled ~= enabled then
RunService:SetRobloxGuiFocused(enabled)
end
end
return BlurredModalPortal
@@ -0,0 +1,227 @@
local CoreGui = game:GetService("CoreGui")
local RunService = game:GetService("RunService")
local TextService = game:GetService("TextService")
local CorePackages = game:GetService("CorePackages")
local ContextActionService = game:GetService("ContextActionService")
local InGameMenuDependencies = require(CorePackages.InGameMenuDependencies)
local Roact = InGameMenuDependencies.Roact
local UIBlox = InGameMenuDependencies.UIBlox
local t = InGameMenuDependencies.t
local withStyle = UIBlox.Core.Style.withStyle
local InGameMenu = script.Parent.Parent
local Assets = require(InGameMenu.Resources.Assets)
local ThemedTextLabel = require(script.Parent.ThemedTextLabel)
local ImageSetLabel = UIBlox.Core.ImageSet.Label
local MODAL_CONFIRM_ACTION = "InGameMenuModalConfirm"
local MODAL_CANCEL_ACTION = "InGameMenuModalCancel"
local ConfirmationDialog = Roact.PureComponent:extend("ConfirmationDialog")
ConfirmationDialog.validateProps = t.strictInterface({
bodyText = t.string,
cancelText = t.string,
confirmText = t.string,
titleText = t.string,
bindReturnToConfirm = t.boolean,
onCancel = t.callback,
onConfirm = t.callback,
blurBackground = t.boolean,
visible = t.boolean,
})
ConfirmationDialog.defaultProps = {
bodyText = "This is some body text. It will wrap when needed.",
cancelText = "Cancel",
confirmText = "Confirm",
titleText = "Dialog Title",
blurBackground = false,
}
function ConfirmationDialog:render()
local props = self.props
return withStyle(function(style)
local bodyFont = style.Font.Body.Font
local bodyFontSize = style.Font.Body.RelativeSize * style.Font.BaseSize
local bodyText = props.bodyText
local textHeight = TextService:GetTextSize(
bodyText,
bodyFontSize,
bodyFont,
-- 335 (width) - 20px padding on left and right
Vector2.new(335 - 20 - 20, math.huge)
).Y
-- 20px padding top, 20px padding bottom
-- Minimum height of two lines of text.
-- TODO: Line height!
local bodyTextContainerHeight = 20 + math.max(textHeight, bodyFontSize * 2) + 20
return Roact.createElement(Roact.Portal, {
target = CoreGui,
}, {
InGameMenuConfirmationDialog = Roact.createElement("ScreenGui", {
DisplayOrder = 8,
IgnoreGuiInset = true,
OnTopOfCoreBlur = props.blurBackground,
Enabled = props.visible,
ZIndexBehavior = Enum.ZIndexBehavior.Sibling,
}, {
Overlay = Roact.createElement("TextButton", {
AutoButtonColor = false,
BackgroundColor3 = style.Theme.Overlay.Color,
BackgroundTransparency = style.Theme.Overlay.Transparency,
BorderSizePixel = 0,
Size = UDim2.new(1, 0, 1, 0),
Text = "",
}),
DialogMainFrame = Roact.createElement(ImageSetLabel, {
AnchorPoint = Vector2.new(0.5, 0.5),
BackgroundTransparency = 1,
Image = Assets.Images.RoundedRect.Image,
ImageColor3 = style.Theme.BackgroundUIDefault.Color,
ImageTransparency = style.Theme.BackgroundUIDefault.Transparency,
Position = UDim2.new(0.5, 0, 0.5, 0),
ScaleType = Assets.Images.RoundedRect.ScaleType,
-- button container (36px), bottom padding (20px)
-- height: title bar (48px), divider (1px), body text height,
Size = UDim2.new(0, 335, 0, 48 + 1 + bodyTextContainerHeight + 36 + 20),
SliceCenter = Assets.Images.RoundedRect.SliceCenter,
}, {
Padding = Roact.createElement("UIPadding", {
PaddingBottom = UDim.new(0, 20),
PaddingLeft = UDim.new(0, 20),
PaddingRight = UDim.new(0, 20),
}),
Layout = Roact.createElement("UIListLayout", {
FillDirection = Enum.FillDirection.Vertical,
HorizontalAlignment = Enum.HorizontalAlignment.Center,
SortOrder = Enum.SortOrder.LayoutOrder,
}),
Title = Roact.createElement(ThemedTextLabel, {
fontKey = "Header1",
themeKey = "TextEmphasis",
LayoutOrder = 1,
Size = UDim2.new(1, 0, 0, 48),
Text = props.titleText,
}),
Divider = Roact.createElement("Frame", {
BackgroundColor3 = style.Theme.Divider.Color,
BackgroundTransparency = style.Theme.Divider.Transparency,
BorderSizePixel = 0,
LayoutOrder = 3,
Size = UDim2.new(0.8, 0, 0, 1),
}),
BodyTextContainer = Roact.createElement("Frame", {
BackgroundTransparency = 1,
LayoutOrder = 4,
Size = UDim2.new(1, 0, 0, bodyTextContainerHeight),
}, {
BodyText = Roact.createElement(ThemedTextLabel, {
Size = UDim2.new(1, 0, 1, 0),
Text = props.bodyText,
TextWrapped = true,
})
}),
ButtonContainer = Roact.createElement("Frame", {
BackgroundTransparency = 1,
LayoutOrder = 5,
Size = UDim2.new(1, 0, 0, 36),
}, {
Layout = Roact.createElement("UIListLayout", {
FillDirection = Enum.FillDirection.Horizontal,
HorizontalAlignment = Enum.HorizontalAlignment.Center,
Padding = UDim.new(0, 10),
SortOrder = Enum.SortOrder.LayoutOrder,
VerticalAlignment = Enum.VerticalAlignment.Center,
}),
CancelButton = Roact.createElement(UIBlox.App.Button.SecondaryButton, {
layoutOrder = 1,
size = UDim2.new(0.5, -5, 1, 0),
text = props.cancelText,
onActivated = props.onCancel,
}),
ConfirmButton = Roact.createElement(UIBlox.App.Button.PrimarySystemButton, {
layoutOrder = 2,
size = UDim2.new(0.5, -5, 1, 0),
text = props.confirmText,
onActivated = props.onConfirm,
}),
}),
}),
}),
})
end)
end
function ConfirmationDialog:updateBlur()
local shouldBlur = self.props.blurBackground and self.props.visible
RunService:SetRobloxGuiFocused(shouldBlur)
end
-- TODO: consider if we want to have a button selection model like the old menu rather than enter always confirming
function ConfirmationDialog:bindActions()
local function confirmFunc(actionName, inputState, input)
if inputState == Enum.UserInputState.Begin then
self.props.onConfirm()
end
end
if self.props.bindReturnToConfirm then
ContextActionService:BindCoreAction(
MODAL_CONFIRM_ACTION, confirmFunc, false, Enum.KeyCode.Return)
end
local function cancelFunc(actionName, inputState, input)
if inputState == Enum.UserInputState.Begin then
self.props.onCancel()
end
end
ContextActionService:BindCoreAction(
MODAL_CANCEL_ACTION, cancelFunc, false, Enum.KeyCode.Escape)
end
function ConfirmationDialog:unbindActions()
ContextActionService:UnbindCoreAction(MODAL_CONFIRM_ACTION)
ContextActionService:UnbindCoreAction(MODAL_CANCEL_ACTION)
end
function ConfirmationDialog:didMount()
self:updateBlur()
if self.props.visible then
self:bindActions()
end
end
function ConfirmationDialog:didUpdate()
self:updateBlur()
if self.props.visible then
self:bindActions()
else
self:unbindActions()
end
end
function ConfirmationDialog:willUnmount()
if self.props.blurBackground then
RunService:SetRobloxGuiFocused(false)
end
self:unbindActions()
end
return ConfirmationDialog
@@ -0,0 +1,59 @@
return function()
local CoreGui = game:GetService("CoreGui")
local CorePackages = game:GetService("CorePackages")
local InGameMenuDependencies = require(CorePackages.InGameMenuDependencies)
local Roact = InGameMenuDependencies.Roact
local UIBlox = InGameMenuDependencies.UIBlox
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 ConfirmationDialog = require(script.Parent.ConfirmationDialog)
local dummyDialogProps = {
bodyText = "Hello world!",
cancelText = "Cancel",
confirmText = "Confirm",
titleText = "Title",
bindReturnToConfirm = false,
onCancel = function()
print("cancel")
end,
onConfirm = function()
print("confirm")
end,
blurBackground = false,
visible = true,
}
it("should create and destroy without errors", function()
local element = Roact.createElement(UIBlox.Core.Style.Provider, {
style = appStyle,
}, {
ConfirmationDialog = Roact.createElement(ConfirmationDialog, dummyDialogProps),
})
local instance = Roact.mount(element)
Roact.unmount(instance)
end)
it("should be portaled into CoreGui", function()
local element = Roact.createElement(UIBlox.Core.Style.Provider, {
style = appStyle,
}, {
ConfirmationDialog = Roact.createElement(ConfirmationDialog, dummyDialogProps),
})
local instance = Roact.mount(element)
expect(CoreGui:FindFirstChild("InGameMenuConfirmationDialog")).to.never.equal(nil)
Roact.unmount(instance)
end)
end
@@ -0,0 +1,63 @@
local CorePackages = game:GetService("CorePackages")
local Players = game:GetService("Players")
local InGameMenuDependencies = require(CorePackages.InGameMenuDependencies)
local Roact = InGameMenuDependencies.Roact
local RoactRodux = InGameMenuDependencies.RoactRodux
local InGameMenu = script.Parent.Parent.Parent
local SetFriendStatus = require(InGameMenu.Actions.SetFriendStatus)
local ExternalEventConnection = require(InGameMenu.Utility.ExternalEventConnection)
local FriendStatusConnector = Roact.PureComponent:extend("FriendStatusConnector")
function FriendStatusConnector:init()
self.state = {
localPlayer = Players.LocalPlayer,
}
end
function FriendStatusConnector:render()
local connections = {
PlayerAdded = Roact.createElement(ExternalEventConnection, {
event = Players.PlayerAdded,
callback = function(player)
if player == Players.LocalPlayer then
self:setState({
localPlayer = player,
})
else
self.props.setFriendStatus(player.UserId, Enum.FriendStatus.Unknown)
end
end,
}),
PlayerRemoving = Roact.createElement(ExternalEventConnection, {
event = Players.PlayerRemoving,
callback = function(player)
self.props.setFriendStatus(player.UserId, nil)
end,
}),
}
if self.state.localPlayer then
connections.FriendStatusChanged = Roact.createElement(ExternalEventConnection, {
event = Players.LocalPlayer.FriendStatusChanged,
callback = function(player, friendStatus)
self.props.setFriendStatus(player.UserId, friendStatus)
end,
})
end
return Roact.createFragment(connections)
end
return RoactRodux.UNSTABLE_connect2(nil, function(dispatch)
return {
setFriendStatus = function(player, friendStatus)
return dispatch(SetFriendStatus(player, friendStatus))
end,
}
end)(FriendStatusConnector)
@@ -0,0 +1,63 @@
local CorePackages = game:GetService("CorePackages")
local UserInputService = game:GetService("UserInputService")
local InGameMenuDependencies = require(CorePackages.InGameMenuDependencies)
local Roact = InGameMenuDependencies.Roact
local RoactRodux = InGameMenuDependencies.RoactRodux
local InGameMenu = script.Parent.Parent.Parent
local SetInputType = require(InGameMenu.Actions.SetInputType)
local ExternalEventConnection = require(InGameMenu.Utility.ExternalEventConnection)
local Constants = require(InGameMenu.Resources.Constants)
local InputType = Constants.InputType
local LastInputTypeConnector = Roact.PureComponent:extend("LastInputTypeConnector")
local inputTypeMap = {
[Enum.UserInputType.MouseButton2] = InputType.MouseAndKeyBoard,
[Enum.UserInputType.MouseButton3] = InputType.MouseAndKeyBoard,
[Enum.UserInputType.MouseWheel] = InputType.MouseAndKeyBoard,
[Enum.UserInputType.MouseMovement] = InputType.MouseAndKeyBoard,
[Enum.UserInputType.Keyboard] = InputType.MouseAndKeyBoard,
[Enum.UserInputType.Gamepad1] = InputType.Gamepad,
[Enum.UserInputType.Gamepad2] = InputType.Gamepad,
[Enum.UserInputType.Gamepad3] = InputType.Gamepad,
[Enum.UserInputType.Gamepad4] = InputType.Gamepad,
[Enum.UserInputType.Gamepad5] = InputType.Gamepad,
[Enum.UserInputType.Gamepad6] = InputType.Gamepad,
[Enum.UserInputType.Gamepad7] = InputType.Gamepad,
[Enum.UserInputType.Gamepad8] = InputType.Gamepad,
[Enum.UserInputType.Touch] = InputType.Touch,
}
function LastInputTypeConnector:init()
local initalInputType = inputTypeMap[UserInputService:GetLastInputType()]
if initalInputType then
self.props.setInputType(initalInputType)
end
end
function LastInputTypeConnector:render()
return Roact.createElement(ExternalEventConnection, {
event = UserInputService.LastInputTypeChanged,
callback = function(lastInputType)
local inputType = inputTypeMap[lastInputType]
if inputType then
self.props.setInputType(inputType)
end
end,
})
end
return RoactRodux.UNSTABLE_connect2(nil, function(dispatch)
return {
setInputType = function(inputType)
return dispatch(SetInputType(inputType))
end,
}
end)(LastInputTypeConnector)
@@ -0,0 +1,62 @@
local CorePackages = game:GetService("CorePackages")
local UserInputService = game:GetService("UserInputService")
local VRService = game:GetService("VRService")
local InGameMenuDependencies = require(CorePackages.InGameMenuDependencies)
local Roact = InGameMenuDependencies.Roact
local RoactRodux = InGameMenuDependencies.RoactRodux
local t = InGameMenuDependencies.t
local InGameMenu = script.Parent.Parent.Parent
local Constants = require(InGameMenu.Resources.Constants)
local ExternalEventConnection = require(InGameMenu.Utility.ExternalEventConnection)
local InputType = Constants.InputType
local MouseOverrideManager = Roact.PureComponent:extend("MouseOverrideManager")
MouseOverrideManager.validateProps = t.strictInterface({
menuOpen = t.boolean,
inputType = t.string,
})
function MouseOverrideManager:render()
return Roact.createElement(ExternalEventConnection, {
event = UserInputService:GetPropertyChangedSignal("OverrideMouseIconBehavior"),
callback = function()
-- Another menu that overrides the mouse icon behaviour has closed.
-- But we are still open so we need to keep re-override the icon behaviour.
if self.props.menuOpen and UserInputService.OverrideMouseIconBehavior == Enum.OverrideMouseIconBehavior.None then
self:updateMouseIconOverride()
end
end,
})
end
function MouseOverrideManager:updateMouseIconOverride()
if self.props.menuOpen then
if self.props.inputType == InputType.Gamepad or VRService.VREnabled then
UserInputService.OverrideMouseIconBehavior = Enum.OverrideMouseIconBehavior.ForceHide
else
UserInputService.OverrideMouseIconBehavior = Enum.OverrideMouseIconBehavior.ForceShow
end
else
UserInputService.OverrideMouseIconBehavior = Enum.OverrideMouseIconBehavior.None
end
end
function MouseOverrideManager:didMount()
self:updateMouseIconOverride()
end
function MouseOverrideManager:didUpdate()
self:updateMouseIconOverride()
end
return RoactRodux.UNSTABLE_connect2(function(state)
return {
menuOpen = state.isMenuOpen,
inputType = state.displayOptions.inputType,
}
end, nil)(MouseOverrideManager)
@@ -0,0 +1,83 @@
local GuiService = game:GetService("GuiService")
local AppStorageService = game:GetService("AppStorageService")
local RbxAnalyticsService = game:GetService("RbxAnalyticsService")
local CorePackages = game:GetService("CorePackages")
local InGameMenuDependencies = require(CorePackages.InGameMenuDependencies)
local Roact = InGameMenuDependencies.Roact
local RoactRodux = InGameMenuDependencies.RoactRodux
local t = InGameMenuDependencies.t
local InGameMenu = script.Parent.Parent.Parent
local OpenNativeClosePrompt = require(InGameMenu.Actions.OpenNativeClosePrompt)
local ExternalEventConnection = require(InGameMenu.Utility.ExternalEventConnection)
local InGameMenuPolicy = require(InGameMenu.InGameMenuPolicy)
local LOCAL_STORAGE_KEY_NATIVE_CLOSE = "NativeCloseLuaPromptDisplayCount"
local notificationTypes = GuiService:GetNotificationTypeList()
local NativeCloseEventConnector = Roact.PureComponent:extend("NativeCloseEventConnector")
NativeCloseEventConnector.validateProps = t.strictInterface({
openNativeClosePrompt = t.callback,
isEducationalPopupEnabled = t.boolean,
maxDisplayCount = t.integer,
})
function NativeCloseEventConnector:init()
local function getDisplayCount()
return tonumber(AppStorageService:GetItem(LOCAL_STORAGE_KEY_NATIVE_CLOSE)) or 0
end
local function setDisplayCount(number)
AppStorageService:SetItem(LOCAL_STORAGE_KEY_NATIVE_CLOSE, tostring(number))
AppStorageService:flush()
end
self.onNativeCloseConnect = function()
if self.props.isEducationalPopupEnabled then
RbxAnalyticsService:ReportCounter("LeaveGame_NativeClose", 1)
if self.props.isClosingApp then
GuiService:BroadcastNotification("", notificationTypes.NATIVE_EXIT)
return
end
local displayCount = getDisplayCount()
local maxDisplayCount = self.props.maxDisplayCount
if displayCount >= maxDisplayCount then
GuiService:BroadcastNotification("", notificationTypes.NATIVE_EXIT)
else
self.props.openNativeClosePrompt()
setDisplayCount(displayCount + 1)
end
end
end
end
function NativeCloseEventConnector:render()
return Roact.createElement(ExternalEventConnection, {
event = GuiService.NativeClose,
callback = self.onNativeCloseConnect,
})
end
NativeCloseEventConnector = InGameMenuPolicy.connect(function(appPolicy, props)
return {
isEducationalPopupEnabled = appPolicy.enableEducationalPopup(),
maxDisplayCount = appPolicy.educationalPopupMaxDisplayCount(),
}
end)(NativeCloseEventConnector)
return RoactRodux.UNSTABLE_connect2(function(state, props)
return {
isClosingApp = state.nativeClosePrompt.closingApp,
}
end, function(dispatch)
return {
openNativeClosePrompt = function()
return dispatch(OpenNativeClosePrompt())
end,
}
end)(NativeCloseEventConnector)
@@ -0,0 +1,52 @@
return function()
local CorePackages = game:GetService("CorePackages")
local InGameMenuDependencies = require(CorePackages.InGameMenuDependencies)
local Roact = InGameMenuDependencies.Roact
local RoactRodux = InGameMenuDependencies.RoactRodux
local Rodux = InGameMenuDependencies.Rodux
local InGameMenu = script.Parent.Parent.Parent
local reducer = require(InGameMenu.reducer)
local InGameMenuPolicy = require(InGameMenu.InGameMenuPolicy)
local mockEducationalPolicyMapper = function(policy)
return {
enableEducationalPopup = function()
return false
end,
educationalPopupMaxDisplayCount = function()
return 0
end,
}
end
local NativeCloseEventConnector = require(script.Parent.NativeCloseEventConnector)
it("should create and destroy without errors on engine feature enabled", function()
local isNativeCloseIntercept = game:GetEngineFeature("NativeCloseIntercept")
--[[
NativeCloseEventConnector relies on GuiService.NativeClose which guarded
by an engine feature NativeCloseIntercept
Skip unit test for older engine version which NativeCloseIntercept is disabled
]]
if not isNativeCloseIntercept then
SKIP()
return
end
local element = Roact.createElement(RoactRodux.StoreProvider, {
store = Rodux.Store.new(reducer)
}, {
PolicyProvider = Roact.createElement(InGameMenuPolicy.Provider, {
policy = { mockEducationalPolicyMapper },
}, {
TestComponent = Roact.createElement(NativeCloseEventConnector, {
openNativeClosePrompt = function() end,
}),
})
})
local instance = Roact.mount(element)
Roact.unmount(instance)
end)
end
@@ -0,0 +1,35 @@
--!nocheck
local CorePackages = game:GetService("CorePackages")
local GameSettings = settings():WaitForChild("Game Options") -- TODO: we should migrate the APIs from here to CoreGui
local InGameMenu = script.Parent.Parent.Parent
local InGameMenuDependencies = require(CorePackages.InGameMenuDependencies)
local Roact = InGameMenuDependencies.Roact
local RoactRodux = InGameMenuDependencies.RoactRodux
local SetVideoRecording = require(InGameMenu.Actions.SetVideoRecording)
local ExternalEventConnection = require(InGameMenu.Utility.ExternalEventConnection)
local VideoRecordConnector = Roact.PureComponent:extend("VideoRecordConnector")
function VideoRecordConnector:init()
self.props.setVideoRecording(GameSettings.VideoRecording)
end
function VideoRecordConnector:render()
return Roact.createElement(ExternalEventConnection, {
event = GameSettings:GetPropertyChangedSignal("VideoRecording"),
callback = function ()
self.props.setVideoRecording(GameSettings.VideoRecording)
end,
})
end
return RoactRodux.UNSTABLE_connect2(nil, function (dispatch)
return {
setVideoRecording = function (state)
return dispatch(SetVideoRecording(state))
end,
}
end)(VideoRecordConnector)
@@ -0,0 +1,41 @@
local CorePackages = game:GetService("CorePackages")
local InGameMenuDependencies = require(CorePackages.InGameMenuDependencies)
local Roact = InGameMenuDependencies.Roact
local t = InGameMenuDependencies.t
local FriendStatusConnector = require(script.FriendStatusConnector)
local LastInputTypeConnector = require(script.LastInputTypeConnector)
local MouseOverrideManager = require(script.MouseOverrideManager)
local VideoRecordConnector = require(script.VideoRecordConnector)
local NativeCloseEventConnector = require(script.NativeCloseEventConnector)
local RobloxGui = game:GetService("CoreGui"):WaitForChild("RobloxGui")
local InGameMenuPolicy = require(RobloxGui.Modules.InGameMenu.InGameMenuPolicy)
local GetFFlagUseRoactPolicyProvider = require(RobloxGui.Modules.Flags.GetFFlagUseRoactPolicyProvider)
local validateProps = t.strictInterface({
isEducationalPopupEnabled = t.optional(t.boolean),
})
local function Connection(props)
assert(validateProps)
return Roact.createFragment({
FriendStatusConnector = Roact.createElement(FriendStatusConnector),
LastInputTypeConnector = Roact.createElement(LastInputTypeConnector),
MouseOverrideManager = Roact.createElement(MouseOverrideManager),
VideoRecordConnector = Roact.createElement(VideoRecordConnector),
NativeCloseEventConnector = props.isEducationalPopupEnabled and Roact.createElement(NativeCloseEventConnector)
or nil,
})
end
if GetFFlagUseRoactPolicyProvider() then
Connection = InGameMenuPolicy.connect(function(appPolicy, props)
return {
isEducationalPopupEnabled = appPolicy.enableEducationalPopup(),
}
end)(Connection)
end
return Connection
@@ -0,0 +1,139 @@
local CorePackages = game:GetService("CorePackages")
local InGameMenuDependencies = require(CorePackages.InGameMenuDependencies)
local Roact = InGameMenuDependencies.Roact
local t = InGameMenuDependencies.t
local InGameMenu = script.Parent.Parent.Parent
local GlobalConfig = require(InGameMenu.GlobalConfig)
local ThemedTextLabel = require(InGameMenu.Components.ThemedTextLabel)
local KeyLabel = require(InGameMenu.Components.KeyLabel)
local PointerLabel = require(InGameMenu.Components.PointerLabel)
local Divider = require(InGameMenu.Components.Divider)
local withLocalization = require(InGameMenu.Localization.withLocalization)
local CONTROL_ENTRY_HEIGHT = 50
local validateProps = t.strictInterface({
actionDescription = t.string,
inputs = t.table,
isList = t.optional(t.boolean),
hasDivider = t.boolean,
LayoutOrder = t.integer,
})
local function renderHorizontalSpacer(symbol, layoutOrder, isElipses)
local size = isElipses and UDim2.new(0, 19, 0, 24) or UDim2.new(0, 19, 0, 16)
local yAlignment = isElipses and Enum.TextYAlignment.Bottom
return Roact.createElement(ThemedTextLabel, {
themeKey = "TextEmphasis",
fontKey = "Header1",
Text = symbol,
Size = size,
TextYAlignment = yAlignment,
LayoutOrder = layoutOrder,
})
end
local function renderKey(input, layoutOrder)
local inputComponent = input.EnumType == Enum.KeyCode and KeyLabel or PointerLabel
return Roact.createElement(inputComponent, {
input = input,
LayoutOrder = layoutOrder,
})
end
local function renderInputIconContainer(inputs, isList)
local children = {}
-- If we're rendering inputs as a "list", we render the keys next to
-- eachother, with a "..." at the end, so we want 0 padding.
local paddingWidth = not isList and 7 or 0
children.ListLayout = Roact.createElement("UIListLayout", {
FillDirection = Enum.FillDirection.Horizontal,
VerticalAlignment = Enum.VerticalAlignment.Center,
HorizontalAlignment = Enum.HorizontalAlignment.Right,
SortOrder = Enum.SortOrder.LayoutOrder,
Padding = UDim.new(0, paddingWidth),
})
local layoutOrder = 1
for i, input in ipairs(inputs) do
-- If input is a group of inputs, we iterate over that table and render
-- with "+"" as spacers. Otherwise, we render the single input with "/"
-- as the spacer.
if typeof(input) ~= "table" then
children["input"..i] = renderKey(input, layoutOrder)
layoutOrder = layoutOrder + 1
-- if "list", append elipses to the end of inputs
if isList and i == #inputs then
children.Elipses = renderHorizontalSpacer("...", layoutOrder, true)
end
else
for i2, groupedInput in ipairs(input) do
local order = string.format("%d_%d", i, i2)
children["groupedInput"..order] = renderKey(groupedInput, layoutOrder)
layoutOrder = layoutOrder + 1
if i2 < #input then
children["groupedDivider"..order] = renderHorizontalSpacer("+", layoutOrder)
layoutOrder = layoutOrder + 1
end
end
end
-- If "list", don't render dividers
if not isList and i < #inputs then
children["divider"..i] = renderHorizontalSpacer("/", layoutOrder)
layoutOrder = layoutOrder + 1
end
end
return Roact.createElement("Frame", {
BackgroundTransparency = 1,
Size = UDim2.new(0, 0, 1, 0),
Position = UDim2.new(1, 0, 0, 0),
}, children)
end
local function ControlEntry(props)
if GlobalConfig.propValidation then
assert(validateProps(props))
end
return withLocalization({
actionDescription = props.actionDescription,
})(function(localized)
return Roact.createElement("Frame", {
LayoutOrder = props.LayoutOrder,
BackgroundTransparency = 1,
Size = UDim2.new(1, 0, 0, CONTROL_ENTRY_HEIGHT)
}, {
ActionLabel = Roact.createElement(ThemedTextLabel, {
fontKey = "Header2",
themeKey = "TextEmphasis",
TextXAlignment = Enum.TextXAlignment.Left,
Size = UDim2.new(0, 200, 0, 22),
AnchorPoint = Vector2.new(0, 0.5),
Position = UDim2.new(0, 0, 0.5, 0),
Text = localized.actionDescription,
}),
Divider = props.hasDivider and Roact.createElement(Divider, {
Size = UDim2.new(1, 0, 0, 1),
Position = UDim2.new(0, 0, 1, -1),
}),
Inputs = renderInputIconContainer(props.inputs, props.isList),
})
end)
end
return ControlEntry
@@ -0,0 +1,101 @@
return function()
local CorePackages = game:GetService("CorePackages")
local InGameMenuDependencies = require(CorePackages.InGameMenuDependencies)
local Roact = InGameMenuDependencies.Roact
local Rodux = InGameMenuDependencies.Rodux
local RoactRodux = InGameMenuDependencies.RoactRodux
local UIBlox = InGameMenuDependencies.UIBlox
local InGameMenu = script.Parent.Parent.Parent
local Localization = require(InGameMenu.Localization.Localization)
local LocalizationProvider = require(InGameMenu.Localization.LocalizationProvider)
local reducer = require(InGameMenu.reducer)
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 ControlEntry = require(script.Parent.ControlEntry)
local key = Enum.KeyCode
it("should create and destroy without errors", function()
local element = Roact.createElement(RoactRodux.StoreProvider, {
store = Rodux.Store.new(reducer)
}, {
ThemeProvider = Roact.createElement(UIBlox.Core.Style.Provider, {
style = appStyle,
}, {
LocalizationProvider = Roact.createElement(LocalizationProvider, {
localization = Localization.new("en-us"),
}, {
ControlEntry = Roact.createElement(ControlEntry, {
actionDescription = "CoreScripts.InGameMenu.Controls.MoveForward",
inputs = { key.A },
isList = false,
hasDivider = false,
LayoutOrder = 5,
}),
}),
}),
})
local instance = Roact.mount(element)
Roact.unmount(instance)
end)
it("should create and destroy without errors when displaying a list", function()
local element = Roact.createElement(RoactRodux.StoreProvider, {
store = Rodux.Store.new(reducer)
}, {
ThemeProvider = Roact.createElement(UIBlox.Core.Style.Provider, {
style = appStyle,
}, {
LocalizationProvider = Roact.createElement(LocalizationProvider, {
localization = Localization.new("en-us"),
}, {
ControlEntry = Roact.createElement(ControlEntry, {
actionDescription = "CoreScripts.InGameMenu.Controls.MoveForward",
inputs = { key.A, key.B, key.C, key.D },
isList = true,
hasDivider = true,
LayoutOrder = 1,
}),
}),
}),
})
local instance = Roact.mount(element)
Roact.unmount(instance)
end)
it("should create and destroy without errors when displaying multiple key inputs", function()
local element = Roact.createElement(RoactRodux.StoreProvider, {
store = Rodux.Store.new(reducer)
}, {
ThemeProvider = Roact.createElement(UIBlox.Core.Style.Provider, {
style = appStyle,
}, {
LocalizationProvider = Roact.createElement(LocalizationProvider, {
localization = Localization.new("en-us"),
}, {
ControlEntry = Roact.createElement(ControlEntry, {
actionDescription = "CoreScripts.InGameMenu.Controls.MoveForward",
inputs = { {key.LeftSuper, key.LeftShift, key.Three} },
isList = false,
hasDivider = false,
LayoutOrder = 1,
}),
}),
}),
})
local instance = Roact.mount(element)
Roact.unmount(instance)
end)
end
@@ -0,0 +1,52 @@
local CorePackages = game:GetService("CorePackages")
local InGameMenuDependencies = require(CorePackages.InGameMenuDependencies)
local Roact = InGameMenuDependencies.Roact
local t = InGameMenuDependencies.t
local InGameMenu = script.Parent.Parent.Parent
local GlobalConfig = require(InGameMenu.GlobalConfig)
local ThemedTextLabel = require(InGameMenu.Components.ThemedTextLabel)
local withLocalization = require(InGameMenu.Localization.withLocalization)
local HEADER_PADDING_TOP = 20
local HEADER_TEXT_HEIGHT = 17
local HEADER_PADDING_BOTTOM = 8
local HEADER_HEIGHT = HEADER_PADDING_TOP + HEADER_TEXT_HEIGHT + HEADER_PADDING_BOTTOM
local validateProps = t.strictInterface({
title = t.string,
LayoutOrder = t.integer,
})
local function ControlEntry(props)
if GlobalConfig.propValidation then
assert(validateProps(props))
end
return withLocalization({
title = props.title,
})(function(localized)
return Roact.createElement("Frame", {
Size = UDim2.new(1, 0, 0, HEADER_HEIGHT),
BackgroundTransparency = 1,
LayoutOrder = props.LayoutOrder,
}, {
HeaderText = Roact.createElement(ThemedTextLabel, {
fontKey = "CaptionHeader",
themeKey = "TextMuted",
Text = localized.title,
TextXAlignment = Enum.TextXAlignment.Left,
Position = UDim2.new(0, 0, 0, HEADER_PADDING_TOP),
Size = UDim2.new(1, 0, 0, HEADER_TEXT_HEIGHT),
})
})
end)
end
return ControlEntry
@@ -0,0 +1,46 @@
return function()
local CorePackages = game:GetService("CorePackages")
local InGameMenuDependencies = require(CorePackages.InGameMenuDependencies)
local Roact = InGameMenuDependencies.Roact
local Rodux = InGameMenuDependencies.Rodux
local RoactRodux = InGameMenuDependencies.RoactRodux
local UIBlox = InGameMenuDependencies.UIBlox
local InGameMenu = script.Parent.Parent.Parent
local Localization = require(InGameMenu.Localization.Localization)
local LocalizationProvider = require(InGameMenu.Localization.LocalizationProvider)
local reducer = require(InGameMenu.reducer)
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 ControlEntryHeader = require(script.Parent.ControlEntryHeader)
it("should create and destroy without errors", function()
local element = Roact.createElement(RoactRodux.StoreProvider, {
store = Rodux.Store.new(reducer)
}, {
ThemeProvider = Roact.createElement(UIBlox.Core.Style.Provider, {
style = appStyle,
}, {
LocalizationProvider = Roact.createElement(LocalizationProvider, {
localization = Localization.new("en-us"),
}, {
ControlEntryHeader = Roact.createElement(ControlEntryHeader, {
title = "CoreScripts.InGameMenu.Controls.CharacterMovementTitle",
LayoutOrder = 1,
}),
}),
}),
})
local instance = Roact.mount(element)
Roact.unmount(instance)
end)
end
@@ -0,0 +1,136 @@
local CorePackages = game:GetService("CorePackages")
local InGameMenuDependencies = require(CorePackages.InGameMenuDependencies)
local Roact = InGameMenuDependencies.Roact
local RoactRodux = InGameMenuDependencies.RoactRodux
local UIBlox = InGameMenuDependencies.UIBlox
local t = InGameMenuDependencies.t
local withStyle = UIBlox.Core.Style.withStyle
local InGameMenu = script.Parent.Parent.Parent
local GlobalConfig = require(InGameMenu.GlobalConfig)
local ThemedTextLabel = require(script.Parent.Parent.ThemedTextLabel)
local Divider = require(script.Parent.Parent.Divider)
-- local KeyLabel = require(script.Parent.KeyLabel)
local SetCurrentPage = require(InGameMenu.Actions.SetCurrentPage)
local withLocalization = require(InGameMenu.Localization.withLocalization)
local Assets = require(InGameMenu.Resources.Assets)
local Constants = require(InGameMenu.Resources.Constants)
local ImageSetButton = UIBlox.Core.ImageSet.Button
local ImageSetLabel = UIBlox.Core.ImageSet.Label
local HEADER_HEIGHT = 132
local HEADER_SIDE_PADDING = 95
local HEADER_CONTENT_Y_CENTER = 100
local validateProps = t.strictInterface({
titleText = t.string,
[Roact.Children] = t.optional(t.table),
onClosed = t.callback,
})
local function ControlLayoutContainer(props)
if GlobalConfig.propValidation then
assert(validateProps(props))
end
return withLocalization({
titleText = props.titleText,
})(function(localized)
return withStyle(function(style)
return Roact.createElement("Frame", {
Size = UDim2.new(1, 0, 1, 0),
BackgroundColor3 = style.Theme.Overlay.Color,
BackgroundTransparency = style.Theme.Overlay.Transparency,
Active = true,
}, {
PageContent = Roact.createElement("Frame", {
BackgroundTransparency = 1,
Size = UDim2.new(1, 0, 1, -HEADER_HEIGHT),
Position = UDim2.new(0, 0, 0, HEADER_HEIGHT),
ClipsDescendants = true
}, props[Roact.Children]),
HeaderContainer = Roact.createElement("Frame", {
Size = UDim2.new(1, 0, 0, HEADER_HEIGHT),
BackgroundTransparency = 1,
}, {
Padding = Roact.createElement("UIPadding", {
PaddingLeft = UDim.new(0, HEADER_SIDE_PADDING),
PaddingRight = UDim.new(0, HEADER_SIDE_PADDING),
}),
CloseButton = Roact.createElement(ImageSetButton, {
AnchorPoint = Vector2.new(0, 0.5),
Position = UDim2.new(0, 0, 0, HEADER_CONTENT_Y_CENTER),
Size = UDim2.new(0, 36, 0, 36),
Image = Assets.Images.CloseModal,
BackgroundTransparency = 1,
[Roact.Event.Activated] = props.onClosed,
}),
HeaderTextLabel = Roact.createElement(ThemedTextLabel, {
fontKey = "Header1",
themeKey = "TextEmphasis",
Size = UDim2.new(0, 250, 0, 28),
AnchorPoint = Vector2.new(0.5, 0.5),
Position = UDim2.new(0.5, 0, 0, HEADER_CONTENT_Y_CENTER),
Text = localized.titleText,
}),
HeaderDivider = Roact.createElement(Divider, {
Size = UDim2.new(1, 0, 0, 2),
AnchorPoint = Vector2.new(0.5, 1),
Position = UDim2.new(0.5, 0, 1, 0),
}),
-- Yale wanted to keep this around, unsure if we're going to use it or not
-- CloseButtonLabel = Roact.createElement(KeyLabel, {
-- Position = UDim2.new(0, 36, 0, 0),
-- input = Enum.KeyCode.Escape,
-- }),
}),
RadialGlowBig = Roact.createElement(ImageSetLabel, {
BackgroundTransparency = 1,
Image = Assets.Images.RadialGlow,
AnchorPoint = Vector2.new(0.5, 0.5),
Position = UDim2.new(0.5, 0, 0.5, 0),
-- Renders as a square based off of screen width
SizeConstraint = Enum.SizeConstraint.RelativeXX,
Size = UDim2.new(1, 0, 1, 0),
}),
RadialGlowSquashed = Roact.createElement(ImageSetLabel, {
BackgroundTransparency = 1,
Image = Assets.Images.RadialGlow,
AnchorPoint = Vector2.new(0.5, 0.5),
Position = UDim2.new(0.5, 0, 1, 0),
-- Renders with height always at 60% of width
SizeConstraint = Enum.SizeConstraint.RelativeXX,
Size = UDim2.new(1, 0, 0.6, 0),
})
})
end)
end)
end
return RoactRodux.UNSTABLE_connect2(
nil,
function(dispatch)
return {
onClosed = function()
dispatch(SetCurrentPage(Constants.defaultPageKey))
end
}
end
)(ControlLayoutContainer)
@@ -0,0 +1,45 @@
return function()
local CorePackages = game:GetService("CorePackages")
local InGameMenuDependencies = require(CorePackages.InGameMenuDependencies)
local Roact = InGameMenuDependencies.Roact
local Rodux = InGameMenuDependencies.Rodux
local RoactRodux = InGameMenuDependencies.RoactRodux
local UIBlox = InGameMenuDependencies.UIBlox
local InGameMenu = script.Parent.Parent.Parent
local Localization = require(InGameMenu.Localization.Localization)
local LocalizationProvider = require(InGameMenu.Localization.LocalizationProvider)
local reducer = require(InGameMenu.reducer)
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 ControlLayoutContainer = require(script.Parent.ControlLayoutContainer)
it("should create and destroy without errors", function()
local element = Roact.createElement(RoactRodux.StoreProvider, {
store = Rodux.Store.new(reducer)
}, {
ThemeProvider = Roact.createElement(UIBlox.Core.Style.Provider, {
style = appStyle,
}, {
LocalizationProvider = Roact.createElement(LocalizationProvider, {
localization = Localization.new("en-us"),
}, {
ControlEntryHeader = Roact.createElement(ControlLayoutContainer, {
titleText = "CoreScripts.InGameMenu.Controls.GamepadTitle",
}),
}),
}),
})
local instance = Roact.mount(element)
Roact.unmount(instance)
end)
end
@@ -0,0 +1,48 @@
local CorePackages = game:GetService("CorePackages")
local UserInputService = game:GetService("UserInputService")
local InGameMenuDependencies = require(CorePackages.InGameMenuDependencies)
local Roact = InGameMenuDependencies.Roact
local RoactRodux = InGameMenuDependencies.RoactRodux
local InGameMenu = script.Parent.Parent.Parent
local Controls = require(InGameMenu.Resources.Controls)
local SetControlLayout = require(InGameMenu.Actions.SetControlLayout)
local ControlLayoutSetter = Roact.PureComponent:extend("ControlLayoutSetter")
function ControlLayoutSetter:render()
return nil
end
function ControlLayoutSetter:didMount()
self.inputChangedConnection = UserInputService.LastInputTypeChanged:Connect(function(lastInputType)
local controlLayout = Controls.inputToControlTypeMap[lastInputType]
if controlLayout then
self.props.dispatchSetControlLayout(controlLayout)
end
end)
self.props.dispatchSetControlLayout(Controls.ControlLayouts.TOUCH)
end
function ControlLayoutSetter:willUnmount()
if self.inputChangedConnection then
self.inputChangedConnection:Disconnect()
end
end
return RoactRodux.UNSTABLE_connect2(
nil,
function(dispatch)
return {
dispatchSetControlLayout = function(controlLayout)
dispatch(SetControlLayout(controlLayout))
end
}
end
)(ControlLayoutSetter)
@@ -0,0 +1,102 @@
local UserInputService = game:GetService("UserInputService")
local CorePackages = game:GetService("CorePackages")
local InGameMenuDependencies = require(CorePackages.InGameMenuDependencies)
local Roact = InGameMenuDependencies.Roact
local UIBlox = InGameMenuDependencies.UIBlox
local InGameMenu = script.Parent.Parent.Parent.Parent
local ControlLayoutContainer = require(script.Parent.Parent.ControlLayoutContainer)
local ThemedTextLabel = require(InGameMenu.Components.ThemedTextLabel)
local Assets = require(InGameMenu.Resources.Assets)
local Controls = require(InGameMenu.Resources.Controls)
local withLocalization = require(InGameMenu.Localization.withLocalization)
local ImageSetLabel = UIBlox.Core.ImageSet.Label
local GAMEPAD_IMAGE_HEIGHT = 465
local GAMEPAD_IMAGE_WIDTH = 473
local GAMEPAD_WIDTH = 440
local GAMEPAD_ICONS_WIDTH = GAMEPAD_IMAGE_WIDTH - GAMEPAD_WIDTH
-- Height and padding for gamepad button labels
local TEXT_HEIGHT = 22
local LIST_PADDING = 21
-- Offsets for labels
local TEXT_Y_OFFSET = 62
local TEXT_X_PADDING = 26
-- Text for camera controls needs be offset based on the other text on the right
-- side of the controller. We mutliply the number of control items by the total
-- size (height + padding) of each control label element.
local RIGHT_LABELS_HEIGHT = (#Controls.gamepadLabels.right * (TEXT_HEIGHT + LIST_PADDING))
local CONTROL_CAMERA_TEXT_Y_OFFSET = RIGHT_LABELS_HEIGHT + TEXT_Y_OFFSET
local function verticalControlsList(props)
return withLocalization(props.controls)(function(localized)
local listChildren = {}
listChildren.ListLayout = Roact.createElement("UIListLayout", {
SortOrder = Enum.SortOrder.LayoutOrder,
Padding = UDim.new(0, LIST_PADDING),
})
for i, control in ipairs(localized) do
listChildren["controlLabel_"..i] = Roact.createElement(ThemedTextLabel, {
fontKey = "Header2",
themeKey = "TextEmphasis",
TextXAlignment = props.TextXAlignment,
Size = UDim2.new(0, 1, 0, TEXT_HEIGHT),
Text = control,
LayoutOrder = i,
})
end
return Roact.createElement("Frame", {
Position = props.Position,
Size = UDim2.new(0, 0, 1, 0),
BackgroundTransparency = 1,
}, listChildren)
end)
end
local function GamepadControls()
local platform = UserInputService:GetPlatform()
local isXbox = platform == Enum.Platform.XBox360 or platform == Enum.Platform.XBoxOne
return Roact.createElement(ControlLayoutContainer, {
titleText = "CoreScripts.InGameMenu.Controls.GamepadTitle"
}, {
GamepadImage = Roact.createElement(ImageSetLabel, {
BackgroundTransparency = 1,
Image = isXbox and Assets.Images.GamepadXbox or Assets.Images.Gamepad,
Size = UDim2.new(0, GAMEPAD_IMAGE_WIDTH, 0, GAMEPAD_IMAGE_HEIGHT),
Position = UDim2.new(0.5, GAMEPAD_ICONS_WIDTH/2, 0.5, 0),
AnchorPoint = Vector2.new(0.5, 0.5),
}, {
LeftControlsList = Roact.createElement(verticalControlsList, {
TextXAlignment = Enum.TextXAlignment.Right,
Position = UDim2.new(0, -TEXT_X_PADDING, 0, TEXT_Y_OFFSET),
controls = Controls.gamepadLabels.left
}),
RightControlsList = Roact.createElement(verticalControlsList, {
TextXAlignment = Enum.TextXAlignment.Left,
Position = UDim2.new(1, TEXT_X_PADDING-GAMEPAD_ICONS_WIDTH, 0, TEXT_Y_OFFSET),
controls = Controls.gamepadLabels.right
}),
RightIconsControlslIst = Roact.createElement(verticalControlsList, {
TextXAlignment = Enum.TextXAlignment.Left,
Position = UDim2.new(1, TEXT_X_PADDING, 0, CONTROL_CAMERA_TEXT_Y_OFFSET),
controls = Controls.gamepadLabels.camera
})
})
})
end
return GamepadControls
@@ -0,0 +1,43 @@
return function()
local CorePackages = game:GetService("CorePackages")
local InGameMenuDependencies = require(CorePackages.InGameMenuDependencies)
local Roact = InGameMenuDependencies.Roact
local Rodux = InGameMenuDependencies.Rodux
local RoactRodux = InGameMenuDependencies.RoactRodux
local UIBlox = InGameMenuDependencies.UIBlox
local InGameMenu = script.Parent.Parent.Parent.Parent
local Localization = require(InGameMenu.Localization.Localization)
local LocalizationProvider = require(InGameMenu.Localization.LocalizationProvider)
local reducer = require(InGameMenu.reducer)
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 GamepadControls = require(script.Parent.GamepadControls)
it("should create and destroy without errors", function()
local element = Roact.createElement(RoactRodux.StoreProvider, {
store = Rodux.Store.new(reducer)
}, {
ThemeProvider = Roact.createElement(UIBlox.Core.Style.Provider, {
style = appStyle,
}, {
LocalizationProvider = Roact.createElement(LocalizationProvider, {
localization = Localization.new("en-us"),
}, {
GamepadControls = Roact.createElement(GamepadControls),
}),
}),
})
local instance = Roact.mount(element)
Roact.unmount(instance)
end)
end
@@ -0,0 +1,89 @@
local CorePackages = game:GetService("CorePackages")
local InGameMenuDependencies = require(CorePackages.InGameMenuDependencies)
local Roact = InGameMenuDependencies.Roact
local InGameMenu = script.Parent.Parent.Parent.Parent
local Controls = require(InGameMenu.Resources.Controls)
local ControlLayoutContainer = require(script.Parent.Parent.ControlLayoutContainer)
local PaddedScrollFrame = require(InGameMenu.Components.PaddedScrollFrame)
local ControlEntry = require(script.Parent.Parent.ControlEntry)
local ControlEntryHeader = require(script.Parent.Parent.ControlEntryHeader)
local CONTENT_MAX_WIDTH = 900
local CONTENT_MIN_WIDTH = 150
local MIN_SIDE_PADDING = 95
local HEADER_HEIGHT = 45
local ENTRY_HEIGHT = 50
local BOTTOM_PADDING = 130
local function KeyboardControls(props)
local controlsChildren = {}
controlsChildren.ListLayout = Roact.createElement("UIListLayout", {
SortOrder = Enum.SortOrder.LayoutOrder,
})
controlsChildren.Padding = Roact.createElement("UIPadding", {
PaddingLeft = UDim.new(0, MIN_SIDE_PADDING),
PaddingRight = UDim.new(0, MIN_SIDE_PADDING)
})
controlsChildren.SizeConstraint = Roact.createElement("UISizeConstraint", {
MaxSize = Vector2.new(CONTENT_MAX_WIDTH + (MIN_SIDE_PADDING * 2), math.huge),
MinSize = Vector2.new(CONTENT_MIN_WIDTH, 0),
})
local numHeaders = 0
local numEntries = 0
local layoutOrder = 1
for _, controlsGroup in ipairs(Controls.keybinds) do
local groupTitle = controlsGroup.title
numHeaders = numHeaders + 1
controlsChildren[groupTitle.."_header"] = Roact.createElement(ControlEntryHeader, {
title = groupTitle,
LayoutOrder = layoutOrder,
})
layoutOrder = layoutOrder + 1
for controlIndex, controlAction in ipairs(controlsGroup.controls) do
numEntries = numEntries + 1
controlsChildren[groupTitle.."_"..controlIndex] = Roact.createElement(ControlEntry, {
LayoutOrder = layoutOrder,
actionDescription = controlAction.desc,
inputs = controlAction.inputs,
isList = controlAction.list,
hasDivider = controlIndex ~= #controlsGroup.controls,
})
layoutOrder = layoutOrder + 1
end
end
local canvasHeight = (HEADER_HEIGHT * numHeaders) + (ENTRY_HEIGHT * numEntries)
return Roact.createElement(ControlLayoutContainer, {
titleText = "CoreScripts.InGameMenu.Controls.KeyboardAndMouseTitle",
}, {
ScrollContent = Roact.createElement(PaddedScrollFrame, {
CanvasSize = UDim2.new(0, 0, 0, canvasHeight),
ScrollingDirection = Enum.ScrollingDirection.Y,
Size = UDim2.new(1, 0, 1, -BOTTOM_PADDING),
}, {
ListContainer = Roact.createElement("Frame", {
Size = UDim2.new(1, 0, 1, 0),
BackgroundTransparency = 1,
AnchorPoint = Vector2.new(0.5, 0),
Position = UDim2.new(0.5, 0, 0, 0)
}, controlsChildren)
})
})
end
return KeyboardControls
@@ -0,0 +1,43 @@
return function()
local CorePackages = game:GetService("CorePackages")
local InGameMenuDependencies = require(CorePackages.InGameMenuDependencies)
local Roact = InGameMenuDependencies.Roact
local Rodux = InGameMenuDependencies.Rodux
local RoactRodux = InGameMenuDependencies.RoactRodux
local UIBlox = InGameMenuDependencies.UIBlox
local InGameMenu = script.Parent.Parent.Parent.Parent
local Localization = require(InGameMenu.Localization.Localization)
local LocalizationProvider = require(InGameMenu.Localization.LocalizationProvider)
local reducer = require(InGameMenu.reducer)
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 KeyboardControls = require(script.Parent.KeyboardControls)
it("should create and destroy without errors", function()
local element = Roact.createElement(RoactRodux.StoreProvider, {
store = Rodux.Store.new(reducer)
}, {
ThemeProvider = Roact.createElement(UIBlox.Core.Style.Provider, {
style = appStyle,
}, {
LocalizationProvider = Roact.createElement(LocalizationProvider, {
localization = Localization.new("en-us"),
}, {
KeyboardControls = Roact.createElement(KeyboardControls),
}),
}),
})
local instance = Roact.mount(element)
Roact.unmount(instance)
end)
end
@@ -0,0 +1,12 @@
local CorePackages = game:GetService("CorePackages")
local InGameMenuDependencies = require(CorePackages.InGameMenuDependencies)
local Roact = InGameMenuDependencies.Roact
local KeyboardControls = require(script.Parent.KeyboardControls)
local function TouchControls(props)
return Roact.createElement(KeyboardControls, props)
end
return TouchControls
@@ -0,0 +1,43 @@
return function()
local CorePackages = game:GetService("CorePackages")
local InGameMenuDependencies = require(CorePackages.InGameMenuDependencies)
local Roact = InGameMenuDependencies.Roact
local Rodux = InGameMenuDependencies.Rodux
local RoactRodux = InGameMenuDependencies.RoactRodux
local UIBlox = InGameMenuDependencies.UIBlox
local InGameMenu = script.Parent.Parent.Parent.Parent
local Localization = require(InGameMenu.Localization.Localization)
local LocalizationProvider = require(InGameMenu.Localization.LocalizationProvider)
local reducer = require(InGameMenu.reducer)
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 TouchControls = require(script.Parent.TouchControls)
it("should create and destroy without errors", function()
local element = Roact.createElement(RoactRodux.StoreProvider, {
store = Rodux.Store.new(reducer)
}, {
ThemeProvider = Roact.createElement(UIBlox.Core.Style.Provider, {
style = appStyle,
}, {
LocalizationProvider = Roact.createElement(LocalizationProvider, {
localization = Localization.new("en-us"),
}, {
TouchControls = Roact.createElement(TouchControls),
}),
}),
})
local instance = Roact.mount(element)
Roact.unmount(instance)
end)
end
@@ -0,0 +1,37 @@
local CorePackages = game:GetService("CorePackages")
local InGameMenuDependencies = require(CorePackages.InGameMenuDependencies)
local Roact = InGameMenuDependencies.Roact
local RoactRodux = InGameMenuDependencies.RoactRodux
local InGameMenu = script.Parent.Parent
local Controls = require(InGameMenu.Resources.Controls)
local KeyboardControls = require(script.ControlLayouts.KeyboardControls)
local GamepadControls = require(script.ControlLayouts.GamepadControls)
-- local TouchControls = require(InGameMenu.Components.ControlLayouts.TouchControls)
local ControlsPage = Roact.PureComponent:extend("ControlsPage")
function ControlsPage:render()
local controlLayout = self.props.controlLayout
if controlLayout == Controls.ControlLayouts.KEYBOARD then
return Roact.createElement(KeyboardControls)
elseif controlLayout == Controls.ControlLayouts.GAMEPAD then
return Roact.createElement(GamepadControls)
-- elseif controlLayout == Controls.ControlLayouts.TOUCH then
-- return Roact.createElement(TouchControls)
else
return nil
end
end
return RoactRodux.UNSTABLE_connect2(function(state)
local controlLayout = state.controlLayout
return {
controlLayout = controlLayout
}
end)(ControlsPage)
@@ -0,0 +1,40 @@
local CorePackages = game:GetService("CorePackages")
local InGameMenuDependencies = require(CorePackages.InGameMenuDependencies)
local Roact = InGameMenuDependencies.Roact
local UIBlox = InGameMenuDependencies.UIBlox
local Cryo = InGameMenuDependencies.Cryo
local t = InGameMenuDependencies.t
local withStyle = UIBlox.Core.Style.withStyle
local InGameMenu = script.Parent.Parent
local GlobalConfig = require(InGameMenu.GlobalConfig)
local validateProps = t.strictInterface({
Size = t.optional(t.UDim2),
Position = t.optional(t.UDim2),
AnchorPoint = t.optional(t.Vector2),
LayoutOrder = t.optional(t.integer),
Visible = t.optional(t.boolean),
})
local function Divider(props)
if GlobalConfig.propValidation then
assert(validateProps(props))
end
return withStyle(function(style)
-- Divider relies on being right-aligned in order to be left-indented.
return Roact.createElement("Frame", Cryo.Dictionary.join({
Size = UDim2.new(1, 0, 0, 1),
}, props, {
BorderSizePixel = 0,
BackgroundColor3 = style.Theme.Divider.Color,
BackgroundTransparency = style.Theme.Divider.Transparency,
}))
end)
end
return Divider
@@ -0,0 +1,307 @@
local CorePackages = game:GetService("CorePackages")
local InGameMenuDependencies = require(CorePackages.InGameMenuDependencies)
local Roact = InGameMenuDependencies.Roact
local UIBlox = InGameMenuDependencies.UIBlox
local t = InGameMenuDependencies.t
local withStyle = UIBlox.Core.Style.withStyle
local InGameMenu = script.Parent.Parent
local Assets = require(InGameMenu.Resources.Assets)
local divideTransparency = require(InGameMenu.Utility.divideTransparency)
local ThemedButton = require(script.Parent.ThemedButton)
local ThemedTextLabel = require(script.Parent.ThemedTextLabel)
local Divider = require(script.Parent.Divider)
local BarOnTopScrollingFrame = require(script.Parent.BarOnTopScrollingFrame)
local withLocalization = require(InGameMenu.Localization.withLocalization)
local ImageSetLabel = UIBlox.Core.ImageSet.Label
local DropDownSelection = Roact.PureComponent:extend("DropDownSelection")
local DROP_DOWN_BORDER_SIZE = 4
local MAX_ITEMS_DISPLAYED = 7.5
local ITEM_HEIGHT = 44
DropDownSelection.validateProps = t.intersection(t.strictInterface({
Size = t.UDim2,
Position = t.optional(t.UDim2),
LayoutOrder = t.optional(t.integer),
placeHolderText = t.optional(t.string),
selectedIndex = t.integer, --0 if placeholder is selected
selections = t.array(t.string),
localize = t.boolean,
enabled = t.boolean,
selectionChanged = t.callback,
}), function(props)
if props.selectedIndex < 0 then
return false, "selectedIndex must be greater than 0"
end
if props.selectedIndex > #props.selections then
return false, "selectedIndex must not be greater than the number of selections"
end
return true
end)
function DropDownSelection:init()
self.rootRef = Roact.createRef()
self.state = {
isOpen = false,
dropDownItemHeight = 10,
absolutePositionX = 0,
absolutePositionY = 0,
screenSizeX = 0,
screenSizeY = 0,
}
self.ancestryConnection = nil
self.sizeConnection = nil
end
function DropDownSelection:renderDropDownList(style, localized)
local dropDownList = {}
dropDownList.Layout = Roact.createElement("UIListLayout", {
FillDirection = Enum.FillDirection.Vertical,
HorizontalAlignment = Enum.HorizontalAlignment.Center,
SortOrder = Enum.SortOrder.LayoutOrder,
})
for index, dropDownOption in ipairs(self.props.selections) do
local buttonText = dropDownOption
if self.props.localize then
buttonText = localized[index]
end
dropDownList["button" .. index] = Roact.createElement(ThemedButton, {
Size = UDim2.new(1, 0, 0, self.state.dropDownItemHeight - 1),
LayoutOrder = index * 2,
onActivated = function()
self:setState({
isOpen = false,
})
self.props.selectionChanged(index)
end,
normalThemeKey = "BackgroundUIDefault",
hoverThemeKey = "BackgroundOnHover",
imageProps = Assets.Images.WhiteSquare,
renderChildren = function(transparency, isHovered, isPressed)
local iconColor = isHovered and style.Theme.IconOnHover or style.Theme.IconDefault
local iconColor3 = iconColor.Color
local iconTransparency = divideTransparency(iconColor.Transparency, isPressed and 2 or 1)
return {
SelectedIcon = Roact.createElement(ImageSetLabel, {
BackgroundTransparency = 1,
AnchorPoint = Vector2.new(1, 0.5),
Size = UDim2.new(0, 15, 0, 12),
Position = UDim2.new(1, -25, 0.5, 0),
Image = Assets.Images.SelectedCheck,
ImageColor3 = iconColor3,
ImageTransparency = iconTransparency,
Visible = self.props.selectedIndex == index
}),
TextLabel = Roact.createElement(ThemedTextLabel, {
fontKey = "Header2",
themeKey = "TextEmphasis",
Position = UDim2.new(0, 25, 0, 0),
Size = UDim2.new(1, -25, 1, 0),
Text = buttonText,
TextXAlignment = Enum.TextXAlignment.Left,
}),
}
end,
})
if index < #self.props.selections then
dropDownList["divider" .. index] = Roact.createElement(Divider, {
LayoutOrder = index * 2 + 1
})
end
end
return dropDownList
end
function DropDownSelection:render()
return withStyle(function(style)
return withLocalization(
self.props.localize and self.props.selections or {}
)(function(localized)
local dropDownOpenText = self.props.placeHolderText
if self.props.selectedIndex > 0 then
if self.props.localize then
dropDownOpenText = localized[self.props.selectedIndex]
else
dropDownOpenText = self.props.selections[self.props.selectedIndex]
end
end
local shadowSize = Assets.Images.Shadow.Size
local itemSize = math.min(#self.props.selections, MAX_ITEMS_DISPLAYED) * ITEM_HEIGHT
local dropDownList = self:renderDropDownList(style, localized)
return Roact.createElement("Frame", {
Size = self.props.Size,
Position = self.props.Position,
LayoutOrder = self.props.LayoutOrder,
BackgroundTransparency = 1,
[Roact.Ref] = self.rootRef,
[Roact.Change.AbsoluteSize] = function(rbx)
self:setState({
dropDownItemHeight = rbx.AbsoluteSize.Y,
})
end,
[Roact.Change.AbsolutePosition] = function(rbx)
self:setState({
absolutePositionX = rbx.AbsolutePosition.X,
absolutePositionY = rbx.AbsolutePosition.Y,
})
end,
}, {
OpenDropDownButton = Roact.createElement(ThemedButton, {
Size = UDim2.new(1, 0, 1, 0),
Visible = not self.state.isOpen,
enabled = self.props.enabled,
onActivated = function()
self:setState({
isOpen = true,
})
end,
normalThemeKey = "SecondaryDefault",
hoverThemeKey = "SecondaryOnHover",
imageProps = Assets.Images.RoundedRectBorder,
renderChildren = function(transparency, isHovered, isPressed)
local iconColor = isHovered and style.Theme.IconOnHover or style.Theme.IconDefault
local iconColor3 = iconColor.Color
local iconTransparency = divideTransparency(iconColor.Transparency, isPressed and 2 or 1)
return {
ButtonIcon = Roact.createElement(ImageSetLabel, {
BackgroundTransparency = 1,
Size = UDim2.new(0, 36, 0, 36),
Position = UDim2.new(1, -16, 0.5, 0),
AnchorPoint = Vector2.new(1, 0.5),
Image = Assets.Images.ExpandArrow,
ImageColor3 = iconColor3,
ImageTransparency = iconTransparency,
}),
ButtonText = Roact.createElement(ThemedTextLabel, {
Text = dropDownOpenText,
themeKey = "SecondaryContent",
fontKey = "Header2",
Position = UDim2.new(0, 12, 0, 0),
Size = UDim2.new(1, -12, 1, 0),
TextTransparency = transparency,
TextXAlignment = Enum.TextXAlignment.Left,
}),
}
end,
}),
CloseDropDownArea = Roact.createElement("TextButton", {
BackgroundTransparency = 1,
Text = "",
Position = UDim2.new(0, -self.state.absolutePositionX, 0, -self.state.absolutePositionY),
Size = UDim2.new(0, self.state.screenSizeX, 0, self.state.screenSizeY),
Visible = self.state.isOpen,
[Roact.Event.Activated] = function()
self:setState({
isOpen = false,
})
end,
ZIndex = 2,
}),
DropDownShadow = Roact.createElement(ImageSetLabel, {
BackgroundTransparency = 1,
Image = Assets.Images.Shadow.Image,
Position = UDim2.new(0, -shadowSize, 0, -shadowSize),
Size = UDim2.new(1, shadowSize * 2, 0, shadowSize * 2 + DROP_DOWN_BORDER_SIZE * 2 + itemSize),
ScaleType = Assets.Images.Shadow.ScaleType,
SliceCenter = Assets.Images.Shadow.SliceCenter,
Visible = self.state.isOpen,
ZIndex = 3,
}, {
DropDownBackground = Roact.createElement(ImageSetLabel, {
BackgroundTransparency = 1,
Image = Assets.Images.RoundedRect.Image,
ImageColor3 = style.Theme.BackgroundUIDefault.Color,
ImageTransparency = style.Theme.BackgroundUIDefault.Transparency,
ScaleType = Assets.Images.RoundedRect.ScaleType,
Size = UDim2.new(1, -(shadowSize * 2), 1, -(shadowSize * 2)),
Position = UDim2.new(0.5, 0, 0.5, 0),
AnchorPoint = Vector2.new(0.5, 0.5),
SliceCenter = Assets.Images.RoundedRect.SliceCenter,
SliceScale = 0.5,
}, {
DropDownList = Roact.createElement(BarOnTopScrollingFrame, {
Position = UDim2.new(0, 0, 0, DROP_DOWN_BORDER_SIZE),
Size = UDim2.new(1, 0, 1, -DROP_DOWN_BORDER_SIZE * 2),
CanvasSize = UDim2.new(1, 0, 0, #self.props.selections * self.state.dropDownItemHeight),
scrollBarOffset = 4,
}, dropDownList),
}),
}),
})
end)
end)
end
function DropDownSelection:watchLayerCollector(layerCollector)
self:setState({
screenSizeX = layerCollector.AbsoluteSize.X,
screenSizeY = layerCollector.AbsoluteSize.Y,
})
self.sizeConnection = layerCollector:GetPropertyChangedSignal("AbsoluteSize"):Connect(function()
self:setState({
screenSizeX = layerCollector.AbsoluteSize.X,
screenSizeY = layerCollector.AbsoluteSize.Y,
})
end)
end
function DropDownSelection:didMount()
local root = self.rootRef.current
local layerCollector = root:FindFirstAncestorWhichIsA("LayerCollector")
if layerCollector == nil then
self.ancestryConnection = root.AncestryChanged:Connect(function()
layerCollector = root:FindFirstAncestorWhichIsA("LayerCollector")
if layerCollector ~= nil then
self.ancestryConnection:Disconnect()
self.ancestryConnection = nil
self:watchLayerCollector(layerCollector)
end
end)
else
self:watchLayerCollector(layerCollector)
end
end
function DropDownSelection:willUnmount()
if self.ancestryConnection then
self.ancestryConnection:Disconnect()
end
if self.sizeConnection then
self.sizeConnection:Disconnect()
end
end
return DropDownSelection
@@ -0,0 +1,97 @@
return function()
local CorePackages = game:GetService("CorePackages")
local InGameMenuDependencies = require(CorePackages.InGameMenuDependencies)
local Cryo = InGameMenuDependencies.Cryo
local Roact = InGameMenuDependencies.Roact
local UIBlox = InGameMenuDependencies.UIBlox
local InGameMenu = script.Parent.Parent
local Localization = require(InGameMenu.Localization.Localization)
local LocalizationProvider = require(InGameMenu.Localization.LocalizationProvider)
local AppDarkTheme = require(CorePackages.AppTempCommon.LuaApp.Style.Themes.DarkTheme)
local AppFont = require(CorePackages.AppTempCommon.LuaApp.Style.Fonts.Gotham)
local DropDownSelection = require(script.Parent.DropDownSelection)
local appStyle = {
Theme = AppDarkTheme,
Font = AppFont,
}
local dummyDropDownProps = {
Size = UDim2.new(0, 200, 0, 44),
Position = UDim2.new(0.5, 0, 0.5, 0),
LayoutOrder = 0,
placeHolderText = "Placeholder",
selectedIndex = 0,
selections = {
"Option1",
"Option2",
"Option3",
},
localize = false,
enabled = true,
selectionChanged = function()
print("selection changed")
end,
}
it("should create and destroy without errors", function()
local element = Roact.createElement(UIBlox.Core.Style.Provider, {
style = appStyle,
}, {
LocalizationProvider = Roact.createElement(LocalizationProvider, {
localization = Localization.new("en-us"),
}, {
DropDownSelection = Roact.createElement(DropDownSelection, dummyDropDownProps),
}),
})
local instance = Roact.mount(element)
Roact.unmount(instance)
end)
it("should create and destroy without errors with a selected item", function()
local element = Roact.createElement(UIBlox.Core.Style.Provider, {
style = appStyle,
}, {
LocalizationProvider = Roact.createElement(LocalizationProvider, {
localization = Localization.new("en-us"),
}, {
DropDownSelection = Roact.createElement(DropDownSelection, Cryo.Dictionary.join(dummyDropDownProps, {
selectedIndex = 1,
})),
}),
})
local instance = Roact.mount(element)
Roact.unmount(instance)
end)
it("should throw when passed a selectedIndex out of bounds when propvalidation is turned on", function()
Roact.setGlobalConfig({
propValidation = true,
})
local element = Roact.createElement(UIBlox.Core.Style.Provider, {
style = appStyle,
}, {
LocalizationProvider = Roact.createElement(LocalizationProvider, {
localization = Localization.new("en-us"),
}, {
DropDownSelection = Roact.createElement(DropDownSelection, Cryo.Dictionary.join(dummyDropDownProps, {
selectedIndex = 100,
})),
}),
})
expect(function()
local instance = Roact.mount(element)
Roact.unmount(instance)
end).to.throw()
end)
end
@@ -0,0 +1,126 @@
local GuiService = game:GetService("GuiService")
local CorePackages = game:GetService("CorePackages")
local CoreGui = game:GetService("CoreGui")
local RbxAnalyticsService = game:GetService("RbxAnalyticsService")
local InGameMenuDependencies = require(CorePackages.InGameMenuDependencies)
local Roact = InGameMenuDependencies.Roact
local RoactRodux = InGameMenuDependencies.RoactRodux
local t = InGameMenuDependencies.t
local UIBlox = InGameMenuDependencies.UIBlox
local Images = UIBlox.App.ImageSet.Images
local InGameMenu = script.Parent.Parent
local withLocalization = require(InGameMenu.Localization.withLocalization)
local Constants = require(InGameMenu.Resources.Constants)
local CloseNativeClosePrompt = require(InGameMenu.Actions.CloseNativeClosePrompt)
local SetMenuIconTooltipOpen = require(InGameMenu.Actions.SetMenuIconTooltipOpen)
local OpenMenu = require(InGameMenu.Thunks.OpenMenu)
local EducationalPopupDialog = require(script.Parent.EducationalPopupDialog)
local SendAnalytics = require(InGameMenu.Utility.SendAnalytics)
local RobloxGui = CoreGui:WaitForChild("RobloxGui")
local GetFFlagInstrumentMenuOpenMethods = require(RobloxGui.Modules.Flags.GetFFlagInstrumentMenuOpenMethods)
local GetFFlagInGameMenuIconTooltip = require(RobloxGui.Modules.Flags.GetFFlagInGameMenuIconTooltip)
local NotificationType = GuiService:GetNotificationTypeList()
local EducationalAnalytics = {
EventContext = "educational_popup",
ConfirmName = "educational_confirmed",
CancelName = "educational_close_app",
DismissName = "educational_dismiss_prompt",
}
local EducationalPopup = Roact.PureComponent:extend("EducationalPopup")
EducationalPopup.validateProps = t.strictInterface({
screenSize = t.Vector2,
onDismiss = t.callback,
onConfirm = t.callback,
isClosingApp = t.boolean,
})
function EducationalPopup:init()
self.onCancel = function()
SendAnalytics(EducationalAnalytics.EventContext, EducationalAnalytics.CancelName, {})
GuiService:BroadcastNotification("", NotificationType.NATIVE_EXIT)
end
end
function EducationalPopup:render()
return withLocalization({
bodyTextLn1 = "CoreScripts.InGameMenu.EducationalPopup.BodyTextOpenSystemMenu",
bodyTextLn2 = "CoreScripts.InGameMenu.EducationalPopup.BodyTextClickHome",
bodyTextLn3 = "CoreScripts.InGameMenu.EducationalPopup.BodyTextPlayMore",
titleText = "CoreScripts.InGameMenu.EducationalPopup.Title",
confirmText = "CoreScripts.InGameMenu.Ok",
cancelText = "CoreScripts.InGameMenu.EducationalPopup.LeaveRoblox",
})(function(localized)
return Roact.createElement(EducationalPopupDialog, {
bodyContents = {
{
icon = Images["icons/logo/block"],
text = localized.bodyTextLn1,
layoutOrder = 1,
isSystemMenuIcon = true,
},
{
icon = Images["icons/menu/home_off"],
text = localized.bodyTextLn2,
layoutOrder = 2,
},
{
icon = Images["icons/menu/games_off"],
text = localized.bodyTextLn3,
layoutOrder = 3,
},
},
cancelText = localized.cancelText,
confirmText = localized.confirmText,
titleText = localized.titleText,
titleBackgroundImageProps = {
image = "rbxasset://textures/ui/LuaApp/graphic/EducationalBackground.png",
imageHeight = 261,
},
screenSize = self.props.screenSize,
onDismiss = self.props.onDismiss,
onCancel = self.onCancel,
onConfirm = self.props.onConfirm,
blurBackground = true,
visible = self.props.isClosingApp,
})
end)
end
return RoactRodux.UNSTABLE_connect2(function(state, props)
return {
isClosingApp = state.nativeClosePrompt.closingApp,
screenSize = state.screenSize,
}
end, function(dispatch)
return {
onDismiss = function()
dispatch(CloseNativeClosePrompt())
RbxAnalyticsService:ReportCounter("EducationalPopup_Dismiss", 1)
SendAnalytics(EducationalAnalytics.EventContext, EducationalAnalytics.DismissName, {})
end,
onConfirm = function()
dispatch(CloseNativeClosePrompt())
if GetFFlagInGameMenuIconTooltip() then
dispatch(SetMenuIconTooltipOpen(true))
elseif GetFFlagInstrumentMenuOpenMethods() then
dispatch(OpenMenu(Constants.AnalyticsMenuOpenTypes.EducationalMenuTriggered))
else
dispatch(OpenMenu)
end
RbxAnalyticsService:ReportCounter("EducationalPopup_Confirm", 1)
SendAnalytics(EducationalAnalytics.EventContext, EducationalAnalytics.ConfirmName, {})
end,
}
end)(EducationalPopup)
@@ -0,0 +1,47 @@
return function()
local CorePackages = game:GetService("CorePackages")
local InGameMenuDependencies = require(CorePackages.InGameMenuDependencies)
local Roact = InGameMenuDependencies.Roact
local RoactRodux = InGameMenuDependencies.RoactRodux
local Rodux = InGameMenuDependencies.Rodux
local UIBlox = InGameMenuDependencies.UIBlox
local InGameMenu = script.Parent.Parent
local reducer = require(InGameMenu.reducer)
local Localization = require(InGameMenu.Localization.Localization)
local LocalizationProvider = require(InGameMenu.Localization.LocalizationProvider)
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 EducationalPopup = require(script.Parent.EducationalPopup)
it("should create and destroy without errors", function()
local element = Roact.createElement(RoactRodux.StoreProvider, {
store = Rodux.Store.new(reducer)
}, {
ThemeProvider = Roact.createElement(UIBlox.Core.Style.Provider, {
style = appStyle,
}, {
LocalizationProvider = Roact.createElement(LocalizationProvider, {
localization = Localization.new("en-us"),
}, {
EducationalPopup = Roact.createElement(EducationalPopup, {
screenSize = Vector2.new(1920, 1080),
onDismiss = function()end,
onConfirm = function()end,
isClosingApp = false,
}),
}),
}),
})
local instance = Roact.mount(element)
Roact.unmount(instance)
end)
end
@@ -0,0 +1,97 @@
local CoreGui = game:GetService("CoreGui")
local RunService = game:GetService("RunService")
local CorePackages = game:GetService("CorePackages")
local InGameMenuDependencies = require(CorePackages.InGameMenuDependencies)
local Roact = InGameMenuDependencies.Roact
local t = InGameMenuDependencies.t
local UIBlox = InGameMenuDependencies.UIBlox
local EducationalModal = UIBlox.App.Dialog.Modal.EducationalModal
local withStyle = UIBlox.Core.Style.withStyle
local EducationalPopupDialog = Roact.PureComponent:extend("EducationalPopupDialog")
EducationalPopupDialog.validateProps = t.strictInterface({
bodyContents = t.array(t.strictInterface({
icon = t.union(t.string, t.table),
text = t.string,
layoutOrder = t.integer,
isSystemMenuIcon = t.optional(t.boolean),
})),
cancelText = t.string,
confirmText = t.string,
titleText = t.string,
titleBackgroundImageProps = t.strictInterface({
image = t.string,
imageHeight = t.number,
}),
screenSize = t.Vector2,
onDismiss = t.callback,
onCancel = t.callback,
onConfirm = t.callback,
blurBackground = t.boolean,
visible = t.boolean,
})
EducationalPopupDialog.defaultProps = {
blurBackground = false,
}
function EducationalPopupDialog:render()
return withStyle(function(style)
return Roact.createElement(Roact.Portal, {
target = CoreGui,
}, {
InGameMenuEducationalPopupDialog = Roact.createElement("ScreenGui", {
IgnoreGuiInset = true,
OnTopOfCoreBlur = self.props.blurBackground,
Enabled = self.props.visible,
ZIndexBehavior = Enum.ZIndexBehavior.Sibling,
}, {
Overlay = Roact.createElement("TextButton", {
AutoButtonColor = false,
BackgroundColor3 = style.Theme.Overlay.Color,
BackgroundTransparency = style.Theme.Overlay.Transparency,
BorderSizePixel = 0,
Size = UDim2.fromScale(1, 1),
Text = "",
}),
Popup = Roact.createElement(EducationalModal, {
bodyContents = self.props.bodyContents,
cancelText = self.props.cancelText,
confirmText = self.props.confirmText,
titleText = self.props.titleText,
titleBackgroundImageProps = self.props.titleBackgroundImageProps,
screenSize = self.props.screenSize,
onDismiss = self.props.onDismiss,
onCancel = self.props.onCancel,
onConfirm = self.props.onConfirm,
})
})
})
end)
end
function EducationalPopupDialog:updateBlur()
local shouldBlur = self.props.blurBackground and self.props.visible
RunService:SetRobloxGuiFocused(shouldBlur)
end
function EducationalPopupDialog:didMount()
self:updateBlur()
end
function EducationalPopupDialog:didUpdate()
self:updateBlur()
end
function EducationalPopupDialog:willUnmount()
if self.props.blurBackground then
RunService:SetRobloxGuiFocused(false)
end
end
return EducationalPopupDialog
@@ -0,0 +1,72 @@
return function()
local CorePackages = game:GetService("CorePackages")
local InGameMenuDependencies = require(CorePackages.InGameMenuDependencies)
local Roact = InGameMenuDependencies.Roact
local RoactRodux = InGameMenuDependencies.RoactRodux
local Rodux = InGameMenuDependencies.Rodux
local UIBlox = InGameMenuDependencies.UIBlox
local Images = UIBlox.App.ImageSet.Images
local InGameMenu = script.Parent.Parent
local reducer = require(InGameMenu.reducer)
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 EducationalPopupDialog = require(script.Parent.EducationalPopupDialog)
local mockProps = {
bodyContents = {
{
icon = Images["icons/logo/block"],
text = "Body 1",
layoutOrder = 1
},
{
icon = Images["icons/menu/home_on"],
text = "Body 2",
layoutOrder = 2
},
{
icon = Images["icons/menu/games_on"],
text = "Body 3",
layoutOrder = 3
},
},
cancelText = "Cancel",
confirmText = "Confirm",
titleText = "Title",
titleBackgroundImageProps = {
image = "rbxassetid://2610133241",
imageHeight = 200,
},
screenSize = Vector2.new(1920, 1080),
onDismiss = function()end,
onCancel = function()end,
onConfirm = function()end,
blurBackground = true,
visible = true,
}
it("should create and destroy without errors", function()
local element = Roact.createElement(RoactRodux.StoreProvider, {
store = Rodux.Store.new(reducer)
}, {
ThemeProvider = Roact.createElement(UIBlox.Core.Style.Provider, {
style = appStyle,
}, {
EducationalPopupDialog = Roact.createElement(EducationalPopupDialog, mockProps),
}),
})
local instance = Roact.mount(element)
Roact.unmount(instance)
end)
end
@@ -0,0 +1,78 @@
--[[
A square game icon
]]
local CorePackages = game:GetService("CorePackages")
local InGameMenuDependencies = require(CorePackages.InGameMenuDependencies)
local Roact = InGameMenuDependencies.Roact
local UIBlox = InGameMenuDependencies.UIBlox
local t = InGameMenuDependencies.t
local InGameMenu = script.Parent.Parent
local withStyle = UIBlox.Core.Style.withStyle
local Images = UIBlox.App.ImageSet.Images
local AssetImage = require(InGameMenu.Components.AssetImage)
local ImageSetLabel = UIBlox.Core.ImageSet.Label
local getGameIconRequestSize = require(InGameMenu.Components.getGameIconRequestSize)
local MAX_PLACEHOLDER_SIZE = 44
local GameIcon = Roact.PureComponent:extend("GameIcon")
GameIcon.validateProps = t.strictInterface({
gameId = t.number,
iconSize = t.numberMin(0),
layoutOrder = t.optional(t.number),
})
function GameIcon:render()
local iconSize = self.props.iconSize
local requestIconSize = getGameIconRequestSize(iconSize)
local gameId = self.props.gameId
local iconUrl = ("rbxthumb://type=GameIcon&id=%d&w=%d&h=%d"):format(
gameId,
requestIconSize,
requestIconSize
)
--Special case for when the gameId is not available. For example developing in studio with a un-published game.
local isPlaceholder = gameId == 0
if isPlaceholder then
local placeholderIconSize = MAX_PLACEHOLDER_SIZE
if iconSize <= MAX_PLACEHOLDER_SIZE then
placeholderIconSize = iconSize
end
return withStyle(function(style)
return Roact.createElement(AssetImage.Label, {
imageKey = "RoundedRect",
Size = UDim2.fromOffset(iconSize, iconSize),
ImageColor3 = style.Theme.PlaceHolder.Color,
ImageTransparency = style.Theme.PlaceHolder.Transparency,
LayoutOrder = self.props.layoutOrder,
}, {
PlaceholderIcon = Roact.createElement(ImageSetLabel, {
BackgroundTransparency = 1,
Size = UDim2.fromOffset(placeholderIconSize, placeholderIconSize),
Position = UDim2.fromScale(0.5, 0.5),
Image = Images["icons/imageUnavailable"],
ImageColor3 = style.Theme.UIDefault.Color,
ImageTransparency = style.Theme.UIDefault.Transparency,
AnchorPoint = Vector2.new(0.5, 0.5),
}),
})
end)
else
return Roact.createElement("ImageLabel", {
Image = iconUrl,
Size = UDim2.fromOffset(iconSize, iconSize),
BackgroundTransparency = 1,
LayoutOrder = self.props.layoutOrder,
})
end
end
return GameIcon
@@ -0,0 +1,70 @@
return function()
local CorePackages = game:GetService("CorePackages")
local InGameMenuDependencies = require(CorePackages.InGameMenuDependencies)
local Roact = InGameMenuDependencies.Roact
local RoactRodux = InGameMenuDependencies.RoactRodux
local Rodux = InGameMenuDependencies.Rodux
local UIBlox = InGameMenuDependencies.UIBlox
local InGameMenu = script.Parent.Parent
local reducer = require(InGameMenu.reducer)
local Localization = require(InGameMenu.Localization.Localization)
local LocalizationProvider = require(InGameMenu.Localization.LocalizationProvider)
local AppDarkTheme = require(CorePackages.AppTempCommon.LuaApp.Style.Themes.DarkTheme)
local AppFont = require(CorePackages.AppTempCommon.LuaApp.Style.Fonts.Gotham)
local GameIcon = require(script.Parent.GameIcon)
local appStyle = {
Theme = AppDarkTheme,
Font = AppFont,
}
it("should create and destroy without errors", function()
local element = Roact.createElement(RoactRodux.StoreProvider, {
store = Rodux.Store.new(reducer)
}, {
ThemeProvider = Roact.createElement(UIBlox.Core.Style.Provider, {
style = appStyle,
}, {
LocalizationProvider = Roact.createElement(LocalizationProvider, {
localization = Localization.new("en-us"),
}, {
GameIcon = Roact.createElement(GameIcon, {
gameId = game.gameId,
iconSize = 100,
layoutOrder = 1,
}),
}),
}),
})
local instance = Roact.mount(element)
Roact.unmount(instance)
end)
it("should create and destroy placeholder game icon without errors", function()
local element = Roact.createElement(RoactRodux.StoreProvider, {
store = Rodux.Store.new(reducer)
}, {
ThemeProvider = Roact.createElement(UIBlox.Core.Style.Provider, {
style = appStyle,
}, {
LocalizationProvider = Roact.createElement(LocalizationProvider, {
localization = Localization.new("en-us"),
}, {
GameIcon = Roact.createElement(GameIcon, {
gameId = 0,
iconSize = 100,
layoutOrder = 1,
}),
}),
}),
})
local instance = Roact.mount(element)
Roact.unmount(instance)
end)
end
@@ -0,0 +1,51 @@
local CorePackages = game:GetService("CorePackages")
local InGameMenuDependencies = require(CorePackages.InGameMenuDependencies)
local Roact = InGameMenuDependencies.Roact
local RoactRodux = InGameMenuDependencies.RoactRodux
local InGameMenu = script.Parent.Parent
local ThemedTextLabel = require(InGameMenu.Components.ThemedTextLabel)
local GameIcon = require(InGameMenu.Components.GameIcon)
local GAME_ICON_RENDER_SIZE = 100
local EXTERIOR_PADDING = 24
local TEXT_ICON_PADDING = 12
local function GameIconHeader(props)
return Roact.createElement("ImageLabel", {
BackgroundTransparency = 1,
Size = UDim2.new(1, 0, 0, GAME_ICON_RENDER_SIZE + EXTERIOR_PADDING * 2),
}, {
Padding = Roact.createElement("UIPadding", {
PaddingBottom = UDim.new(0, EXTERIOR_PADDING),
PaddingLeft = UDim.new(0, EXTERIOR_PADDING),
PaddingRight = UDim.new(0, EXTERIOR_PADDING),
PaddingTop = UDim.new(0, EXTERIOR_PADDING),
}),
GameIcon = Roact.createElement(GameIcon, {
gameId = game.GameId,
iconSize = 100,
}),
GameName = Roact.createElement(ThemedTextLabel, {
fontKey = "Title",
themeKey = "TextEmphasis",
Position = UDim2.new(0, GAME_ICON_RENDER_SIZE + TEXT_ICON_PADDING, 0, 0),
Size = UDim2.new(1, -(GAME_ICON_RENDER_SIZE + TEXT_ICON_PADDING), 1, 0),
Text = props.gameName,
TextTruncate = Enum.TextTruncate.AtEnd,
TextWrapped = true,
TextXAlignment = Enum.TextXAlignment.Left,
TextYAlignment = Enum.TextYAlignment.Top,
}),
})
end
return RoactRodux.UNSTABLE_connect2(function(state)
return {
gameName = state.localization.currentGameName,
}
end)(GameIconHeader)
@@ -0,0 +1,43 @@
return function()
local CorePackages = game:GetService("CorePackages")
local InGameMenuDependencies = require(CorePackages.InGameMenuDependencies)
local Roact = InGameMenuDependencies.Roact
local RoactRodux = InGameMenuDependencies.RoactRodux
local Rodux = InGameMenuDependencies.Rodux
local UIBlox = InGameMenuDependencies.UIBlox
local InGameMenu = script.Parent.Parent
local reducer = require(InGameMenu.reducer)
local Localization = require(InGameMenu.Localization.Localization)
local LocalizationProvider = require(InGameMenu.Localization.LocalizationProvider)
local AppDarkTheme = require(CorePackages.AppTempCommon.LuaApp.Style.Themes.DarkTheme)
local AppFont = require(CorePackages.AppTempCommon.LuaApp.Style.Fonts.Gotham)
local GameIconHeader = require(script.Parent.GameIconHeader)
local appStyle = {
Theme = AppDarkTheme,
Font = AppFont,
}
it("should create and destroy without errors", function()
local element = Roact.createElement(RoactRodux.StoreProvider, {
store = Rodux.Store.new(reducer)
}, {
ThemeProvider = Roact.createElement(UIBlox.Core.Style.Provider, {
style = appStyle,
}, {
LocalizationProvider = Roact.createElement(LocalizationProvider, {
localization = Localization.new("en-us"),
}, {
GameIconHeader = Roact.createElement(GameIconHeader),
}),
}),
})
local instance = Roact.mount(element)
Roact.unmount(instance)
end)
end
@@ -0,0 +1,138 @@
local CoreGui = game:GetService("CoreGui")
local CorePackages = game:GetService("CorePackages")
local UserGameSettings = UserSettings():GetService("UserGameSettings")
local MicroProfilerChanged = UserGameSettings:GetPropertyChangedSignal("OnScreenProfilerEnabled")
local InGameMenuDependencies = require(CorePackages.InGameMenuDependencies)
local Roact = InGameMenuDependencies.Roact
local RoactRodux = InGameMenuDependencies.RoactRodux
local t = InGameMenuDependencies.t
local UIBlox = InGameMenuDependencies.UIBlox
local withStyle = UIBlox.Core.Style.withStyle
local DevConsoleMaster = require(CoreGui.RobloxGui.Modules.DevConsoleMaster)
local InGameMenu = script.Parent.Parent.Parent
local Divider = require(InGameMenu.Components.Divider)
local ExternalEventConnection = require(InGameMenu.Utility.ExternalEventConnection)
local Page = require(script.Parent.Parent.Page)
local CategoryHeader = require(script.Parent.CategoryHeader)
local ToggleEntry = require(script.Parent.ToggleEntry)
local VersionReporter = require(script.Parent.VersionReporter)
local CloseMenu = require(InGameMenu.Thunks.CloseMenu)
local withLocalization = require(InGameMenu.Localization.withLocalization)
local SendAnalytics = require(InGameMenu.Utility.SendAnalytics)
local Constants = require(InGameMenu.Resources.Constants)
local AdvancedPage = Roact.PureComponent:extend("AdvancedPage")
AdvancedPage.validateProps = t.strictInterface({
closeMenu = t.callback,
pageTitle = t.string,
})
function AdvancedPage:init()
self:setState({
microProfilerEnabled = UserGameSettings.OnScreenProfilerEnabled,
performanceStatsEnabled = UserGameSettings.PerformanceStatsVisible,
})
end
function AdvancedPage:render()
return withStyle(function(style)
return Roact.createElement(Page, {
pageTitle = self.props.pageTitle,
zIndex = 2,
position = self.props.position,
}, {
Layout = Roact.createElement("UIListLayout", {
SortOrder = Enum.SortOrder.LayoutOrder,
HorizontalAlignment = Enum.HorizontalAlignment.Right,
VerticalAlignment = Enum.VerticalAlignment.Top,
}),
AdvancedHeader = Roact.createElement(CategoryHeader, {
LayoutOrder = 1,
localizationKey = "CoreScripts.InGameMenu.GameSettings.AdvancedSettingsTitle",
}),
PerformanceStats = Roact.createElement(ToggleEntry, {
LayoutOrder = 2,
labelKey = "CoreScripts.InGameMenu.GameSettings.ShowPerfStats",
checked = self.state.performanceStatsEnabled,
onToggled = function()
UserGameSettings.PerformanceStatsVisible = not UserGameSettings.PerformanceStatsVisible
SendAnalytics(Constants.AnalyticsSettingsChangeName, nil, {}, true)
end,
}),
MicroProfiler = Roact.createElement(ToggleEntry, {
LayoutOrder = 3,
labelKey = "CoreScripts.InGameMenu.GameSettings.ShowMicroProfiler",
checked = self.state.microProfilerEnabled,
onToggled = function()
UserGameSettings.OnScreenProfilerEnabled = not UserGameSettings.OnScreenProfilerEnabled
SendAnalytics(Constants.AnalyticsSettingsChangeName, nil, {}, true)
end,
}),
DeveloperConsole = withLocalization({
text = "CoreScripts.InGameMenu.GameSettings.DeveloperConsole"
})(function(localized)
return Roact.createElement("TextButton", {
BackgroundTransparency = 1,
Size = UDim2.new(1, 0, 0, 54),
Text = localized.text,
TextColor3 = style.Theme.TextEmphasis.Color,
Font = style.Font.Header2.Font,
TextSize = style.Font.Header2.RelativeSize * style.Font.BaseSize,
TextXAlignment = Enum.TextXAlignment.Left,
LayoutOrder = 4,
[Roact.Event.Activated] = function()
DevConsoleMaster:SetVisibility(true)
self.props.closeMenu()
end,
}, {
Padding = Roact.createElement("UIPadding", {
PaddingLeft = UDim.new(0, 24),
}),
})
end),
Divider = Roact.createElement(Divider, {
Size = UDim2.new(1, -24, 0, 1),
LayoutOrder = 5,
}),
VersionReporter = Roact.createElement(VersionReporter, {
LayoutOrder = 6,
}),
MicroProfilerVisibilityListener = Roact.createElement(ExternalEventConnection, {
event = MicroProfilerChanged,
callback = function()
self:setState({
microProfilerEnabled = UserGameSettings.OnScreenProfilerEnabled,
})
end,
}),
PerformanceStatsVisibilityListener = Roact.createElement(ExternalEventConnection, {
event = UserGameSettings.PerformanceStatsVisibleChanged,
callback = function()
self:setState({
performanceStatsEnabled = UserGameSettings.PerformanceStatsVisible,
})
end,
}),
})
end)
end
return RoactRodux.UNSTABLE_connect2(nil, function(dispatch)
return {
closeMenu = function()
dispatch(CloseMenu)
end
}
end)(AdvancedPage)
@@ -0,0 +1,90 @@
local CorePackages = game:GetService("CorePackages")
local InGameMenuDependencies = require(CorePackages.InGameMenuDependencies)
local Roact = InGameMenuDependencies.Roact
local t = InGameMenuDependencies.t
local InGameMenu = script.Parent.Parent.Parent
local ExternalEventConnection = require(InGameMenu.Utility.ExternalEventConnection)
local ToggleEntry = require(script.Parent.ToggleEntry)
local SendAnalytics = require(InGameMenu.Utility.SendAnalytics)
local Constants = require(InGameMenu.Resources.Constants)
local AutoPropertyToggleEntry = Roact.PureComponent:extend("AutoPropertyToggleEntry")
AutoPropertyToggleEntry.validateProps = t.strictInterface({
instance = t.Instance,
key = t.string,
lockedToOff = t.optional(t.boolean),
onValue = t.optional(t.any),
offValue = t.optional(t.any),
labelKey = t.string,
LayoutOrder = t.integer,
subtextEnabled = t.optional(t.boolean),
subtextKey = t.optional(t.string),
})
AutoPropertyToggleEntry.defaultProps = {
onValue = true,
offValue = false,
lockedToOff = false,
subtextEnabled = false,
subtextKey = "",
}
function AutoPropertyToggleEntry:init(props)
local currentValue = props.instance[props.key]
self.changeEvent = props.instance:GetPropertyChangedSignal(props.key)
self:setState({
checked = not props.lockedToOff and currentValue == props.onValue,
})
self.onExternalChange = function()
local newValue = props.instance[props.key]
self:setState({
checked = newValue == props.onValue
})
end
self.onToggled = function()
if self.props.lockedToOff then
return
end
local isOn = props.instance[props.key] == props.onValue
if isOn then
props.instance[props.key] = props.offValue
else
props.instance[props.key] = props.onValue
end
SendAnalytics(Constants.AnalyticsSettingsChangeName, nil, {}, true)
end
end
function AutoPropertyToggleEntry:render()
local props = self.props
return Roact.createFragment({
Toggle = Roact.createElement(ToggleEntry, {
checked = self.state.checked,
lockedToOff = props.lockedToOff,
labelKey = props.labelKey,
LayoutOrder = props.LayoutOrder,
onToggled = self.onToggled,
subtextEnabled = props.subtextEnabled,
subtextKey = props.subtextKey,
}),
Listener = Roact.createElement(ExternalEventConnection, {
event = self.changeEvent,
callback = self.onExternalChange,
}),
})
end
return AutoPropertyToggleEntry
@@ -0,0 +1,57 @@
return function()
local CorePackages = game:GetService("CorePackages")
local InGameMenuDependencies = require(CorePackages.InGameMenuDependencies)
local Roact = InGameMenuDependencies.Roact
local Rodux = InGameMenuDependencies.Rodux
local RoactRodux = InGameMenuDependencies.RoactRodux
local UIBlox = InGameMenuDependencies.UIBlox
local InGameMenu = script.Parent.Parent.Parent
local Localization = require(InGameMenu.Localization.Localization)
local LocalizationProvider = require(InGameMenu.Localization.LocalizationProvider)
local reducer = require(InGameMenu.reducer)
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 AutoPropertyToggleEntry = require(script.Parent.AutoPropertyToggleEntry)
it("should create and destroy without errors", function()
local dummyValue = Instance.new("BoolValue")
local element = Roact.createElement(RoactRodux.StoreProvider, {
store = Rodux.Store.new(reducer)
}, {
ThemeProvider = Roact.createElement(UIBlox.Core.Style.Provider, {
style = appStyle,
}, {
LocalizationProvider = Roact.createElement(LocalizationProvider, {
localization = Localization.new("en-us"),
}, {
AutoPropertyToggleEntry = Roact.createElement(AutoPropertyToggleEntry, {
instance = dummyValue,
key = "Value",
lockedToOff = false,
onValue = true,
offValue = false,
labelKey = "CoreScripts.InGameMenu.GameSettings.ShowPerfStats",
LayoutOrder = 2,
}),
}),
}),
})
local instance = Roact.mount(element)
dummyValue.Value = false
dummyValue.Value = true
Roact.unmount(instance)
end)
end
@@ -0,0 +1,258 @@
local Players = game:GetService("Players")
local UserGameSettings = UserSettings():GetService("UserGameSettings")
local GuiService = game:GetService("GuiService")
local CorePackages = game:GetService("CorePackages")
local VRService = game:GetService("VRService")
local RobloxGui = game:GetService("CoreGui"):WaitForChild("RobloxGui")
local InGameMenuDependencies = require(CorePackages.InGameMenuDependencies)
local Roact = InGameMenuDependencies.Roact
local RoactRodux = InGameMenuDependencies.RoactRodux
local t = InGameMenuDependencies.t
local UIBlox = InGameMenuDependencies.UIBlox
local localPlayer = Players.LocalPlayer
local DevEnableMouseLockChanged = localPlayer:GetPropertyChangedSignal("DevEnableMouseLock")
local IsUsingCameraYInvertedChanged = UserGameSettings:GetPropertyChangedSignal("IsUsingCameraYInverted")
local InGameMenu = script.Parent.Parent.Parent
local Divider = require(InGameMenu.Components.Divider)
local ExternalEventConnection = require(InGameMenu.Utility.ExternalEventConnection)
local Page = require(InGameMenu.Components.Page)
local ThemedTextLabel = require(InGameMenu.Components.ThemedTextLabel)
local withLocalization = require(InGameMenu.Localization.withLocalization)
local Assets = require(InGameMenu.Resources.Assets)
local SetCurrentPage = require(InGameMenu.Actions.SetCurrentPage)
local AutoPropertyToggleEntry = require(script.Parent.AutoPropertyToggleEntry)
local CameraModeEntry = require(script.Parent.CameraModeEntry)
local CameraSensitivityEntry = require(script.Parent.CameraSensitivityEntry)
local CategoryHeader = require(script.Parent.CategoryHeader)
local GraphicsQualityEntry = require(script.Parent.GraphicsQualityEntry)
local MovementModeEntry = require(script.Parent.MovementModeEntry)
local ToggleEntry = require(script.Parent.ToggleEntry)
local VolumeEntry = require(script.Parent.VolumeEntry)
local SendAnalytics = require(InGameMenu.Utility.SendAnalytics)
local Constants = require(InGameMenu.Resources.Constants)
local ImageSetLabel = UIBlox.Core.ImageSet.Label
local FFlagInGameMenuVRToggle = game:DefineFastFlag("InGameMenuVRToggle", false)
local GetFFlagFullscreenAnalytics = require(RobloxGui.Modules.Flags.GetFFlagFullscreenAnalytics)
local VRAvailableChanged = VRService:GetPropertyChangedSignal("VREnabled")
local VREnabledChanged = UserGameSettings:GetPropertyChangedSignal("VREnabled")
local BasicPage = Roact.PureComponent:extend("BasicPage")
BasicPage.validateProps = t.strictInterface({
switchToAdvancedPage = t.callback,
pageTitle = t.string,
})
local vrEnabledAtModuleLoad = UserGameSettings.VREnabled
function BasicPage:init()
self:setState({
shiftLockEnabled = localPlayer.DevEnableMouseLock,
fullScreenEnabled = UserGameSettings:InFullScreen(),
invertedCameraEnabled = UserGameSettings.IsUsingCameraYInverted,
vrAvailable = VRService.VREnabled,
vrEnabled = UserGameSettings.VREnabled,
})
self.pageSize, self.setPageSize = Roact.createBinding(UDim2.new(0, 0, 0, 0))
end
function BasicPage:render()
local shouldSettingsDisabledInVRBeShown = not (FFlagInGameMenuVRToggle and self.state.vrEnabled and self.state.vrAvailable)
return Roact.createElement(Page, {
pageTitle = self.props.pageTitle,
position = self.props.position,
}, {
PageContents = Roact.createElement("ScrollingFrame", {
BackgroundTransparency = 1,
BorderSizePixel = 0,
CanvasSize = self.pageSize,
Position = self.props.position,
Size = UDim2.new(1, 0, 1, 0),
}, {
Layout = Roact.createElement("UIListLayout", {
HorizontalAlignment = Enum.HorizontalAlignment.Right,
SortOrder = Enum.SortOrder.LayoutOrder,
VerticalAlignment = Enum.VerticalAlignment.Top,
[Roact.Change.AbsoluteContentSize] = function(rbx)
self.setPageSize(UDim2.new(0, 0, 0, rbx.AbsoluteContentSize.Y))
end,
}),
CameraHeader = Roact.createElement(CategoryHeader, {
LayoutOrder = 1,
localizationKey = "CoreScripts.InGameMenu.GameSettings.CameraTitle",
}),
CameraMode = shouldSettingsDisabledInVRBeShown and Roact.createElement(CameraModeEntry, {
LayoutOrder = 2,
}),
InvertedCamera = shouldSettingsDisabledInVRBeShown and self.state.invertedCameraEnabled and Roact.createElement(AutoPropertyToggleEntry, {
LayoutOrder = 3,
labelKey = "CoreScripts.InGameMenu.GameSettings.InvertedCamera",
instance = UserGameSettings,
key = "CameraYInverted",
}),
CameraSensitivity = shouldSettingsDisabledInVRBeShown and Roact.createElement(CameraSensitivityEntry, {
LayoutOrder = 4,
}),
CameraDivider = Roact.createElement(Divider, {
LayoutOrder = 5,
Size = UDim2.new(1, -24, 0, 1),
}),
ControlsHeader = Roact.createElement(CategoryHeader, {
LayoutOrder = 6,
localizationKey = "CoreScripts.InGameMenu.GameSettings.ControlsAudio",
}),
ShiftLock = shouldSettingsDisabledInVRBeShown and Roact.createElement(AutoPropertyToggleEntry, {
LayoutOrder = 7,
labelKey = "CoreScripts.InGameMenu.GameSettings.ShiftLock",
instance = UserGameSettings,
key = "ControlMode",
onValue = Enum.ControlMode.MouseLockSwitch,
offValue = Enum.ControlMode.Classic,
lockedToOff = not self.state.shiftLockEnabled,
}),
MovementMode = shouldSettingsDisabledInVRBeShown and Roact.createElement(MovementModeEntry, {
LayoutOrder = 8,
}),
VolumeEntry = Roact.createElement(VolumeEntry, {
LayoutOrder = 9,
}),
ControlsDivider = Roact.createElement(Divider, {
LayoutOrder = 10,
Size = UDim2.new(1, -24, 0, 1),
}),
GraphicsHeader = Roact.createElement(CategoryHeader, {
LayoutOrder = 11,
localizationKey = "CoreScripts.InGameMenu.GameSettings.GraphicsTitle",
}),
GraphicsQualityEntry = Roact.createElement(GraphicsQualityEntry, {
LayoutOrder = 12,
}),
FullScreen = shouldSettingsDisabledInVRBeShown and Roact.createElement(ToggleEntry, {
LayoutOrder = 13,
labelKey = "CoreScripts.InGameMenu.GameSettings.FullScreen",
checked = self.state.fullScreenEnabled,
onToggled = function()
GuiService:ToggleFullscreen()
SendAnalytics(Constants.AnalyticsSettingsChangeName, nil, {}, true)
end,
}),
VRMode = FFlagInGameMenuVRToggle and self.state.vrAvailable and Roact.createElement(AutoPropertyToggleEntry, {
LayoutOrder = 14,
labelKey = "CoreScripts.InGameMenu.GameSettings.VREnabled",
instance = UserGameSettings,
key = "VREnabled",
subtextEnabled = self.state.vrEnabled ~= vrEnabledAtModuleLoad,
subtextKey = "CoreScripts.InGameMenu.GameSettings.RestartPending",
}),
GraphicsDivider = Roact.createElement(Divider, {
LayoutOrder = 15,
Size = UDim2.new(1, -24, 0, 1),
}),
AdvancedSettings = Roact.createElement("TextButton", {
LayoutOrder = 16,
BackgroundTransparency = 1,
Size = UDim2.new(1, 0, 0, 54),
Text = "",
[Roact.Event.Activated] = self.props.switchToAdvancedPage,
}, {
Text = withLocalization({
text = "CoreScripts.InGameMenu.GameSettings.SwitchToAdvancedSettings",
})(function(localization)
return Roact.createElement(ThemedTextLabel, {
Text = localization.text,
TextXAlignment = Enum.TextXAlignment.Left,
fontKey = "Body",
themeKey = "TextDefault",
Size = UDim2.new(1, -96, 1, 0),
Position = UDim2.new(0, 24, 0.5, 0),
AnchorPoint = Vector2.new(0, 0.5),
})
end),
Icon = Roact.createElement(ImageSetLabel, {
BackgroundTransparency = 1,
Image = Assets.Images.NavigateForward,
Size = UDim2.new(0, 36, 0, 36),
Position = UDim2.new(1, -36, 0.5, 0),
AnchorPoint = Vector2.new(1, 0.5),
}),
}),
AdvancedDivider = Roact.createElement(Divider, {
LayoutOrder = 17,
Size = UDim2.new(1, -24, 0, 1),
}),
ShiftLockEnabledListener = Roact.createElement(ExternalEventConnection, {
event = DevEnableMouseLockChanged,
callback = function()
self:setState({
shiftLockEnabled = localPlayer.DevEnableMouseLock,
})
end,
}),
FullScreenListener = Roact.createElement(ExternalEventConnection, {
event = UserGameSettings.FullscreenChanged,
callback = function(newFullscreenStatus)
self:setState({
fullScreenEnabled = newFullscreenStatus,
})
if GetFFlagFullscreenAnalytics() then
SendAnalytics(Constants.AnalyticsInGameMenuName, Constants.AnalyticsFullscreenModeName, {
enabled = newFullscreenStatus,
})
end
end,
}),
CameraYInvertedListener = Roact.createElement(ExternalEventConnection, {
event = IsUsingCameraYInvertedChanged,
callback = function()
self:setState({
invertedCameraEnabled = UserGameSettings.IsUsingCameraYInverted,
})
end,
}),
VRAvailableListener = FFlagInGameMenuVRToggle and Roact.createElement(ExternalEventConnection, {
event = VRAvailableChanged,
callback = function()
self:setState({
vrAvailable = VRService.VREnabled
})
end,
}),
VREnabledListener = FFlagInGameMenuVRToggle and Roact.createElement(ExternalEventConnection, {
event = VREnabledChanged,
callback = function()
self:setState({
vrEnabled = UserGameSettings.VREnabled
})
end,
}),
})
})
end
return RoactRodux.UNSTABLE_connect2(nil, function(dispatch)
return {
switchToAdvancedPage = function()
dispatch(SetCurrentPage(Constants.advancedSettingsPageKey))
end,
}
end)(BasicPage)
@@ -0,0 +1,140 @@
local CorePackages = game:GetService("CorePackages")
local Players = game:GetService("Players")
local UserGameSettings = UserSettings():GetService("UserGameSettings")
local ComputerCameraMovementModeChanged = UserGameSettings:GetPropertyChangedSignal("ComputerCameraMovementMode")
local localPlayer = Players.LocalPlayer
local playerScripts = localPlayer:WaitForChild("PlayerScripts")
local DevComputerMovementModeChanged = localPlayer:GetPropertyChangedSignal("DevComputerMovementMode")
local InGameMenuDependencies = require(CorePackages.InGameMenuDependencies)
local Roact = InGameMenuDependencies.Roact
local t = InGameMenuDependencies.t
local InGameMenu = script.Parent.Parent.Parent
local DropDownSelection = require(InGameMenu.Components.DropDownSelection)
local ExternalEventConnection = require(InGameMenu.Utility.ExternalEventConnection)
local InputLabel = require(script.Parent.InputLabel)
local DeveloperLockLabel = require(script.Parent.DeveloperLockLabel)
local withLocalization = require(InGameMenu.Localization.withLocalization)
local SendAnalytics = require(InGameMenu.Utility.SendAnalytics)
local Constants = require(InGameMenu.Resources.Constants)
local CAMERA_MODE_LOCALIZATION_KEYS = {
[Enum.ComputerCameraMovementMode.Classic] = "CoreScripts.InGameMenu.GameSettings.CameraModeClassic",
[Enum.ComputerCameraMovementMode.Follow] = "CoreScripts.InGameMenu.GameSettings.CameraModeFollow",
[Enum.ComputerCameraMovementMode.Default] = "CoreScripts.InGameMenu.GameSettings.Default",
[Enum.ComputerCameraMovementMode.CameraToggle] = "CoreScripts.InGameMenu.GameSettings.CameraModeCameraToggle",
}
local CameraModeEntry = Roact.PureComponent:extend("CameraSensitivityEntry")
CameraModeEntry.validateProps = t.strictInterface({
LayoutOrder = t.integer,
})
function CameraModeEntry:init()
self:setState({
selectedComputerMode = UserGameSettings.ComputerCameraMovementMode,
computerOptions = playerScripts:GetRegisteredComputerCameraMovementModes(),
developerComputerMode = localPlayer.DevComputerCameraMode,
})
end
function CameraModeEntry:render()
-- TODO: Support gamepad and touch controls.
local result = {
ComputerCameraMovementModeListener = Roact.createElement(ExternalEventConnection, {
event = ComputerCameraMovementModeChanged,
callback = function()
self:setState({
selectedComputerMode = UserGameSettings.ComputerCameraMovementMode,
})
end,
}),
ComputerCameraMovementModeRegisteredListener = Roact.createElement(ExternalEventConnection, {
event = playerScripts.ComputerCameraMovementModeRegistered,
callback = function()
self:setState({
computerOptions = playerScripts:GetRegisteredComputerCameraMovementModes(),
})
end,
}),
LocalPlayerComputerMovementMode = Roact.createElement(ExternalEventConnection, {
event = DevComputerMovementModeChanged,
callback = function()
self:setState({
developerComputerMode = localPlayer.DevComputerCameraMode,
})
end,
}),
}
if #self.state.computerOptions == 0 then
return Roact.createFragment(result)
end
local disabled = self.state.developerComputerMode ~= Enum.DevComputerCameraMovementMode.UserChoice
local optionTexts = {}
local selectedIndex = 0
if not disabled then
for index, enum in ipairs(self.state.computerOptions) do
optionTexts[index] = CAMERA_MODE_LOCALIZATION_KEYS[enum]
assert(optionTexts[index] ~= nil, "Camera movement mode " .. enum.Name .. " has no localization key")
if self.state.selectedComputerMode.Value == enum.Value then
selectedIndex = index
end
end
end
result.CameraModeEntrySelector = Roact.createElement("Frame", {
Size = UDim2.new(1, 0, 0, 44 + 56 + 20),
BackgroundTransparency = 1,
LayoutOrder = self.props.LayoutOrder,
ZIndex = 2,
}, {
Padding = Roact.createElement("UIPadding", {
PaddingLeft = UDim.new(0, 24),
PaddingRight = UDim.new(0, 24),
}),
InputLabel = Roact.createElement(InputLabel, {
localizationKey = "CoreScripts.InGameMenu.GameSettings.CameraMode",
Size = UDim2.new(1, 0, 0, 56),
Position = UDim2.new(0, 0, 0, 0),
AnchorPoint = Vector2.new(0, 0),
}),
Dropdown = withLocalization({
placeholder = disabled and CAMERA_MODE_LOCALIZATION_KEYS[self.state.developerComputerMode] or nil
})(function(localized)
return Roact.createElement(DropDownSelection, {
Size = UDim2.new(1, 0, 0, 44),
Position = UDim2.new(0, 0, 0, 56),
selections = optionTexts,
selectedIndex = selectedIndex,
placeHolderText = self.state.developerComputerMode.Name,
enabled = not disabled,
localize = true,
selectionChanged = function(newSelection)
UserGameSettings.ComputerCameraMovementMode = self.state.computerOptions[newSelection]
SendAnalytics(Constants.AnalyticsSettingsChangeName, nil, {}, true)
end,
})
end),
LockedLabel = disabled and Roact.createElement(DeveloperLockLabel, {
AnchorPoint = Vector2.new(0, 1),
Position = UDim2.new(0, 0, 1, 0),
Size = UDim2.new(1, -72, 0.25, 0),
}),
})
return Roact.createFragment(result)
end
return CameraModeEntry
@@ -0,0 +1,147 @@
local MIN_ENGINE_MOUSE_SENSITIVITY = 0
local MAX_ENGINE_MOUSE_SENSITIVITY = 10
local MIN_GUI_MOUSE_SENSITIVITY = 0
local MAX_GUI_MOUSE_SENSITIVITY = 10
local GUI_MOUSE_SENSITIVITY_GRANULARITY = 0.01
local UserGameSettings = UserSettings():GetService("UserGameSettings")
local CorePackages = game:GetService("CorePackages")
local InGameMenuDependencies = require(CorePackages.InGameMenuDependencies)
local Roact = InGameMenuDependencies.Roact
local t = InGameMenuDependencies.t
local sensitivityEvents = {
UserGameSettings:GetPropertyChangedSignal("GamepadCameraSensitivity"),
UserGameSettings:GetPropertyChangedSignal("MouseSensitivity"),
UserGameSettings:GetPropertyChangedSignal("MouseSensitivityFirstPerson"),
UserGameSettings:GetPropertyChangedSignal("MouseSensitivityThirdPerson"),
}
local InGameMenu = script.Parent.Parent.Parent
local ExternalEventConnection = require(InGameMenu.Utility.ExternalEventConnection)
local SliderEntry = require(script.Parent.SliderEntry)
local SendAnalytics = require(InGameMenu.Utility.SendAnalytics)
local Constants = require(InGameMenu.Resources.Constants)
local FFlagCameraSensitivityAllowGranularKeyboardInput = game:DefineFastFlag("CameraSensitivityAllowGranularKeyboardInput", false)
-- remove with FFlagCameraSensitivityAllowGranularKeyboardInput
local function guiToEngineSensitivity_DEPRECATED(guiSensitivity)
local engineSensitivity
if guiSensitivity <= 5 then
engineSensitivity = 0.16 * guiSensitivity + 0.2
else
engineSensitivity = 0.6 * guiSensitivity - 2
end
return math.clamp(
engineSensitivity,
MIN_ENGINE_MOUSE_SENSITIVITY,
MAX_ENGINE_MOUSE_SENSITIVITY
)
end
local function guiToEngineSensitivity(guiSensitivity)
local engineSensitivity
if guiSensitivity <= 5 then
engineSensitivity = 0.198 * guiSensitivity + 0.01
else
engineSensitivity = 0.6 * guiSensitivity - 2
end
return math.clamp(
engineSensitivity,
MIN_ENGINE_MOUSE_SENSITIVITY,
MAX_ENGINE_MOUSE_SENSITIVITY
)
end
-- remove with FFlagCameraSensitivityAllowGranularKeyboardInput
local function engineToGuiSensitivity_DEPRECATED(engineSensitivity)
local guiSensitivity
if engineSensitivity <= 1 then
guiSensitivity = math.floor((engineSensitivity - 0.2) / 0.16 + 0.5)
else
guiSensitivity = math.floor((engineSensitivity + 2) / 0.6 + 0.5)
end
return math.clamp(
guiSensitivity,
MIN_GUI_MOUSE_SENSITIVITY,
MAX_GUI_MOUSE_SENSITIVITY
)
end
local function engineToGuiSensitivity(engineSensitivity)
local guiSensitivity
if engineSensitivity <= 1 then
guiSensitivity = (engineSensitivity - 0.01) / 0.198
else
guiSensitivity = (engineSensitivity + 2) / 0.6
end
guiSensitivity = math.floor(guiSensitivity / GUI_MOUSE_SENSITIVITY_GRANULARITY + 0.5) * GUI_MOUSE_SENSITIVITY_GRANULARITY
return math.clamp(
guiSensitivity,
MIN_GUI_MOUSE_SENSITIVITY,
MAX_GUI_MOUSE_SENSITIVITY
)
end
local CameraSensitivityEntry = Roact.PureComponent:extend("CameraSensitivityEntry")
CameraSensitivityEntry.validateProps = t.strictInterface({
LayoutOrder = t.integer,
})
function CameraSensitivityEntry:init()
self.onSensitivityChanged = function()
self:setState({
-- TODO: Implement gamepad sensitivity.
engineSensitivity = UserGameSettings.MouseSensitivityFirstPerson.X,
})
end
self.onSensitivityChanged()
end
function CameraSensitivityEntry:render()
local children = {
-- TODO: Switch to gamepad sensitivity when appropriate
CameraSensitivityMouseSlider = Roact.createElement(SliderEntry, {
LayoutOrder = self.props.LayoutOrder,
labelKey = "CoreScripts.InGameMenu.GameSettings.CameraSensitivity",
value = FFlagCameraSensitivityAllowGranularKeyboardInput
and engineToGuiSensitivity(self.state.engineSensitivity)
or engineToGuiSensitivity_DEPRECATED(self.state.engineSensitivity),
min = MIN_GUI_MOUSE_SENSITIVITY,
max = MAX_GUI_MOUSE_SENSITIVITY,
stepInterval = 1,
keyboardInputStepInterval = FFlagCameraSensitivityAllowGranularKeyboardInput and GUI_MOUSE_SENSITIVITY_GRANULARITY or nil,
valueChanged = function(value)
local newEngineSensitivity = FFlagCameraSensitivityAllowGranularKeyboardInput
and guiToEngineSensitivity(value)
or guiToEngineSensitivity_DEPRECATED(value)
local engineSensitivityVector = Vector2.new(newEngineSensitivity, newEngineSensitivity)
UserGameSettings.MouseSensitivityFirstPerson = engineSensitivityVector
UserGameSettings.MouseSensitivityThirdPerson = engineSensitivityVector
SendAnalytics(Constants.AnalyticsSettingsChangeName, nil, {}, true)
end,
})
}
for _, event in ipairs(sensitivityEvents) do
children[tostring(event) .. "CameraSensitivityControlConnection"] = Roact.createElement(ExternalEventConnection, {
event = event,
callback = self.onSensitivityChanged,
})
end
return Roact.createFragment(children)
end
return CameraSensitivityEntry
@@ -0,0 +1,46 @@
local CorePackages = game:GetService("CorePackages")
local InGameMenuDependencies = require(CorePackages.InGameMenuDependencies)
local Roact = InGameMenuDependencies.Roact
local t = InGameMenuDependencies.t
local InGameMenu = script.Parent.Parent.Parent
local GlobalConfig = require(InGameMenu.GlobalConfig)
local ThemedTextLabel = require(script.Parent.Parent.ThemedTextLabel)
local withLocalization = require(InGameMenu.Localization.withLocalization)
local validateProps = t.strictInterface({
LayoutOrder = t.integer,
localizationKey = t.string,
})
return function(props)
if GlobalConfig.propValidation then
assert(validateProps(props))
end
return Roact.createElement("Frame", {
BackgroundTransparency = 1,
LayoutOrder = props.LayoutOrder,
Size = UDim2.new(1, 0, 0, 36),
}, {
Label = withLocalization({
text = props.localizationKey
})(function(localization)
return Roact.createElement(ThemedTextLabel, {
fontKey = "Footer",
themeKey = "TextEmphasis",
AnchorPoint = Vector2.new(0.5, 0.5),
Position = UDim2.new(0.5, 0, 0.5, 0),
Size = UDim2.new(1, -48, 0, 14),
Text = localization.text,
TextTruncate = Enum.TextTruncate.AtEnd,
TextXAlignment = Enum.TextXAlignment.Left,
})
end)
})
end
@@ -0,0 +1,40 @@
local CorePackages = game:GetService("CorePackages")
local InGameMenuDependencies = require(CorePackages.InGameMenuDependencies)
local Roact = InGameMenuDependencies.Roact
local t = InGameMenuDependencies.t
local InGameMenu = script.Parent.Parent.Parent
local GlobalConfig = require(InGameMenu.GlobalConfig)
local withLocalization = require(InGameMenu.Localization.withLocalization)
local ThemedTextLabel = require(InGameMenu.Components.ThemedTextLabel)
local validateProps = t.strictInterface({
Position = t.UDim2,
Size = t.UDim2,
AnchorPoint = t.Vector2,
})
return function(props)
if GlobalConfig.propValidation then
assert(validateProps(props))
end
return withLocalization({
lockedText = "CoreScripts.InGameMenu.GameSettings.LockedByDeveloper",
})(function(localized)
return Roact.createElement(ThemedTextLabel, {
themeKey = "TextDefault",
fontKey = "CaptionBody",
TextTransparency = 0.5,
Text = localized.lockedText,
Size = UDim2.new(1, -72, 0.25, 0),
Position = UDim2.new(0, 0, 1, 0),
AnchorPoint = Vector2.new(0, 1),
TextXAlignment = Enum.TextXAlignment.Left,
TextYAlignment = Enum.TextYAlignment.Bottom,
})
end)
end
@@ -0,0 +1,121 @@
--[[
Encompasses both the automatic graphics quality toggle _and_ the graphics
quality slider.
]]
local CorePackages = game:GetService("CorePackages")
local InGameMenuDependencies = require(CorePackages.InGameMenuDependencies)
local Roact = InGameMenuDependencies.Roact
local t = InGameMenuDependencies.t
local UserGameSettings = UserSettings():GetService("UserGameSettings")
local RenderSettings = settings().Rendering
local SavedQualityLevelChanged = UserGameSettings:GetPropertyChangedSignal("SavedQualityLevel")
local InGameMenu = script.Parent.Parent.Parent
local ToggleEntry = require(script.Parent.ToggleEntry)
local ExternalEventConnection = require(InGameMenu.Utility.ExternalEventConnection)
local SliderEntry = require(script.Parent.SliderEntry)
local SendAnalytics = require(InGameMenu.Utility.SendAnalytics)
local Constants = require(InGameMenu.Resources.Constants)
local SAVED_QUALITY_LEVELS = #Enum.SavedQualitySetting:GetEnumItems() - 1
local function mapInteger(x, xMin, xMax, yMin, yMax)
return math.clamp(
math.floor(yMin + (x - xMin)*(yMax - yMin)/(xMax - xMin)),
yMin,
yMax)
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
self:setState({
quality = not changedToAutomatic and newSavedQuality.Value or nil,
automatic = newSavedQuality == Enum.SavedQualitySetting.Automatic,
})
end
if isAutomatic then
self:setAutomaticQualityLevel()
else
self:setManualQualityLevel(self.state.quality)
end
end
function GraphicsQualityEntry:setAutomaticQualityLevel()
UserGameSettings.SavedQualityLevel = Enum.SavedQualitySetting.Automatic
RenderSettings.QualityLevel = 0
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
end
function GraphicsQualityEntry:render()
return Roact.createElement("Frame", {
BackgroundTransparency = 1,
Size = UDim2.new(1, 0, 0, 56 + 100),
LayoutOrder = self.props.LayoutOrder,
}, {
Layout = Roact.createElement("UIListLayout", {
SortOrder = Enum.SortOrder.LayoutOrder,
}),
AutoToggle = Roact.createElement(ToggleEntry, {
LayoutOrder = 1,
labelKey = "CoreScripts.InGameMenu.GameSettings.AutoGraphicsQuality",
checked = self.state.automatic,
onToggled = function()
if self.state.automatic then
self:setManualQualityLevel(self.state.quality)
else
self:setAutomaticQualityLevel()
end
SendAnalytics(Constants.AnalyticsSettingsChangeName, nil, {}, true)
end,
}),
Slider = Roact.createElement(SliderEntry, {
LayoutOrder = 2,
labelKey = "CoreScripts.InGameMenu.GameSettings.ManualGraphicsQuality",
min = 1,
max = 10,
stepInterval = 1,
value = self.state.quality,
disabled = self.state.automatic,
valueChanged = function(newValue)
self:setManualQualityLevel(newValue)
SendAnalytics(Constants.AnalyticsSettingsChangeName, nil, {}, true)
end,
}),
QualityListener = Roact.createElement(ExternalEventConnection, {
event = SavedQualityLevelChanged,
callback = self.onQualityChanged,
})
})
end
return GraphicsQualityEntry
@@ -0,0 +1,82 @@
return function()
local CorePackages = game:GetService("CorePackages")
local InGameMenuDependencies = require(CorePackages.InGameMenuDependencies)
local Roact = InGameMenuDependencies.Roact
local Rodux = InGameMenuDependencies.Rodux
local RoactRodux = InGameMenuDependencies.RoactRodux
local UIBlox = InGameMenuDependencies.UIBlox
local InGameMenu = script.Parent.Parent.Parent
local Localization = require(InGameMenu.Localization.Localization)
local LocalizationProvider = require(InGameMenu.Localization.LocalizationProvider)
local reducer = require(InGameMenu.reducer)
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 GraphicsQualityEntry = require(script.Parent.GraphicsQualityEntry)
local UserGameSettings = UserSettings():GetService("UserGameSettings")
local RenderSettings = settings().Rendering
it("should create and destroy without errors", function()
local element = Roact.createElement(RoactRodux.StoreProvider, {
store = Rodux.Store.new(reducer)
}, {
ThemeProvider = Roact.createElement(UIBlox.Core.Style.Provider, {
style = appStyle,
}, {
LocalizationProvider = Roact.createElement(LocalizationProvider, {
localization = Localization.new("en-us"),
}, {
GraphicsQualityEntry = Roact.createElement(GraphicsQualityEntry, {
LayoutOrder = 2,
}),
}),
}),
})
local instance = Roact.mount(element)
UserGameSettings.SavedQualityLevel = 10
RenderSettings.QualityLevel = 5
Roact.unmount(instance)
end)
it("should set the rendering setting to the saved quality level when initalized", function()
local element = Roact.createElement(RoactRodux.StoreProvider, {
store = Rodux.Store.new(reducer)
}, {
ThemeProvider = Roact.createElement(UIBlox.Core.Style.Provider, {
style = appStyle,
}, {
LocalizationProvider = Roact.createElement(LocalizationProvider, {
localization = Localization.new("en-us"),
}, {
GraphicsQualityEntry = Roact.createElement(GraphicsQualityEntry, {
LayoutOrder = 2,
}),
}),
}),
})
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
@@ -0,0 +1,42 @@
local CorePackages = game:GetService("CorePackages")
local InGameMenuDependencies = require(CorePackages.InGameMenuDependencies)
local Roact = InGameMenuDependencies.Roact
local t = InGameMenuDependencies.t
local InGameMenu = script.Parent.Parent.Parent
local GlobalConfig = require(InGameMenu.GlobalConfig)
local ThemedTextLabel = require(InGameMenu.Components.ThemedTextLabel)
local withLocalization = require(InGameMenu.Localization.withLocalization)
local validateProps = t.strictInterface({
AnchorPoint = t.optional(t.Vector2),
Position = t.optional(t.UDim2),
Size = t.optional(t.UDim2),
localizationKey = t.string,
})
return function(props)
if GlobalConfig.propValidation then
assert(validateProps(props))
end
return withLocalization({
text = props.localizationKey,
})(function(localized)
return Roact.createElement(ThemedTextLabel, {
fontKey = "Body",
themeKey = "TextDefault",
AnchorPoint = props.AnchorPoint or Vector2.new(0, 0.5),
Position = props.Position or UDim2.new(0, 0, 0.5, 0),
Size = props.Size or UDim2.new(1, -72, 0.5, 0),
Text = localized.text,
TextTruncate = Enum.TextTruncate.AtEnd,
TextXAlignment = Enum.TextXAlignment.Left,
})
end)
end
@@ -0,0 +1,144 @@
local CorePackages = game:GetService("CorePackages")
local Players = game:GetService("Players")
local UserGameSettings = UserSettings():GetService("UserGameSettings")
local ComputerMovementModeChanged = UserGameSettings:GetPropertyChangedSignal("ComputerMovementMode")
local localPlayer = Players.LocalPlayer
local playerScripts = localPlayer:WaitForChild("PlayerScripts")
local DevComputerMovementModeChanged = localPlayer:GetPropertyChangedSignal("DevComputerMovementMode")
local InGameMenuDependencies = require(CorePackages.InGameMenuDependencies)
local Roact = InGameMenuDependencies.Roact
local t = InGameMenuDependencies.t
local InGameMenu = script.Parent.Parent.Parent
local DropDownSelection = require(InGameMenu.Components.DropDownSelection)
local ExternalEventConnection = require(InGameMenu.Utility.ExternalEventConnection)
local InputLabel = require(script.Parent.InputLabel)
local DeveloperLockLabel = require(script.Parent.DeveloperLockLabel)
local withLocalization = require(InGameMenu.Localization.withLocalization)
local SendAnalytics = require(InGameMenu.Utility.SendAnalytics)
local Constants = require(InGameMenu.Resources.Constants)
local MOVEMENT_MODE_LOCALIZATION_KEYS = {
[Enum.ComputerMovementMode.KeyboardMouse] = "CoreScripts.InGameMenu.GameSettings.ComputerMoveModeKeyboardMouse",
[Enum.ComputerMovementMode.ClickToMove] = "CoreScripts.InGameMenu.GameSettings.ComputerMoveModeClickToMove",
[Enum.ComputerMovementMode.Default] = "CoreScripts.InGameMenu.GameSettings.Default",
}
local DEV_MOVEMENT_MODE_LOCALIZATION_KEYS = {
[Enum.DevComputerMovementMode.KeyboardMouse] = "CoreScripts.InGameMenu.GameSettings.ComputerMoveModeKeyboardMouse",
[Enum.DevComputerMovementMode.ClickToMove] = "CoreScripts.InGameMenu.GameSettings.ComputerMoveModeClickToMove",
}
local MovementModeEntry = Roact.PureComponent:extend("MovementModeEntry")
MovementModeEntry.validateProps = t.strictInterface({
LayoutOrder = t.integer,
})
function MovementModeEntry:init()
self:setState({
selectedComputerMode = UserGameSettings.ComputerMovementMode,
computerOptions = playerScripts:GetRegisteredComputerMovementModes(),
developerComputerMode = localPlayer.DevComputerMovementMode,
})
end
function MovementModeEntry:render()
-- TODO: Support gamepad and touch controls.
local result = {
ComputerMovementModeListener = Roact.createElement(ExternalEventConnection, {
event = ComputerMovementModeChanged,
callback = function()
self:setState({
selectedComputerMode = UserGameSettings.ComputerMovementMode,
})
end,
}),
ComputerMovementModeRegisteredListener = Roact.createElement(ExternalEventConnection, {
event = playerScripts.ComputerMovementModeRegistered,
callback = function()
self:setState({
computerOptions = playerScripts:GetRegisteredComputerMovementModes(),
})
end,
}),
DevComputerMovementMode = Roact.createElement(ExternalEventConnection, {
event = DevComputerMovementModeChanged,
callback = function()
self:setState({
developerComputerMode = localPlayer.DevComputerMovementMode,
})
end,
}),
}
if #self.state.computerOptions == 0 then
return Roact.createFragment(result)
end
local disabled = self.state.developerComputerMode ~= Enum.DevComputerMovementMode.UserChoice
local optionTexts = {}
local selectedIndex = 0
if not disabled then
for index, enum in ipairs(self.state.computerOptions) do
optionTexts[index] = MOVEMENT_MODE_LOCALIZATION_KEYS[enum]
assert(optionTexts[index] ~= nil, "Movement mode " .. enum.Name .. " has no localization key")
if self.state.selectedComputerMode.Value == enum.Value then
selectedIndex = index
end
end
end
result.MovementModeEntrySelector = Roact.createElement("Frame", {
Size = UDim2.new(1, 0, 0, 44 + 56 + 20),
BackgroundTransparency = 1,
LayoutOrder = self.props.LayoutOrder,
ZIndex = 2,
}, {
Padding = Roact.createElement("UIPadding", {
PaddingLeft = UDim.new(0, 24),
PaddingRight = UDim.new(0, 24),
}),
InputLabel = Roact.createElement(InputLabel, {
localizationKey = "CoreScripts.InGameMenu.GameSettings.MovementMode",
Size = UDim2.new(1, 0, 0, 56),
Position = UDim2.new(0, 0, 0, 0),
AnchorPoint = Vector2.new(0, 0),
}),
Dropdown = withLocalization({
placeholder = disabled and DEV_MOVEMENT_MODE_LOCALIZATION_KEYS[self.state.developerComputerMode] or nil
})(function(localized)
return Roact.createElement(DropDownSelection, {
Size = UDim2.new(1, 0, 0, 44),
Position = UDim2.new(0, 0, 0, 56),
selections = optionTexts,
selectedIndex = selectedIndex,
placeHolderText = localized.placeholder,
enabled = not disabled,
localize = true,
selectionChanged = function(newSelection)
UserGameSettings.ComputerMovementMode = self.state.computerOptions[newSelection]
SendAnalytics(Constants.AnalyticsSettingsChangeName, nil, {}, true)
end,
})
end),
LockedLabel = disabled and Roact.createElement(DeveloperLockLabel, {
AnchorPoint = Vector2.new(0, 1),
Position = UDim2.new(0, 0, 1, 0),
Size = UDim2.new(1, -72, 0.25, 0),
}),
})
return Roact.createFragment(result)
end
return MovementModeEntry
@@ -0,0 +1,54 @@
return function()
local CorePackages = game:GetService("CorePackages")
local Players = game:GetService("Players")
local InGameMenuDependencies = require(CorePackages.InGameMenuDependencies)
local Roact = InGameMenuDependencies.Roact
local Rodux = InGameMenuDependencies.Rodux
local RoactRodux = InGameMenuDependencies.RoactRodux
local UIBlox = InGameMenuDependencies.UIBlox
local InGameMenu = script.Parent.Parent.Parent
local Localization = require(InGameMenu.Localization.Localization)
local LocalizationProvider = require(InGameMenu.Localization.LocalizationProvider)
local reducer = require(InGameMenu.reducer)
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 MovementModeEntry = require(script.Parent.MovementModeEntry)
local UserGameSettings = UserSettings():GetService("UserGameSettings")
local localPlayer = Players.LocalPlayer
it("should create and destroy without errors", function()
local element = Roact.createElement(RoactRodux.StoreProvider, {
store = Rodux.Store.new(reducer)
}, {
ThemeProvider = Roact.createElement(UIBlox.Core.Style.Provider, {
style = appStyle,
}, {
LocalizationProvider = Roact.createElement(LocalizationProvider, {
localization = Localization.new("en-us"),
}, {
MovementModeEntry = Roact.createElement(MovementModeEntry, {
LayoutOrder = 2,
}),
}),
}),
})
local instance = Roact.mount(element)
UserGameSettings.ComputerMovementMode = Enum.ComputerMovementMode.ClickToMove
localPlayer.DevComputerMovementMode = Enum.DevComputerMovementMode.ClickToMove
Roact.unmount(instance)
end)
end
@@ -0,0 +1,61 @@
local CorePackages = game:GetService("CorePackages")
local InGameMenuDependencies = require(CorePackages.InGameMenuDependencies)
local Roact = InGameMenuDependencies.Roact
local t = InGameMenuDependencies.t
local InGameMenu = script.Parent.Parent.Parent
local GlobalConfig = require(InGameMenu.GlobalConfig)
local InputLabel = require(script.Parent.InputLabel)
local SliderWithInput = require(InGameMenu.Components.SliderWithInput)
local validateProps = t.strictInterface({
LayoutOrder = t.integer,
labelKey = t.string,
min = t.number,
max = t.number,
stepInterval = t.number,
keyboardInputStepInterval = t.optional(t.numberPositive),
value = t.number,
disabled = t.optional(t.boolean),
valueChanged = t.callback,
})
local function SliderEntry(props)
if GlobalConfig.propValidation then
assert(validateProps(props))
end
return Roact.createElement("Frame", {
BackgroundTransparency = 1,
Size = UDim2.new(1, 0, 0, 92),
LayoutOrder = props.LayoutOrder,
}, {
Padding = Roact.createElement("UIPadding", {
PaddingLeft = UDim.new(0, 24),
PaddingTop = UDim.new(0, 12),
PaddingRight = UDim.new(0, 30),
PaddingBottom = UDim.new(0, 24),
}),
Label = Roact.createElement(InputLabel, {
Size = UDim2.new(1, 0, 0, 24),
localizationKey = props.labelKey,
Position = UDim2.new(0, 0, 0, 0),
AnchorPoint = Vector2.new(0, 0),
}),
Slider = Roact.createElement(SliderWithInput, {
min = props.min,
max = props.max,
stepInterval = props.stepInterval,
keyboardInputStepInterval = props.keyboardInputStepInterval,
value = props.value,
Position = UDim2.new(0, 0, 0, 32),
disabled = props.disabled,
valueChanged = props.valueChanged,
}),
})
end
return SliderEntry
@@ -0,0 +1,55 @@
return function()
local CorePackages = game:GetService("CorePackages")
local InGameMenuDependencies = require(CorePackages.InGameMenuDependencies)
local Roact = InGameMenuDependencies.Roact
local Rodux = InGameMenuDependencies.Rodux
local RoactRodux = InGameMenuDependencies.RoactRodux
local UIBlox = InGameMenuDependencies.UIBlox
local InGameMenu = script.Parent.Parent.Parent
local Localization = require(InGameMenu.Localization.Localization)
local LocalizationProvider = require(InGameMenu.Localization.LocalizationProvider)
local reducer = require(InGameMenu.reducer)
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 SliderEntry = require(script.Parent.SliderEntry)
it("should create and destroy without errors", function()
local element = Roact.createElement(RoactRodux.StoreProvider, {
store = Rodux.Store.new(reducer)
}, {
ThemeProvider = Roact.createElement(UIBlox.Core.Style.Provider, {
style = appStyle,
}, {
LocalizationProvider = Roact.createElement(LocalizationProvider, {
localization = Localization.new("en-us"),
}, {
SliderEntry = Roact.createElement(SliderEntry, {
LayoutOrder = 2,
labelKey = "CoreScripts.InGameMenu.GameSettings.CameraSensitivity",
min = 1,
max = 10,
stepInterval = 1,
value = 5,
disabled = false,
valueChanged = function()
print("Value changed")
end,
}),
}),
}),
})
local instance = Roact.mount(element)
Roact.unmount(instance)
end)
end
@@ -0,0 +1,75 @@
local CorePackages = game:GetService("CorePackages")
local InGameMenuDependencies = require(CorePackages.InGameMenuDependencies)
local Roact = InGameMenuDependencies.Roact
local t = InGameMenuDependencies.t
local InGameMenu = script.Parent.Parent.Parent
local GlobalConfig = require(InGameMenu.GlobalConfig)
local ToggleSwitch = require(InGameMenu.Components.ToggleSwitch)
local InputLabel = require(script.Parent.InputLabel)
local DeveloperLockLabel = require(script.Parent.DeveloperLockLabel)
local ThemedTextLabel = require(InGameMenu.Components.ThemedTextLabel)
local withLocalization = require(InGameMenu.Localization.withLocalization)
local validateProps = t.strictInterface({
LayoutOrder = t.integer,
labelKey = t.string,
lockedToOff = t.optional(t.boolean),
subtextEnabled = t.optional(t.boolean),
subtextKey = t.optional(t.string),
checked = t.boolean,
onToggled = t.callback,
})
local function ToggleEntry(props)
if GlobalConfig.propValidation then
assert(validateProps(props))
end
return Roact.createElement("Frame", {
BackgroundTransparency = 1,
LayoutOrder = props.LayoutOrder,
Size = UDim2.new(1, 0, 0, 56),
}, {
Padding = Roact.createElement("UIPadding", {
PaddingRight = UDim.new(0, 30),
PaddingLeft = UDim.new(0, 24),
}),
ControlLabel = Roact.createElement(InputLabel, {
localizationKey = props.labelKey,
}),
LockedLabel = props.lockedToOff and Roact.createElement(DeveloperLockLabel, {
Size = UDim2.new(1, -72, 0.25, 0),
Position = UDim2.new(0, 0, 1, 0),
AnchorPoint = Vector2.new(0, 1),
}),
SubtextLabel = props.subtextEnabled and withLocalization({
text = props.subtextKey,
})(function(localization)
return Roact.createElement(ThemedTextLabel, {
Text = localization.text,
themeKey = "TextDefault",
fontKey = "CaptionBody",
TextTransparency = 0.5,
Size = UDim2.new(1, -72, 0.25, 0),
Position = UDim2.new(0, 0, 1, 0),
AnchorPoint = Vector2.new(0, 1),
TextXAlignment = Enum.TextXAlignment.Left,
TextYAlignment = Enum.TextYAlignment.Bottom,
})
end),
Toggle = Roact.createElement(ToggleSwitch, {
Position = UDim2.new(1, 0, 0.5, 0),
AnchorPoint = Vector2.new(1, 0.5),
checked = props.checked and not props.lockedToOff,
onToggled = props.onToggled,
disabled = props.lockedToOff,
}),
})
end
return ToggleEntry
@@ -0,0 +1,241 @@
local LATEST_PLACE_VERSION = "CoreScripts.InGameMenu.GameSettings.PlaceVersionWithLatest"
local PLACE_VERSION = "CoreScripts.InGameMenu.GameSettings.PlaceVersion"
local ContentProvider = game:GetService("ContentProvider")
local CoreGui = game:GetService("CoreGui")
local CorePackages = game:GetService("CorePackages")
local HttpRbxApiService = game:GetService("HttpRbxApiService")
local HttpService = game:GetService("HttpService")
local Players = game:GetService("Players")
local RobloxReplicatedStorage = game:GetService("RobloxReplicatedStorage")
local RunService = game:GetService("RunService")
local StarterPlayer = game:GetService("StarterPlayer")
local InGameMenuDependencies = require(CorePackages.InGameMenuDependencies)
local Roact = InGameMenuDependencies.Roact
local t = InGameMenuDependencies.t
local UIBlox = InGameMenuDependencies.UIBlox
local FFlagChinaLicensingApp = game:GetFastFlag("ChinaLicensingApp")
local PlayerPermissions = require(CoreGui.RobloxGui.Modules.PlayerPermissionsModule)
local InGameMenu = script.Parent.Parent.Parent
local Divider = require(InGameMenu.Components.Divider)
local ThemedTextLabel = require(InGameMenu.Components.ThemedTextLabel)
local ExternalEventConnection = require(InGameMenu.Utility.ExternalEventConnection)
local withLocalization = require(InGameMenu.Localization.withLocalization)
local canGetCoreScriptVersion = game:getEngineFeature("CoreScriptVersionEnabled")
local VersionReporter = Roact.PureComponent:extend("VersionReporter")
VersionReporter.validateProps = t.strictInterface({
LayoutOrder = t.integer,
})
local function inferPlayerScriptStatus(starterPlayerScripts)
local loader = starterPlayerScripts:FindFirstChild("PlayerScriptsLoader")
local playerModule = starterPlayerScripts:FindFirstChild("PlayerModule")
if loader ~= nil and playerModule ~= nil then
if not playerModule.Archivable and not loader.Archivable then
return "PlayerScript Status: Default"
else
return "PlayerScript Status: Possibly Custom"
end
end
local cameraScript = starterPlayerScripts:FindFirstChild("CameraScript")
local controlScript = starterPlayerScripts:FindFirstChild("ControlScript")
if cameraScript ~= nil or controlScript ~= nil then
return "PlayerScript Status: Custom Old"
else
return "PlayerScript Status: Custom"
end
end
function VersionReporter:init()
local initalPlayerscriptStatus = "PlayerScript Status: Unknown"
local starterPlayerScripts = StarterPlayer:FindFirstChild("StarterPlayerScripts")
if starterPlayerScripts then
initalPlayerscriptStatus = inferPlayerScriptStatus(starterPlayerScripts)
else
self.playerScriptsAddedConnection = StarterPlayer.ChildAdded:Connect(function(child)
if child.Name == "StarterPlayerScripts" then
self.playerScriptsAddedConnection:Disconnect()
self.playerScriptsAddedConnection = nil
self:setState({
playerScriptStatus = inferPlayerScriptStatus(child),
})
end
end)
end
self:setState({
baseUrl = ContentProvider.BaseUrl,
clientVersion = RunService:GetRobloxVersion(),
isAdmin = false,
latestPlaceVersion = nil,
placeVersion = game.PlaceVersion,
serverVersion = nil,
playerScriptStatus = initalPlayerscriptStatus,
coreScriptVersion = canGetCoreScriptVersion and RunService:GetCoreScriptVersion() or nil,
})
end
function VersionReporter:render()
local haveServerVersion = self.state.serverVersion ~= nil
local haveLatestPlaceVersion = self.state.latestPlaceVersion ~= nil
local isTestEnvironment = not self.state.baseUrl:find("www.roblox.com")
local showEnvironment = isTestEnvironment and not FFlagChinaLicensingApp
local showPlayerScriptStatus = (isTestEnvironment or self.state.isAdmin) and not FFlagChinaLicensingApp
return withLocalization({
clientVersion = {
"CoreScripts.InGameMenu.GameSettings.ClientVersion",
CLIENT_VERSION = self.state.clientVersion,
},
serverVersion = {
"CoreScripts.InGameMenu.GameSettings.ServerVersion",
SERVER_VERSION = self.state.serverVersion,
},
placeVersion = {
haveLatestPlaceVersion and LATEST_PLACE_VERSION or PLACE_VERSION,
PLACE_VERSION = self.state.placeVersion,
LATEST_PLACE_VERSION = self.state.latestPlaceVersion,
},
coreScriptVersion = canGetCoreScriptVersion and {
"CoreScripts.InGameMenu.GameSettings.ClientCoreScriptVersion",
CLIENT_CORESCRIPT_VERSION = self.state.coreScriptVersion,
} or nil
})(function(localized)
return Roact.createElement("Frame", {
BackgroundTransparency = 1,
Size = UDim2.new(1, 0, 0, 80),
LayoutOrder = self.props.LayoutOrder,
}, {
Padding = Roact.createElement("UIPadding", {
PaddingTop = UDim.new(0, 14),
PaddingLeft = UDim.new(0, 24),
PaddingBottom = UDim.new(0, 14),
}),
Layout = Roact.createElement("UIListLayout", {
SortOrder = Enum.SortOrder.LayoutOrder,
Padding = UDim.new(0, 10),
}),
ClientVersion = Roact.createElement(ThemedTextLabel, {
themeKey = "TextDefault",
fontKey = "Footer",
Text = localized.clientVersion,
LayoutOrder = 1,
Size = UDim2.new(1, -24, 0, 14),
}),
ServerVersion = Roact.createElement(ThemedTextLabel, {
themeKey = "TextDefault",
fontKey = "Footer",
Text = haveServerVersion and localized.serverVersion or "",
LayoutOrder = 2,
Size = UDim2.new(1, -24, 0, 14),
}, {
not haveServerVersion and Roact.createElement(UIBlox.Loading.ShimmerPanel, {
Size = UDim2.new(1, 0, 1, 0),
shimmerSpeed = 2,
}),
}),
PlaceVersion = Roact.createElement(ThemedTextLabel, {
themeKey = "TextDefault",
fontKey = "Footer",
Text = localized.placeVersion,
LayoutOrder = 3,
Size = UDim2.new(1, -24, 0, 14),
}),
Divider = (showEnvironment or showPlayerScriptStatus) and Roact.createElement(Divider, {
Size = UDim2.new(1, 0, 0, 1),
LayoutOrder = 4,
}),
PlayerScriptStatus = showPlayerScriptStatus and Roact.createElement(ThemedTextLabel, {
themeKey = "TextDefault",
fontKey = "Footer",
Text = self.state.playerScriptStatus,
LayoutOrder = 5,
Size = UDim2.new(1, -24, 0, 14),
}),
Environment = showEnvironment and Roact.createElement(ThemedTextLabel, {
themeKey = "TextDefault",
fontKey = "Footer",
Text = ("Base URL: %s"):format(self.state.baseUrl),
LayoutOrder = 6,
Size = UDim2.new(1, -24, 0, 14),
}),
Divider2 = canGetCoreScriptVersion and Roact.createElement(Divider, {
Size = UDim2.new(1, 0, 0, 1),
LayoutOrder = 7,
}),
CoreScriptVersion = canGetCoreScriptVersion and Roact.createElement(ThemedTextLabel, {
themeKey = "TextDefault",
fontKey = "Footer",
Text = localized.coreScriptVersion,
LayoutOrder = 8,
Size = UDim2.new(1, -24, 0, 14),
}),
VersionWatcher = Roact.createElement(ExternalEventConnection, {
event = game:GetPropertyChangedSignal("PlaceVersion"),
callback = function()
self:setState({
placeVersion = game.PlaceVersion,
})
end,
})
})
end)
end
function VersionReporter:didMount()
self.mounted = true
coroutine.wrap(function()
local serverVersionRemote = RobloxReplicatedStorage:WaitForChild("GetServerVersion", math.huge)
local version = serverVersionRemote:InvokeServer()
if self.mounted then
self:setState({
serverVersion = version,
})
end
end)()
coroutine.wrap(function()
pcall(function()
local json = HttpRbxApiService:GetAsync(string.format("assets/%d/versions", game.PlaceId))
local versionData = HttpService:JSONDecode(json)
local latestVersion = versionData[1].VersionNumber
if self.mounted then
self:setState({
latestPlaceVersion = latestVersion,
})
end
end)
end)()
coroutine.wrap(function()
local isAdmin = PlayerPermissions.IsPlayerAdminAsync(Players.LocalPlayer)
if self.mounted then
self:setState({
isAdmin = isAdmin,
})
end
end)()
end
function VersionReporter:willUnmount()
self.mounted = false
if self.playerScriptsAddedConnection then
self.playerScriptsAddedConnection:Disconnect()
end
end
return VersionReporter
@@ -0,0 +1,67 @@
local CoreGui = game:GetService("CoreGui")
local CorePackages = game:GetService("CorePackages")
local InGameMenuDependencies = require(CorePackages.InGameMenuDependencies)
local Roact = InGameMenuDependencies.Roact
local t = InGameMenuDependencies.t
local UserGameSettings = UserSettings():GetService("UserGameSettings")
local MasterVolumeChanged = UserGameSettings:GetPropertyChangedSignal("MasterVolume")
local InGameMenu = script.Parent.Parent.Parent
local ExternalEventConnection = require(InGameMenu.Utility.ExternalEventConnection)
local SliderEntry = require(script.Parent.SliderEntry)
local SendAnalytics = require(InGameMenu.Utility.SendAnalytics)
local Constants = require(InGameMenu.Resources.Constants)
if CoreGui.RobloxGui:FindFirstChild("Sounds") == nil then
local folder = Instance.new("Folder")
folder.Name = "Sounds"
folder.Parent = CoreGui.RobloxGui
end
local volumeTest = Instance.new("Sound", CoreGui.RobloxGui.Sounds)
volumeTest.Name = "VolumeChangeSound"
volumeTest.Volume = 1
volumeTest.SoundId = "rbxasset://sounds/uuhhh.mp3"
local VolumeEntry = Roact.PureComponent:extend("VolumeEntry")
VolumeEntry.validateProps = t.strictInterface({
LayoutOrder = t.integer,
})
function VolumeEntry:init()
self.onVolumeChanged = function()
self:setState({
volume = math.floor(UserGameSettings.MasterVolume * 10 + 0.5)
})
end
self.onVolumeChanged()
end
function VolumeEntry:render()
return Roact.createFragment({
VolumeSlider = Roact.createElement(SliderEntry, {
labelKey = "CoreScripts.InGameMenu.GameSettings.Volume",
LayoutOrder = self.props.LayoutOrder,
min = 0,
max = 10,
stepInterval = 1,
value = self.state.volume,
valueChanged = function(newValue)
UserGameSettings.MasterVolume = newValue / 10
volumeTest:Play()
SendAnalytics(Constants.AnalyticsSettingsChangeName, nil, {}, true)
end,
}),
VolumeListener = Roact.createElement(ExternalEventConnection, {
event = MasterVolumeChanged,
callback = self.onVolumeChanged,
}),
})
end
return VolumeEntry
@@ -0,0 +1,167 @@
local TextService = game:GetService("TextService")
local CorePackages = game:GetService("CorePackages")
local ContextActionService = game:GetService("ContextActionService")
local InGameMenuDependencies = require(CorePackages.InGameMenuDependencies)
local Roact = InGameMenuDependencies.Roact
local UIBlox = InGameMenuDependencies.UIBlox
local t = InGameMenuDependencies.t
local withStyle = UIBlox.Core.Style.withStyle
local InGameMenu = script.Parent.Parent
local Assets = require(InGameMenu.Resources.Assets)
local ThemedTextLabel = require(script.Parent.ThemedTextLabel)
local Divider = require(script.Parent.Divider)
local ImageSetLabel = UIBlox.Core.ImageSet.Label
local DIALOG_WIDTH = 312
local DIALOG_PADDING = 24
local MODAL_DISMISS_ACTION = "InGameMenuModalDismiss"
local InfoDialog = Roact.PureComponent:extend("InfoDialog")
InfoDialog.validateProps = t.strictInterface({
bodyText = t.string,
dismissText = t.string,
titleText = t.string,
iconImage = t.union(t.string, t.table),
onDismiss = t.callback,
visible = t.boolean,
})
function InfoDialog:render()
local props = self.props
return withStyle(function(style)
local bodyFont = style.Font.Body.Font
local bodyFontSize = style.Font.Body.RelativeSize * style.Font.BaseSize
local bodyText = props.bodyText
local textHeight = TextService:GetTextSize(
bodyText,
bodyFontSize,
bodyFont,
Vector2.new(DIALOG_WIDTH - DIALOG_PADDING * 2, math.huge)
).Y
-- 24px padding top, 24px padding bottom
-- Minimum height of two lines of text.
-- TODO: Line height!
local bodyTextContainerHeight = DIALOG_PADDING * 2 + math.max(textHeight, bodyFontSize * 2)
return Roact.createElement("Frame", {
Size = UDim2.new(1, 0, 1, 0),
BackgroundTransparency = 1,
ZIndex = 8,
Visible = props.visible,
}, {
Overlay = Roact.createElement("TextButton", {
AutoButtonColor = false,
BackgroundColor3 = style.Theme.Overlay.Color,
BackgroundTransparency = style.Theme.Overlay.Transparency,
BorderSizePixel = 0,
Size = UDim2.new(1, 0, 1, 0),
Text = "",
}),
DialogMainFrame = Roact.createElement(ImageSetLabel, {
AnchorPoint = Vector2.new(0.5, 0.5),
BackgroundTransparency = 1,
Image = Assets.Images.RoundedRect.Image,
ImageColor3 = style.Theme.BackgroundUIDefault.Color,
ImageTransparency = style.Theme.BackgroundUIDefault.Transparency,
Position = UDim2.new(0.5, 0, 0.5, 0),
ScaleType = Assets.Images.RoundedRect.ScaleType,
-- button (36px), bottom padding (24px), top padding (10px)
-- height: title bar (48px), divider (1px), body text height, icon(36px)
Size = UDim2.new(0, DIALOG_WIDTH, 0, 48 + 1 + bodyTextContainerHeight + 36 + 36 + DIALOG_PADDING + 10),
SliceCenter = Assets.Images.RoundedRect.SliceCenter,
}, {
Padding = Roact.createElement("UIPadding", {
PaddingTop = UDim.new(0, 10),
PaddingBottom = UDim.new(0, DIALOG_PADDING),
PaddingLeft = UDim.new(0, DIALOG_PADDING),
PaddingRight = UDim.new(0, DIALOG_PADDING),
}),
Layout = Roact.createElement("UIListLayout", {
FillDirection = Enum.FillDirection.Vertical,
HorizontalAlignment = Enum.HorizontalAlignment.Center,
SortOrder = Enum.SortOrder.LayoutOrder,
}),
Icon = Roact.createElement(ImageSetLabel, {
Size = UDim2.new(0, 36, 0, 36),
BackgroundTransparency = 1,
Image = self.props.iconImage,
LayoutOrder = 1,
}),
Title = Roact.createElement(ThemedTextLabel, {
fontKey = "Header1",
themeKey = "TextEmphasis",
LayoutOrder = 2,
Size = UDim2.new(1, 0, 0, 48),
Text = props.titleText,
}),
Divider = Roact.createElement(Divider, {
LayoutOrder = 3,
}),
BodyTextContainer = Roact.createElement("Frame", {
BackgroundTransparency = 1,
LayoutOrder = 4,
Size = UDim2.new(1, 0, 0, bodyTextContainerHeight),
}, {
BodyText = Roact.createElement(ThemedTextLabel, {
Size = UDim2.new(1, 0, 1, 0),
Text = props.bodyText,
TextWrapped = true,
})
}),
ConfirmButton = Roact.createElement(UIBlox.App.Button.PrimarySystemButton, {
layoutOrder = 5,
size = UDim2.new(1, 0, 0, 36),
onActivated = props.onDismiss,
text = props.dismissText,
}),
}),
})
end)
end
function InfoDialog:bindActions()
local function dismissFunc(actionName, inputState, input)
if inputState == Enum.UserInputState.Begin then
self.props.onDismiss()
end
end
ContextActionService:BindCoreAction(
MODAL_DISMISS_ACTION, dismissFunc, false, Enum.KeyCode.Escape)
end
function InfoDialog:unbindActions()
ContextActionService:UnbindCoreAction(MODAL_DISMISS_ACTION)
end
function InfoDialog:didMount()
if self.props.visible then
self:bindActions()
end
end
function InfoDialog:didUpdate()
if self.props.visible then
self:bindActions()
else
self:unbindActions()
end
end
function InfoDialog:willUnmount()
self:unbindActions()
end
return InfoDialog
@@ -0,0 +1,47 @@
return function()
local CorePackages = game:GetService("CorePackages")
local InGameMenuDependencies = require(CorePackages.InGameMenuDependencies)
local Roact = InGameMenuDependencies.Roact
local RoactRodux = InGameMenuDependencies.RoactRodux
local Rodux = InGameMenuDependencies.Rodux
local UIBlox = InGameMenuDependencies.UIBlox
local InGameMenu = script.Parent.Parent
local reducer = require(InGameMenu.reducer)
local AppDarkTheme = require(CorePackages.AppTempCommon.LuaApp.Style.Themes.DarkTheme)
local AppFont = require(CorePackages.AppTempCommon.LuaApp.Style.Fonts.Gotham)
local InfoDialog = require(script.Parent.InfoDialog)
local appStyle = {
Theme = AppDarkTheme,
Font = AppFont,
}
it("should create and destroy without errors", function()
local element = Roact.createElement(RoactRodux.StoreProvider, {
store = Rodux.Store.new(reducer)
}, {
ThemeProvider = Roact.createElement(UIBlox.Core.Style.Provider, {
style = appStyle,
}, {
GameIconHeader = Roact.createElement(InfoDialog, {
bodyText = "Hello world!",
dismissText = "Okay",
titleText = "Title",
iconImage = "",
onDismiss = function()
print("on dismiss")
end,
visible = true,
}),
}),
})
local instance = Roact.mount(element)
Roact.unmount(instance)
end)
end
@@ -0,0 +1,111 @@
local CorePackages = game:GetService("CorePackages")
local TextService = game:GetService("TextService")
local InGameMenuDependencies = require(CorePackages.InGameMenuDependencies)
local Roact = InGameMenuDependencies.Roact
local RoactRodux = InGameMenuDependencies.RoactRodux
local t = InGameMenuDependencies.t
local UIBlox = InGameMenuDependencies.UIBlox
local withStyle = UIBlox.Core.Style.withStyle
local InGameMenu = script.Parent.Parent.Parent
local GlobalConfig = require(InGameMenu.GlobalConfig)
local withLocalization = require(InGameMenu.Localization.withLocalization)
local Assets = require(InGameMenu.Resources.Assets)
local ThemedTextLabel = require(InGameMenu.Components.ThemedTextLabel)
local SetCurrentPage = require(InGameMenu.Actions.SetCurrentPage)
local ImageSetLabel = UIBlox.Core.ImageSet.Label
local CONTAINER_WIDTH = 304
local TEXT_PADDING_TOP = 4
local TEXT_PADDING_BOTTOM = 16
local validateProps = t.strictInterface({
switchToPlayers = t.callback,
})
local function AddFriendsNow(props)
if GlobalConfig.propValidation then
assert(validateProps(props))
end
return withStyle(function(style)
return withLocalization({
noFriendsText = "CoreScripts.InGameMenu.InviteFriends.NoFriends",
makeFriendsNow = "CoreScripts.InGameMenu.InviteFriends.MakeFriendsNow",
})(function(localized)
local bodyFont = style.Font.Body.Font
local bodyFontSize = style.Font.Body.RelativeSize * style.Font.BaseSize
local textHeight = TextService:GetTextSize(
localized.noFriendsText,
bodyFontSize,
bodyFont,
Vector2.new(CONTAINER_WIDTH, math.huge)
).Y
local totalTextPadding = TEXT_PADDING_TOP + TEXT_PADDING_BOTTOM
return Roact.createElement("Frame", {
BackgroundTransparency = 1,
Size = UDim2.new(0, CONTAINER_WIDTH, 1, 0),
Position = UDim2.new(0.5, 0, 0, 0),
AnchorPoint = Vector2.new(0.5, 0),
}, {
Layout = Roact.createElement("UIListLayout", {
FillDirection = Enum.FillDirection.Vertical,
HorizontalAlignment = Enum.HorizontalAlignment.Center,
VerticalAlignment = Enum.VerticalAlignment.Center,
SortOrder = Enum.SortOrder.LayoutOrder,
}),
Icon = Roact.createElement(ImageSetLabel, {
BackgroundTransparency = 1,
Image = Assets.Images.AddFriend,
Size = UDim2.new(0, 64, 0, 64),
ImageTransparency = 0.5,
LayoutOrder = 1,
}),
TextContainer = Roact.createElement("Frame", {
BackgroundTransparency = 1,
Size = UDim2.new(1, 0, 0, textHeight + totalTextPadding),
LayoutOrder = 2,
}, {
Text = Roact.createElement(ThemedTextLabel, {
fontKey = "Body",
themeKey = "TextEmphasis",
Text = localized.noFriendsText,
Position = UDim2.new(0, 0, 0, TEXT_PADDING_TOP),
Size = UDim2.new(1, 0, 1, -totalTextPadding),
TextXAlignment = Enum.TextXAlignment.Center,
TextWrapped = true,
})
}),
MakeFriendsButton = Roact.createElement(UIBlox.App.Button.SecondaryButton, {
layoutOrder = 3,
size = UDim2.new(1, 0, 0, 48),
text = localized.makeFriendsNow,
onActivated = props.switchToPlayers,
}),
})
end)
end)
end
return RoactRodux.UNSTABLE_connect2(
nil,
function(dispatch)
return {
switchToPlayers = function()
dispatch(SetCurrentPage("Players"))
end
}
end
)(AddFriendsNow)
@@ -0,0 +1,44 @@
return function()
local CorePackages = game:GetService("CorePackages")
local InGameMenuDependencies = require(CorePackages.InGameMenuDependencies)
local Roact = InGameMenuDependencies.Roact
local Rodux = InGameMenuDependencies.Rodux
local RoactRodux = InGameMenuDependencies.RoactRodux
local UIBlox = InGameMenuDependencies.UIBlox
local InGameMenu = script.Parent.Parent.Parent
local Localization = require(InGameMenu.Localization.Localization)
local LocalizationProvider = require(InGameMenu.Localization.LocalizationProvider)
local reducer = require(InGameMenu.reducer)
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 AddFriendsNow = require(script.Parent.AddFriendsNow)
it("should create and destroy without errors", function()
local element = Roact.createElement(RoactRodux.StoreProvider, {
store = Rodux.Store.new(reducer)
}, {
ThemeProvider = Roact.createElement(UIBlox.Core.Style.Provider, {
style = appStyle,
}, {
LocalizationProvider = Roact.createElement(LocalizationProvider, {
localization = Localization.new("en-us"),
}, {
AddFriendsNow = Roact.createElement(AddFriendsNow),
}),
}),
})
local instance = Roact.mount(element)
Roact.unmount(instance)
end)
end
@@ -0,0 +1,191 @@
local CorePackages = game:GetService("CorePackages")
local InGameMenuDependencies = require(CorePackages.InGameMenuDependencies)
local Roact = InGameMenuDependencies.Roact
local RoactRodux = InGameMenuDependencies.RoactRodux
local t = InGameMenuDependencies.t
local Cryo = InGameMenuDependencies.Cryo
local InGameMenu = script.Parent.Parent.Parent
local FFlagInGameMenuInviteFriendsDisplayNameSearch =
require(InGameMenu.Flags.FFlagInGameMenuInviteFriendsDisplayNameSearch)
local getFFlagUseNewPlayerLabelDesign = require(InGameMenu.Flags.GetFFlagUseNewPlayerLabelDesign)
local fflagUseNewPlayerLabelDesign = getFFlagUseNewPlayerLabelDesign()
local PlayerLabel = fflagUseNewPlayerLabelDesign and require(InGameMenu.Components.PlayerLabelV2)
or require(InGameMenu.Components.PlayerLabel)
local Divider = require(InGameMenu.Components.Divider)
local SendInviteButton = require(script.Parent.SendInviteButton)
local SearchBox = require(script.Parent.SearchBox)
local BarOnTopScrollingFrame = require(InGameMenu.Components.BarOnTopScrollingFrame)
local PlayerSearchPredicate = require(InGameMenu.Utility.PlayerSearchPredicate)
local Constants = require(InGameMenu.Resources.Constants)
local InviteStatus = Constants.InviteStatus
local InviteUserToPlaceId = require(InGameMenu.Thunks.InviteUserToPlaceId)
local DIVIDER_INDENT = fflagUseNewPlayerLabelDesign and 104 or 70
local PLAYER_LABEL_HEIGHT = fflagUseNewPlayerLabelDesign and 71 or 70
local SEARCHBOX_HEIGHT = 36
local SEARCHBOX_TOP_PADDING = 8
local SEARCHBOX_BOTTOM_PADDING = 20
local PLACE_ID = tostring(game.PlaceId)
local InviteFriendsList = Roact.PureComponent:extend("InviteFriendsList")
InviteFriendsList.validateProps = t.strictInterface({
players = t.array(t.strictInterface({
IsOnline = t.boolean,
Id = t.integer,
Username = t.string,
DisplayName = t.string,
})),
invitesState = t.table,
dispatchInviteUserToPlaceId = t.callback,
})
local function sortPlayers(p1, p2)
if p1.IsOnline == p2.IsOnline then
return p1.Username:lower() < p2.Username:lower()
else
return p1.IsOnline and not p2.IsOnline
end
end
local function searchFilter(searchText, playerName, displayName)
if searchText == "" then
return true
end
return string.find(playerName:lower(), searchText:lower(), 1, true) ~= nil
end
function InviteFriendsList:init()
self.state = {
searchText = "",
}
self.inviteAnimationInProgress = {}
end
function InviteFriendsList:renderListEntries()
local sortedPlayers = Cryo.List.sort(self.props.players, sortPlayers)
local playersCount = #sortedPlayers
local layoutOrder = 1
local listComponents = {}
local visibleEntryCount = 0
listComponents.ListLayout = Roact.createElement("UIListLayout", {
SortOrder = Enum.SortOrder.LayoutOrder,
HorizontalAlignment = Enum.HorizontalAlignment.Right,
})
for index, playerInfo in pairs(sortedPlayers) do
local userId = tostring(playerInfo.Id)
local userInviteStatus = self.props.invitesState[userId]
local isEntryVisible
if FFlagInGameMenuInviteFriendsDisplayNameSearch then
isEntryVisible = PlayerSearchPredicate(self.state.searchText, playerInfo.Username, playerInfo.DisplayName)
else
isEntryVisible = searchFilter(self.state.searchText, playerInfo.Username, playerInfo.DisplayName)
end
if isEntryVisible then
visibleEntryCount = visibleEntryCount + 1
end
listComponents["friend_"..index] = Roact.createElement(PlayerLabel, {
username = playerInfo.Username,
displayName = fflagUseNewPlayerLabelDesign and playerInfo.DisplayName or nil,
userId = playerInfo.Id,
isOnline = playerInfo.IsOnline,
isSelected = false,
LayoutOrder = layoutOrder,
Visible = isEntryVisible,
onActivated = function()
local isPending = userInviteStatus == InviteStatus.Pending
local isSuccess = userInviteStatus == InviteStatus.Success
local inviteAnimationInProgress = self.inviteAnimationInProgress[playerInfo.Id]
if not (isPending or isSuccess or inviteAnimationInProgress) then
self.props.dispatchInviteUserToPlaceId(userId, PLACE_ID)
end
end
}, {
InviteButton = Roact.createElement(SendInviteButton, {
userInviteStatus = userInviteStatus,
onActivated = function()
self.props.dispatchInviteUserToPlaceId(userId, PLACE_ID)
end,
animationBegun = function()
self.inviteAnimationInProgress[playerInfo.Id] = true
end,
animationEnded = function()
self.inviteAnimationInProgress[playerInfo.Id] = nil
end,
})
})
layoutOrder = layoutOrder + 1
if index < playersCount then
listComponents["divider_" .. layoutOrder] = Roact.createElement(Divider, {
LayoutOrder = layoutOrder,
Size = UDim2.new(1, -DIVIDER_INDENT, 0, 1),
Visible = isEntryVisible,
})
layoutOrder = layoutOrder + 1
end
end
return listComponents, visibleEntryCount
end
function InviteFriendsList:render()
local listEntries, visibleEntryCount = self:renderListEntries()
local listOffset = SEARCHBOX_HEIGHT + SEARCHBOX_TOP_PADDING + SEARCHBOX_BOTTOM_PADDING
return Roact.createElement("Frame", {
BackgroundTransparency = 1,
Size = UDim2.new(1, 0, 1, 0),
}, {
SearchBox = Roact.createElement(SearchBox, {
Size = UDim2.new(1, -48, 0, SEARCHBOX_HEIGHT),
Position = UDim2.new(0.5, 0, 0, SEARCHBOX_TOP_PADDING),
AnchorPoint = Vector2.new(0.5, 0),
searchUpdated = function(newSearch)
self:setState({
searchText = newSearch,
})
end,
}),
List = Roact.createElement(BarOnTopScrollingFrame, {
Position = UDim2.new(0, 0, 0, listOffset),
Size = UDim2.new(1, 0, 1, -listOffset),
CanvasSize = UDim2.new(1, 0, 0, visibleEntryCount * (PLAYER_LABEL_HEIGHT + 1)),
scrollBarOffset = 4,
}, listEntries)
})
end
return RoactRodux.UNSTABLE_connect2(
function(state, props)
return {
invitesState = state.invites
}
end,
function(dispatch)
return {
dispatchInviteUserToPlaceId = function(userId, placeId)
dispatch(InviteUserToPlaceId(userId, placeId))
end
}
end
)(InviteFriendsList)
@@ -0,0 +1,103 @@
return function()
local CorePackages = game:GetService("CorePackages")
local InGameMenuDependencies = require(CorePackages.InGameMenuDependencies)
local Roact = InGameMenuDependencies.Roact
local Rodux = InGameMenuDependencies.Rodux
local RoactRodux = InGameMenuDependencies.RoactRodux
local UIBlox = InGameMenuDependencies.UIBlox
local InGameMenu = script.Parent.Parent.Parent
local Localization = require(InGameMenu.Localization.Localization)
local LocalizationProvider = require(InGameMenu.Localization.LocalizationProvider)
local reducer = require(InGameMenu.reducer)
local ReceivedUserInviteStatus = require(InGameMenu.Actions.ReceivedUserInviteStatus)
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 Constants = require(InGameMenu.Resources.Constants)
local InviteStatus = Constants.InviteStatus
local InviteFriendsList = require(script.Parent.InviteFriendsList)
it("should create and destroy without errors", function()
local element = Roact.createElement(RoactRodux.StoreProvider, {
store = Rodux.Store.new(reducer)
}, {
ThemeProvider = Roact.createElement(UIBlox.Core.Style.Provider, {
style = appStyle,
}, {
LocalizationProvider = Roact.createElement(LocalizationProvider, {
localization = Localization.new("en-us"),
}, {
InviteFriendsList = Roact.createElement(InviteFriendsList, {
players = {
{
Id = 2231221,
IsOnline = true,
Username = "TheGamer101",
DisplayName = "TestDisplayName",
},
{
Id = 261,
IsOnline = false,
Username = "Shedletsky",
DisplayName = "TestDisplayName",
},
}
}),
}),
}),
})
local instance = Roact.mount(element)
Roact.unmount(instance)
end)
it("should not throw erros when invite state is updated", function()
local store = Rodux.Store.new(reducer)
local element = Roact.createElement(RoactRodux.StoreProvider, {
store = store
}, {
ThemeProvider = Roact.createElement(UIBlox.Core.Style.Provider, {
style = appStyle,
}, {
LocalizationProvider = Roact.createElement(LocalizationProvider, {
localization = Localization.new("en-us"),
}, {
InviteFriendsList = Roact.createElement(InviteFriendsList, {
players = {
{
Id = 2231221,
IsOnline = true,
Username = "TheGamer101",
DisplayName = "TestDisplayName",
},
{
Id = 261,
IsOnline = false,
Username = "Shedletsky",
DisplayName = "TestDisplayName",
},
}
}),
}),
}),
})
local instance = Roact.mount(element)
store:dispatch(ReceivedUserInviteStatus(261, InviteStatus.Failed))
store:flush()
Roact.unmount(instance)
end)
end
@@ -0,0 +1,98 @@
local CorePackages = game:GetService("CorePackages")
local TextService = game:GetService("TextService")
local InGameMenuDependencies = require(CorePackages.InGameMenuDependencies)
local Roact = InGameMenuDependencies.Roact
local t = InGameMenuDependencies.t
local UIBlox = InGameMenuDependencies.UIBlox
local withStyle = UIBlox.Core.Style.withStyle
local InGameMenu = script.Parent.Parent.Parent
local GlobalConfig = require(InGameMenu.GlobalConfig)
local withLocalization = require(InGameMenu.Localization.withLocalization)
local Assets = require(InGameMenu.Resources.Assets)
local ThemedTextLabel = require(InGameMenu.Components.ThemedTextLabel)
local ImageSetLabel = UIBlox.Core.ImageSet.Label
local CONTAINER_WIDTH = 304
local TEXT_PADDING_TOP = 10
local TEXT_PADDING_BOTTOM = 26
local validateProps = t.strictInterface({
onRetry = t.callback,
})
local function LoadingFriendsError(props)
if GlobalConfig.propValidation then
assert(validateProps(props))
end
return withStyle(function(style)
return withLocalization({
errorOccured = "CoreScripts.InGameMenu.InviteFriends.ErrorOccured",
})(function(localized)
local bodyFont = style.Font.Body.Font
local bodyFontSize = style.Font.Body.RelativeSize * style.Font.BaseSize
local textHeight = TextService:GetTextSize(
localized.errorOccured,
bodyFontSize,
bodyFont,
Vector2.new(CONTAINER_WIDTH, math.huge)
).Y
local totalTextPadding = TEXT_PADDING_TOP + TEXT_PADDING_BOTTOM
return Roact.createElement("Frame", {
BackgroundTransparency = 1,
Size = UDim2.new(0, CONTAINER_WIDTH, 1, 0),
Position = UDim2.new(0.5, 0, 0, 0),
AnchorPoint = Vector2.new(0.5, 0),
}, {
Layout = Roact.createElement("UIListLayout", {
FillDirection = Enum.FillDirection.Vertical,
HorizontalAlignment = Enum.HorizontalAlignment.Center,
VerticalAlignment = Enum.VerticalAlignment.Center,
SortOrder = Enum.SortOrder.LayoutOrder,
}),
Icon = Roact.createElement(ImageSetLabel, {
BackgroundTransparency = 1,
Image = Assets.Images.Friends,
Size = UDim2.new(0, 64, 0, 64),
ImageTransparency = 0.5,
LayoutOrder = 1,
}),
TextContainer = Roact.createElement("Frame", {
BackgroundTransparency = 1,
Size = UDim2.new(1, 0, 0, textHeight + totalTextPadding),
LayoutOrder = 2,
}, {
Text = Roact.createElement(ThemedTextLabel, {
fontKey = "Body",
themeKey = "TextEmphasis",
Text = localized.errorOccured,
Position = UDim2.new(0, 0, 0, TEXT_PADDING_TOP),
Size = UDim2.new(1, 0, 1, -totalTextPadding),
TextXAlignment = Enum.TextXAlignment.Center,
TextWrapped = true,
})
}),
MakeFriendsButton = Roact.createElement(UIBlox.App.Button.SecondaryButton, {
layoutOrder = 3,
size = UDim2.new(1, 0, 0, 48),
icon = Assets.Images.RetryIcon,
onActivated = props.onRetry,
}),
})
end)
end)
end
return LoadingFriendsError
@@ -0,0 +1,48 @@
return function()
local CorePackages = game:GetService("CorePackages")
local InGameMenuDependencies = require(CorePackages.InGameMenuDependencies)
local Roact = InGameMenuDependencies.Roact
local Rodux = InGameMenuDependencies.Rodux
local RoactRodux = InGameMenuDependencies.RoactRodux
local UIBlox = InGameMenuDependencies.UIBlox
local InGameMenu = script.Parent.Parent.Parent
local Localization = require(InGameMenu.Localization.Localization)
local LocalizationProvider = require(InGameMenu.Localization.LocalizationProvider)
local reducer = require(InGameMenu.reducer)
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 LoadingFriendsError = require(script.Parent.LoadingFriendsError)
it("should create and destroy without errors", function()
local element = Roact.createElement(RoactRodux.StoreProvider, {
store = Rodux.Store.new(reducer)
}, {
ThemeProvider = Roact.createElement(UIBlox.Core.Style.Provider, {
style = appStyle,
}, {
LocalizationProvider = Roact.createElement(LocalizationProvider, {
localization = Localization.new("en-us"),
}, {
LoadingFriendsError = Roact.createElement(LoadingFriendsError, {
onRetry = function()
print("On retry")
end,
}),
}),
}),
})
local instance = Roact.mount(element)
Roact.unmount(instance)
end)
end
@@ -0,0 +1,135 @@
local CorePackages = game:GetService("CorePackages")
local InGameMenuDependencies = require(CorePackages.InGameMenuDependencies)
local Roact = InGameMenuDependencies.Roact
local t = InGameMenuDependencies.t
local UIBlox = InGameMenuDependencies.UIBlox
local withStyle = UIBlox.Core.Style.withStyle
local InGameMenu = script.Parent.Parent.Parent
local TEXT_BOX_OFFSET = 44
local Assets = require(InGameMenu.Resources.Assets)
local withLocalization = require(InGameMenu.Localization.withLocalization)
local ThemedTextLabel = require(InGameMenu.Components.ThemedTextLabel)
local ImageSetLabel = UIBlox.Core.ImageSet.Label
local ImageSetButton = UIBlox.Core.ImageSet.Button
local SearchBox = Roact.PureComponent:extend("SearchBox")
SearchBox.validateProps = t.strictInterface({
Size = t.UDim2,
Position = t.UDim2,
AnchorPoint = t.Vector2,
searchUpdated = t.callback,
})
function SearchBox:init()
self.state = {
searchText = "",
}
end
function SearchBox:render()
return withLocalization({
searchPlaceholder = "CoreScripts.InGameMenu.InviteFriends.Search",
})(function(localized)
return withStyle(function(style)
local textTheme = style.Theme.TextEmphasis
local textFont = style.Font.Body
return Roact.createElement(ImageSetLabel, {
BackgroundTransparency = 1,
Size = self.props.Size,
Position = self.props.Position,
AnchorPoint = self.props.AnchorPoint,
Image = Assets.Images.RoundedRect.Image,
ImageColor3 = style.Theme.BackgroundUIContrast.Color,
ImageTransparency = style.Theme.BackgroundUIContrast.Transparency,
ScaleType = Assets.Images.RoundedRect.ScaleType,
SliceCenter = Assets.Images.RoundedRect.SliceCenter,
}, {
Border = Roact.createElement(ImageSetLabel, {
Size = UDim2.new(1, 0, 1, 0),
BackgroundTransparency = 1,
Image = Assets.Images.RoundedRectBorder.Image,
SliceCenter = Assets.Images.RoundedRectBorder.SliceCenter,
ScaleType = Assets.Images.RoundedRectBorder.ScaleType,
ImageColor3 = style.Theme.Divider.Color,
ImageTransparency = style.Theme.Divider.Transparency,
}),
SearchIcon = Roact.createElement(ImageSetLabel, {
BackgroundTransparency = 1,
Size = UDim2.new(0, 16, 0, 16),
Position = UDim2.new(0, 22, 0.5, 0),
AnchorPoint = Vector2.new(0.5, 0.5),
Image = Assets.Images.SearchIcon,
ImageColor3 = style.Theme.IconEmphasis.Color,
}),
TextBox = Roact.createElement("TextBox", {
Size = UDim2.new(1, -TEXT_BOX_OFFSET, 1, 0),
Position = UDim2.new(0, TEXT_BOX_OFFSET, 0, 0),
PlaceholderText = "",
Text = self.state.searchText,
ClearTextOnFocus = false,
BackgroundTransparency = 1,
TextXAlignment = Enum.TextXAlignment.Left,
TextYAlignment = Enum.TextYAlignment.Center,
TextColor3 = textTheme.Color,
TextTransparency = textTheme.Transparency,
Font = textFont.Font,
TextSize = textFont.RelativeSize * style.Font.BaseSize,
TextWrapped = false,
[Roact.Change.Text] = function(rbx)
if self.state.searchText ~= rbx.Text then
self:setState({
searchText = rbx.Text,
})
self.props.searchUpdated(self.state.searchText)
end
end,
}),
--TextBox.PlaceholderText can't be used as it's text transparency can't be changed.
PlaceholderText = Roact.createElement(ThemedTextLabel, {
themeKey = "TextEmphasis",
fontKey = "Body",
Text = localized.searchPlaceholder,
Size = UDim2.new(1, -TEXT_BOX_OFFSET, 1, 0),
Position = UDim2.new(0, TEXT_BOX_OFFSET, 0, 0),
TextTransparency = 0.5,
TextXAlignment = Enum.TextXAlignment.Left,
ZIndex = 2,
Visible = self.state.searchText == "",
}),
ClearButton = Roact.createElement(ImageSetButton, {
Size = UDim2.new(0, 18, 0, 18),
Position = UDim2.new(1, -14, 0.5, 0),
AnchorPoint = Vector2.new(1, 0.5),
BackgroundTransparency = 1,
Image = Assets.Images.ClearIcon,
ImageColor3 = style.Theme.IconEmphasis.Color,
Visible = self.state.searchText ~= "",
[Roact.Event.Activated] = function()
self:setState({
searchText = "",
})
self.props.searchUpdated("")
end,
}),
})
end)
end)
end
return SearchBox
@@ -0,0 +1,262 @@
local RunService = game:GetService("RunService")
local CorePackages = game:GetService("CorePackages")
local InGameMenuDependencies = require(CorePackages.InGameMenuDependencies)
local Roact = InGameMenuDependencies.Roact
local UIBlox = InGameMenuDependencies.UIBlox
local t = InGameMenuDependencies.t
local withStyle = UIBlox.Core.Style.withStyle
local InGameMenu = script.Parent.Parent.Parent
local Constants = require(InGameMenu.Resources.Constants)
local InviteStatus = Constants.InviteStatus
local Assets = require(InGameMenu.Resources.Assets)
local Images = UIBlox.App.ImageSet.Images
local ImageSetLabel = UIBlox.Core.ImageSet.Label
local FFlagFixMenuIcons = require(InGameMenu.Flags.FFlagFixMenuIcons)
local SendInviteButton = Roact.PureComponent:extend("SendInviteButton")
SendInviteButton.validateProps = t.strictInterface({
onActivated = t.callback,
animationBegun = t.callback,
animationEnded = t.callback,
userInviteStatus = t.optional(t.string)
})
local FPS = 30
-- Returns 0-1 based on the value of time betweenStartFrame and endFrame
local function linearTween(dTime, startFrame, endFrame)
local range = (endFrame - startFrame)/FPS
return (dTime - (startFrame/FPS)) / range
end
local function isBetweenFrames(dTime, startFrame, endFrame)
return dTime >= (startFrame/FPS) and dTime <= (endFrame/FPS)
end
local STATUS_TO_ANIMATION_MAP = {
[InviteStatus.Pending] = function(timeElapsed, updateBindings)
local maxTransparency = 0.75
local minTransparency = 0
local range = (maxTransparency - minTransparency) / 2
-- Tween transparency on a sin wave for a transparency "pulse" effect
local transparency = range + (range * math.sin(2 * math.pi * timeElapsed))
updateBindings.sendTransparency(transparency)
end,
[InviteStatus.Success] = function(timeElapsed, updateBindings, stopCallback)
-- Fade out send icon, fade-in and scale-down success icon
if isBetweenFrames(timeElapsed, 0, 4) then
local scaledElapse = linearTween(timeElapsed, 0, 4)
updateBindings.sendTransparency(scaledElapse)
updateBindings.successTransparency(1-scaledElapse)
local successSizeMax = 1.5
local successSizeMin = 0.75
local range = successSizeMax-successSizeMin
local size = successSizeMin + (range * (1 - scaledElapse))
updateBindings.successSize(size)
-- Complete fade-outs, scale success icon to normal size
elseif isBetweenFrames(timeElapsed, 4, 9) then
updateBindings.sendTransparency(1)
updateBindings.successTransparency(0)
local scaledElapse = linearTween(timeElapsed, 4, 9)
local successSizeMax = 1
local successSizeMin = 0.75
local range = successSizeMax-successSizeMin
local size = successSizeMin + (range * (scaledElapse))
updateBindings.successSize(size)
-- Complete success icon size tween,
else
updateBindings.successSize(1)
stopCallback()
end
end,
[InviteStatus.Failed] = function(timeElapsed, updateBindings, stopCallback)
-- Fade out send icon
if isBetweenFrames(timeElapsed, 0, 2) then
local scaledElapse = linearTween(timeElapsed, 0, 4)
updateBindings.sendTransparency(scaledElapse)
-- Hold fadeout to frame 52
elseif isBetweenFrames(timeElapsed, 2, 52) then
updateBindings.sendTransparency(1)
-- Fade in send icon
elseif isBetweenFrames(timeElapsed, 52, 57) then
local scaledElapse = linearTween(timeElapsed, 52, 57)
updateBindings.sendTransparency(1-scaledElapse)
-- Finish send fade-in and stop animation
else
updateBindings.sendTransparency(0)
stopCallback()
end
-- Fade in fail icon
if isBetweenFrames(timeElapsed, 0, 4) then
local scaledElapse = linearTween(timeElapsed, 0, 4)
updateBindings.failTransparency(1-scaledElapse)
-- Shake fail icon
elseif isBetweenFrames(timeElapsed, 4, 42) then
updateBindings.failTransparency(0)
local e = 2.71828182846
-- Shake amplitude
local a = -0.4
-- Shake frequency
local f = 40
-- Shake damping
local k = 8
-- Damped oscillation https://www.desmos.com/calculator/ajx32emroq
local et = timeElapsed - 4/FPS
local pos = a * e^(-et * k) * math.sin(et * f)
updateBindings.failPos(pos)
-- Fadeout fail icon
elseif isBetweenFrames(timeElapsed, 42, 47) then
updateBindings.failPos(0)
local scaledElapse = linearTween(timeElapsed, 42, 47)
updateBindings.failTransparency(scaledElapse)
-- Complete fadeout
else
updateBindings.failTransparency(1)
end
end,
}
function SendInviteButton:init(initialProps)
local sendTransparency, updateSendTransparency = Roact.createBinding(0)
local successTransparency, updateSuccessTransparency = Roact.createBinding(1)
local failTransparency, updateFailTransparency = Roact.createBinding(1)
local successSize, updateSuccessSize = Roact.createBinding(1)
local failPos, updateFailPos = Roact.createBinding(0)
self.bindings = {
sendTransparency = sendTransparency,
successTransparency = successTransparency,
failTransparency = failTransparency,
successSize = successSize:map(function(size)
return UDim2.new(size, 0, size, 0)
end),
failPos = failPos:map(function(pos)
return UDim2.new(pos, 0, 0, 0)
end),
}
self.updateBindings = {
sendTransparency = updateSendTransparency,
successTransparency = updateSuccessTransparency,
failTransparency = updateFailTransparency,
successSize = updateSuccessSize,
failPos = updateFailPos,
}
if initialProps.userInviteStatus == InviteStatus.Success then
updateSuccessTransparency(0)
updateSendTransparency(1)
end
end
function SendInviteButton:render()
local props = self.props
return withStyle(function(style)
return Roact.createElement("TextButton", {
Size = UDim2.new(0, 36, 0, 36),
BackgroundTransparency = 1,
Text = "",
[Roact.Event.Activated] = function()
local isPending = props.userInviteStatus == InviteStatus.Pending
local isSuccess = props.userInviteStatus == InviteStatus.Success
if not self.renderSteppedConnection and not (isPending or isSuccess) then
props.onActivated()
end
end
}, {
SendLabel = Roact.createElement(ImageSetLabel, {
BackgroundTransparency = 1,
Image = FFlagFixMenuIcons and Images["icons/actions/friends/friendInvite"] or Assets.Images.SendInvite,
Size = UDim2.new(1, 0, 1, 0),
ImageColor3 = style.Theme.IconEmphasis.Color,
ImageTransparency = self.bindings.sendTransparency,
}),
SuccessLabel = Roact.createElement(ImageSetLabel, {
BackgroundTransparency = 1,
Image = FFlagFixMenuIcons and Images["icons/status/success"] or Assets.Images.InviteSuccess,
Position = UDim2.new(0.5, 0, 0.5, 0),
AnchorPoint = Vector2.new(0.5, 0.5),
ImageColor3 = style.Theme.IconEmphasis.Color,
ImageTransparency = self.bindings.successTransparency,
Size = self.bindings.successSize
}),
FailLabel = Roact.createElement(ImageSetLabel, {
BackgroundTransparency = 1,
Image = FFlagFixMenuIcons and Images["icons/status/alert"] or Assets.Images.InviteFail,
Size = UDim2.new(1, 0, 1, 0),
ImageColor3 = style.Theme.IconEmphasis.Color,
ImageTransparency = self.bindings.failTransparency,
Position = self.bindings.failPos
})
})
end)
end
function SendInviteButton:didUpdate(previousProps)
if self.props.userInviteStatus and self.props.userInviteStatus ~= previousProps.userInviteStatus then
self:playAnimation(STATUS_TO_ANIMATION_MAP[self.props.userInviteStatus])
end
end
function SendInviteButton:didMount()
if self.props.userInviteStatus then
self:playAnimation(STATUS_TO_ANIMATION_MAP[self.props.userInviteStatus])
end
end
function SendInviteButton:playAnimation(animFunc)
self.animationStartTime = tick()
self.animationFunction = animFunc
if not self.renderSteppedConnection then
self.renderSteppedConnection = RunService.RenderStepped:Connect(function()
self.animationFunction(tick() - self.animationStartTime, self.updateBindings, function()
self:stopAnimation()
end)
end)
self.props.animationBegun()
end
end
function SendInviteButton:stopAnimation()
if self.renderSteppedConnection then
self.renderSteppedConnection:Disconnect()
self.renderSteppedConnection = nil
self.props.animationEnded()
end
end
function SendInviteButton:willUnmount()
self:stopAnimation()
end
return SendInviteButton
@@ -0,0 +1,76 @@
return function()
local CorePackages = game:GetService("CorePackages")
local InGameMenuDependencies = require(CorePackages.InGameMenuDependencies)
local Roact = InGameMenuDependencies.Roact
local UIBlox = InGameMenuDependencies.UIBlox
local InGameMenu = script.Parent.Parent.Parent
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 SendInviteButton = require(script.Parent.SendInviteButton)
local Constants = require(InGameMenu.Resources.Constants)
local InviteStatus = Constants.InviteStatus
it("should create and destroy without errors", function()
local element = Roact.createElement(UIBlox.Core.Style.Provider, {
style = appStyle,
}, {
SendInviteButton = Roact.createElement(SendInviteButton, {
onActivated = function()
print("onActivated")
end,
animationBegun = function()
print("animationBegun")
end,
animationEnded = function()
print("animationEnded")
end,
userInviteStatus = nil,
}),
})
local instance = Roact.mount(element)
Roact.unmount(instance)
end)
describe("animations", function()
HACK_NO_XPCALL()
it("should call animation began and ended once", function()
local animationBegunCalled = 0
local animationEndedCalled = 0
local element = Roact.createElement(UIBlox.Core.Style.Provider, {
style = appStyle,
}, {
SendInviteButton = Roact.createElement(SendInviteButton, {
onActivated = function()
print("onActivated")
end,
animationBegun = function()
animationBegunCalled = animationBegunCalled + 1
end,
animationEnded = function()
animationEndedCalled = animationEndedCalled + 1
end,
userInviteStatus = InviteStatus.Failed,
}),
})
local instance = Roact.mount(element)
wait(1)
Roact.unmount(instance)
expect(animationBegunCalled).to.equal(1)
expect(animationEndedCalled).to.equal(1)
end)
end)
end
@@ -0,0 +1,161 @@
local Players = game:GetService("Players")
local CorePackages = game:GetService("CorePackages")
local InGameMenuDependencies = require(CorePackages.InGameMenuDependencies)
local Roact = InGameMenuDependencies.Roact
local t = InGameMenuDependencies.t
local InGameMenu = script.Parent.Parent
local Promise = require(InGameMenu.Utility.Promise)
local withLocalization = require(InGameMenu.Localization.withLocalization)
local Page = require(InGameMenu.Components.Page)
local ThemedTextLabel = require(InGameMenu.Components.ThemedTextLabel)
local PageNavigationWatcher = require(InGameMenu.Components.PageNavigationWatcher)
local InviteFriendsList = require(script.InviteFriendsList)
local AddFriendsNow = require(script.AddFriendsNow)
local LoadingFriendsError = require(script.LoadingFriendsError)
local InviteFriendsPage = Roact.PureComponent:extend("InviteFriendsPage")
InviteFriendsPage.validateProps = t.strictInterface({
pageTitle = t.string,
})
function InviteFriendsPage:init()
self.state = {
loadingFriends = true,
loadingFriendsError = false,
friends = {},
}
end
function InviteFriendsPage:renderLoadingPage()
return withLocalization({
loading = "CoreScripts.InGameMenu.InviteFriends.Loading",
})(function(localized)
return Roact.createElement(ThemedTextLabel, {
Size = UDim2.new(1, 0, 1, 0),
Text = localized.loading,
fontKey = "Header1",
themeKey = "TextEmphasis",
})
end)
end
function InviteFriendsPage:renderError()
return Roact.createElement(LoadingFriendsError, {
onRetry = function()
self:setState({
loadingFriends = true,
loadingFriendsError = false,
})
self:loadFriends()
end
})
end
function InviteFriendsPage:renderNoFriends()
return Roact.createElement(AddFriendsNow)
end
function InviteFriendsPage:renderFriends()
return Roact.createElement(InviteFriendsList, {
players = self.state.friends,
})
end
function InviteFriendsPage:render()
local state = self.state
local children = {
Watcher = Roact.createElement(PageNavigationWatcher, {
desiredPage = "InviteFriends",
onNavigateTo = function()
self:loadFriends()
end,
})
}
if state.loadingFriends then
children.Loading = self:renderLoadingPage()
elseif state.loadingFriendsError then
children.ErrorLoading = self:renderError()
elseif #state.friends == 0 then
children.NoFriends = self:renderNoFriends()
else
children.FriendsList = self:renderFriends()
end
return Roact.createElement(Page, {
pageTitle = self.props.pageTitle,
}, children)
end
function InviteFriendsPage:didMount()
self.mounted = true
self:loadFriends()
end
function InviteFriendsPage:loadFriends()
Promise.new(function(resolve, reject)
coroutine.wrap(function()
local localPlayer = Players.LocalPlayer
local success, friendsPages = pcall(function()
return Players:GetFriendsAsync(localPlayer.UserId)
end)
if not success then
reject("Error loading friends")
return
end
local friends = {}
while true do
for _, item in ipairs(friendsPages:GetCurrentPage()) do
friends[#friends+1] = {
IsOnline = item.IsOnline,
Id = item.Id,
Username = item.Username,
DisplayName = item.DisplayName,
}
end
if not friendsPages.IsFinished then
success = pcall(function()
friendsPages:AdvanceToNextPageAsync()
end)
if not success then
reject("Error loading friends")
return
end
else
break
end
end
resolve(friends)
end)()
end):andThen(function(friends)
if self.mounted then
self:setState({
loadingFriends = false,
friends = friends
})
end
end):catch(function()
if self.mounted then
self:setState({
loadingFriends = false,
loadingFriendsError = true,
})
end
end)
end
function InviteFriendsPage:willUnmount()
self.mounted = false
end
return InviteFriendsPage
@@ -0,0 +1,190 @@
local TextService = game:GetService("TextService")
local UserInputService = game:GetService("UserInputService")
local CorePackages = game:GetService("CorePackages")
local InGameMenuDependencies = require(CorePackages.InGameMenuDependencies)
local Roact = InGameMenuDependencies.Roact
local UIBlox = InGameMenuDependencies.UIBlox
local t = InGameMenuDependencies.t
local withStyle = UIBlox.Core.Style.withStyle
local InGameMenu = script.Parent.Parent
local Assets = require(InGameMenu.Resources.Assets)
local GlobalConfig = require(InGameMenu.GlobalConfig)
local ThemedTextLabel = require(InGameMenu.Components.ThemedTextLabel)
local ImageSetLabel = UIBlox.Core.ImageSet.Label
-- Additional width for keys with centered text like [Backspace], [Enter], etc
local CENTERED_EXTRA_WIDTH = 14 * 2
-- Side padding for off-centered keys [Shift ]
local OFF_CENTER_PADDING = 9
-- Big and small font sizes for key text. (Var names are succinct to make
-- populating override more easy)
local BIG = "CaptionHeader"
local SMALL = "Footer"
local DEFAULT_BORDER_THEME = "UIEmphasis"
local DEFAULT_TEXT_THEME = "TextEmphasis"
local CONTENT_OVERRIDE_MAP = {
[Enum.KeyCode.Escape] = {text = "ESC", fontKey = SMALL, width = 36},
[Enum.KeyCode.Space] = {text = "Space", width = 92},
[Enum.KeyCode.LeftShift] = {text = "Shift", width = 66, alignment = Enum.TextXAlignment.Left},
[Enum.KeyCode.LeftControl] = {text = "Ctrl", width = 66, alignment = Enum.TextXAlignment.Left},
[Enum.KeyCode.LeftSuper] = {text = "Command"},
[Enum.KeyCode.LeftMeta] = {text = "fn"},
[Enum.KeyCode.LeftAlt] = {text = "Opt"},
[Enum.KeyCode.Tilde] = {text = "~", fontKey = BIG},
[Enum.KeyCode.F10] = {fontKey = BIG, width = 36},
[Enum.KeyCode.F11] = {fontKey = BIG, width = 36},
[Enum.KeyCode.Up] = {image = Assets.Images.UpArrow},
[Enum.KeyCode.Down] = {image = Assets.Images.DownArrow},
[Enum.KeyCode.Left] = {image = Assets.Images.LeftArrow},
[Enum.KeyCode.Right] = {image = Assets.Images.RightArrow},
}
local validateProps = t.strictInterface({
input = t.enum(Enum.KeyCode),
--Defaults to UIEmphasis
borderThemeKey = t.optional(t.string),
--Defaults to TextEmphasis
textThemeKey = t.optional(t.string),
AnchorPoint = t.optional(t.Vector2),
Position = t.optional(t.UDim2),
LayoutOrder = t.optional(t.integer),
ZIndex = t.optional(t.integer),
})
local function getLabelWidthAndContent(input, style, textThemeKey)
local override = CONTENT_OVERRIDE_MAP[input]
if override and override.image then
local width = 36
local content = Roact.createElement(ImageSetLabel, {
AnchorPoint = Vector2.new(0.5, 0.5),
BackgroundTransparency = 1,
Image = override.image,
ImageColor3 = style.Theme.IconEmphasis.Color,
ImageTransparency = style.Theme.IconEmphasis.Transparency,
Position = UDim2.new(0.5, 0, 0.5, -1),
Size = UDim2.new(0, 16, 0, 16),
})
return width, content
else
local text, fontKey, width, alignment do
local keyString = UserInputService:GetStringForKeyCode(input)
if override and override.text then
text = override.text
elseif keyString and keyString ~= "" then
text = keyString
else
text = input.Name
end
local textIsShort = text:len() < 3
if override and override.fontKey then
fontKey = override.fontKey
else
fontKey = textIsShort and BIG or SMALL
end
if override and override.width then
width = override.width
elseif textIsShort then
width = 36
else
local fontStyle = style.Font[fontKey]
local textSize = fontStyle.RelativeSize * style.Font.BaseSize
local fontType = fontStyle.Font
local textWidth = TextService:GetTextSize(
text,
textSize,
fontType,
Vector2.new(math.huge, 36)
).X
width = textWidth + CENTERED_EXTRA_WIDTH
end
if override and override.alignment then
alignment = override.alignment
end
end
local content = Roact.createElement(ThemedTextLabel, {
fontKey = fontKey,
themeKey = textThemeKey or DEFAULT_TEXT_THEME,
Text = text,
Size = UDim2.new(1, 0, 1, 0),
Position = UDim2.new(0, 0, 0, -1),
TextXAlignment = alignment,
})
return width, content, alignment
end
end
local function KeyLabel(props)
if GlobalConfig.propValidation then
assert(validateProps(props))
end
return withStyle(function(style)
local borderTheme = style.Theme[props.borderThemeKey or DEFAULT_BORDER_THEME]
local width, content, alignment = getLabelWidthAndContent(
props.input,
style,
props.textThemeKey
)
local padding
if alignment then
padding = OFF_CENTER_PADDING
end
return Roact.createElement(ImageSetLabel, {
BackgroundTransparency = 1,
ImageTransparency = borderTheme.Transparency,
ImageColor3 = borderTheme.Color,
Image = Assets.Images.KeyBorder,
ScaleType = Enum.ScaleType.Slice,
SliceCenter = Rect.new(Vector2.new(9, 7), Vector2.new(26, 26)),
Size = UDim2.new(0, width, 0, 36),
Position = props.Position,
AnchorPoint = props.AnchorPoint,
LayoutOrder = props.LayoutOrder,
ZIndex = props.ZIndex,
}, {
Padding = padding and Roact.createElement("UIPadding", {
PaddingLeft = UDim.new(0, padding),
PaddingRight = UDim.new(0, padding)
}),
LabelContent = content
})
end)
end
return KeyLabel
@@ -0,0 +1,56 @@
return function()
local CorePackages = game:GetService("CorePackages")
local InGameMenuDependencies = require(CorePackages.InGameMenuDependencies)
local Roact = InGameMenuDependencies.Roact
local UIBlox = InGameMenuDependencies.UIBlox
local AppDarkTheme = require(CorePackages.AppTempCommon.LuaApp.Style.Themes.DarkTheme)
local AppFont = require(CorePackages.AppTempCommon.LuaApp.Style.Fonts.Gotham)
local KeyLabel = require(script.Parent.KeyLabel)
local appStyle = {
Theme = AppDarkTheme,
Font = AppFont,
}
it("should create and destroy without errors", function()
local element = Roact.createElement(UIBlox.Core.Style.Provider, {
style = appStyle,
}, {
KeyLabel = Roact.createElement(KeyLabel, {
input = Enum.KeyCode.A,
}),
})
local instance = Roact.mount(element)
Roact.unmount(instance)
end)
it("should create and destroy without errors for keys with special display strings", function()
local element = Roact.createElement(UIBlox.Core.Style.Provider, {
style = appStyle,
}, {
KeyLabel = Roact.createElement(KeyLabel, {
input = Enum.KeyCode.LeftShift,
}),
})
local instance = Roact.mount(element)
Roact.unmount(instance)
end)
it("should create and destroy without errors for keys with special display images", function()
local element = Roact.createElement(UIBlox.Core.Style.Provider, {
style = appStyle,
}, {
KeyLabel = Roact.createElement(KeyLabel, {
input = Enum.KeyCode.Up,
}),
})
local instance = Roact.mount(element)
Roact.unmount(instance)
end)
end
@@ -0,0 +1,81 @@
local RunService = game:GetService("RunService")
local CorePackages = game:GetService("CorePackages")
local RobloxGui = game:GetService("CoreGui"):WaitForChild("RobloxGui")
local InGameMenuDependencies = require(CorePackages.InGameMenuDependencies)
local Roact = InGameMenuDependencies.Roact
local RoactRodux = InGameMenuDependencies.RoactRodux
local t = InGameMenuDependencies.t
local InGameMenu = script.Parent.Parent
local withLocalization = require(InGameMenu.Localization.withLocalization)
local CancelLeavingGame = require(InGameMenu.Actions.CancelLeavingGame)
local GlobalConfig = require(InGameMenu.GlobalConfig)
local ConfirmationDialog = require(script.Parent.ConfirmationDialog)
local Constants = require(InGameMenu.Resources.Constants)
local SendAnalytics = require(InGameMenu.Utility.SendAnalytics)
local GetFFlagAppUsesAutomaticQualityLevel = require(RobloxGui.Modules.Flags.GetFFlagAppUsesAutomaticQualityLevel)
local GetDefaultQualityLevel = require(RobloxGui.Modules.Common.GetDefaultQualityLevel)
local validateProps = t.strictInterface({
isLeavingGame = t.boolean,
onCancel = t.callback,
sendAnalytics = t.callback,
})
local function LeaveGameDialog(props)
if GlobalConfig.propValidation then
assert(validateProps(props))
end
return withLocalization({
bodyText = "CoreScripts.InGameMenu.ConfirmLeaveGame",
confirmText = "CoreScripts.InGameMenu.Leave",
titleText = "CoreScripts.InGameMenu.LeaveGameQuestion",
cancelText = "CoreScripts.InGameMenu.Cancel",
})(function(localized)
return Roact.createElement(ConfirmationDialog, {
bodyText = localized.bodyText,
confirmText = localized.confirmText,
titleText = localized.titleText,
cancelText = localized.cancelText,
bindReturnToConfirm = true,
onCancel = props.onCancel,
onConfirm = function()
SendAnalytics(Constants.AnalyticsInGameMenuName, Constants.AnalyticsLeaveGameName,
{confirmed = Constants.AnalyticsConfirmedName})
RunService.Heartbeat:Wait()
game:Shutdown()
if GetFFlagAppUsesAutomaticQualityLevel() then
settings().Rendering.QualityLevel = GetDefaultQualityLevel()
end
end,
blurBackground = true,
visible = props.isLeavingGame,
})
end)
end
return RoactRodux.UNSTABLE_connect2(function(state, props)
return {
isLeavingGame = state.leavingGame,
}
end, function(dispatch)
return {
onCancel = function()
dispatch(CancelLeavingGame())
SendAnalytics(Constants.AnalyticsInGameMenuName, Constants.AnalyticsLeaveGameName, {confirmed = Constants.AnalyticsCancelledName})
end,
}
end)(LeaveGameDialog)

Some files were not shown because too many files have changed in this diff Show More