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,237 @@
--[[
A Roact alert overlay screen.
Props:
Key : Variant - The key of the overlay screen.
InFocus : bool - Is the component in focus.
Title : String - The title of the screen.
Description : String - The description of the screen.
ImageLabel : Roact.Component - the image to be displayed.
ButtonTextYes : String - The text on the yes button.
ButtonTextNo : String - The text on the no button.
CallbackYes : function() - The callback function for the yes button.
CallbackNo : function() - The callback function for the no button.
CallbackBack : function() - The callback function for when back is pressed.
DefaultButton : 0 or 1 - The default selection for the overlay screen.
0 for Yes
1 for No
]]
local CoreGui = game:GetService("CoreGui")
local RobloxGui = CoreGui.RobloxGui
local Modules = RobloxGui.Modules
local Roact = require(Modules.Common.Roact)
local Utility = require(Modules.Shell.Utility)
local GlobalSettings = require(Modules.Shell.GlobalSettings)
local SoundManager = require(Modules.Shell.SoundManager)
local RoundedButton = require(Modules.Shell.Components.Common.RoundedButton)
local RedirectComponent = require(Modules.Shell.Components.Common.RedirectComponent)
local AlertOverlay = Roact.PureComponent:extend("AlertOverlay")
local TEXT_LEFT_EDGE = 776
local CONFRIM_KEY = "ComfirmButtonKey"
local CANCEL_KEY = "CancelButtonKey"
function AlertOverlay:init()
self.key = self.props.Key
self.guiObjs = {}
self.defaultItemKey = CANCEL_KEY
if self.props.DefaultButton == 0 then
self.defaultItemKey = CONFRIM_KEY
end
self.state =
{
currentItemKey = self.defaultItemKey
}
self.onSelectionGained = function(key)
self:setState(
{
currentItemKey = key
})
end
end
function AlertOverlay:willUpdate(nextProps, nextState)
self.defaultItemKey = nil
if self.props.InFocus == nextProps.InFocus then
return
end
if nextProps.InFocus then
self.defaultItemKey = nextState.currentItemKey
end
end
function AlertOverlay:render()
local confirmButton;
if not self.props.CallbackYes and self.defaultItemKey == CONFRIM_KEY then
self.defaultItemKey = CANCEL_KEY
elseif not self.props.CallbackNo and self.defaultItemKey == CANCEL_KEY then
self.defaultItemKey = CONFRIM_KEY
end
if self.props.CallbackYes then
local confirmButtonProps =
{
Position = UDim2.new(0, TEXT_LEFT_EDGE, 1, -166);
AnchorPoint = Vector2.new(0,0),
Size = UDim2.new(0, 320, 0, 66);
}
local confirmButtonTextProps =
{
Text = self.props.ButtonTextYes,
TextXAlignment = Enum.TextXAlignment.Center,
}
confirmButton = Roact.createElement(RoundedButton,
{
Button = confirmButtonProps,
Text = confirmButtonTextProps,
Focused = self.props.InFocus and self.state.currentItemKey == CONFRIM_KEY,
Selected = self.defaultItemKey == CONFRIM_KEY,
OnSelectionGained = function()
self.onSelectionGained(CONFRIM_KEY)
end,
OnActivated = function()
SoundManager:Play('ButtonPress')
self.props.CallbackYes()
end
})
end
local cancelButton;
if self.props.CallbackNo then
local cancelButtonProps =
{
Position = UDim2.new(0, 1106, 1 ,-166),
AnchorPoint = Vector2.new(0, 0),
Size = UDim2.new(0, 320, 0, 66),
}
local cancelButtonTextProps =
{
Text = self.props.ButtonTextNo,
TextXAlignment = Enum.TextXAlignment.Center,
}
cancelButton = Roact.createElement(RoundedButton,
{
Button = cancelButtonProps,
Text = cancelButtonTextProps,
Focused = self.props.InFocus and self.state.currentItemKey == CANCEL_KEY,
Selected = self.defaultItemKey == CANCEL_KEY,
OnSelectionGained = function()
self.onSelectionGained(CANCEL_KEY)
end,
OnActivated = function()
SoundManager:Play('ButtonPress')
self.props.CallbackNo()
end
})
elseif self.defaultItemKey == CANCEL_KEY then
self.defaultItemKey = CONFRIM_KEY
end
local titleText = Roact.createElement("TextLabel",
{
Size = UDim2.new(0, 0, 0, 0),
Position = UDim2.new(0, TEXT_LEFT_EDGE, 0, 136),
BackgroundTransparency = 1,
Font = GlobalSettings.RegularFont,
FontSize = GlobalSettings.HeaderSize,
TextColor3 = GlobalSettings.WhiteTextColor,
Text = self.props.Title,
TextXAlignment = Enum.TextXAlignment.Left,
})
local descriptionText = Roact.createElement("TextLabel",
{
Size = UDim2.new(0, 762, 0, 304),
Position = UDim2.new(0, TEXT_LEFT_EDGE, 0, 200),
BackgroundTransparency = 1,
TextXAlignment = Enum.TextXAlignment.Left,
TextYAlignment = Enum.TextYAlignment.Top,
Font = GlobalSettings.LightFont,
FontSize = GlobalSettings.TitleSize,
TextColor3 = GlobalSettings.WhiteTextColor,
TextWrapped = true,
Text = self.props.Description,
})
local reportIcon = self.props.ImageLabel
if reportIcon == nil then
reportIcon = Roact.createElement("ImageLabel",
{
Position = UDim2.new(0.5, 0, 0.5, 0),
AnchorPoint = Vector2.new(0.5, 0.5),
BackgroundTransparency = 1,
Image = GlobalSettings.Images.LargeErrorIcon,
Size = UDim2.new(0, 321, 0, 264),
})
end
local imageContainer = Roact.createElement("Frame",
{
Size = UDim2.new(0, 576, 0, 642),
Position = UDim2.new(0, 100, 0.5, 0),
AnchorPoint = Vector2.new(0, 0.5),
BorderSizePixel = 0,
BackgroundTransparency = 1,
},{
reportImage = reportIcon
})
local container = Roact.createElement("Frame",
{
Size = UDim2.new(1, 0, 0, 640),
AnchorPoint = Vector2.new(0, 0.5),
Position = UDim2.new(0, 0, 0.5, 0),
BackgroundColor3 = GlobalSettings.Colors.OverlayColor;
},{
ImageContainer = imageContainer,
TitleText = titleText,
DescriptionText = descriptionText,
CancelButton = cancelButton,
ConfirmButton = confirmButton,
})
local modalOverlay = Roact.createElement("Frame",
{
Size = UDim2.new(1, 0, 1, 0),
AnchorPoint = Vector2.new(0, 0.5),
Position = UDim2.new(0, 0, 0.5, 0),
BackgroundColor3 = Color3.new(0, 0, 0),
BackgroundTransparency = 0.3,
[Roact.Ref] = function(rbx)
self.ref = rbx
end,
},{
Container = container,
})
local redirectObj;
if self.props.CallbackBack then
redirectObj = Roact.createElement(RedirectComponent,
{
Key = self.key,
InFocus = true,
RedirectBack = function()
self.props.CallbackBack()
end,
})
end
return Roact.createElement(Roact.Portal,
{
target = CoreGui
},{
[self.key] = Roact.createElement("ScreenGui",
{
ZIndexBehavior = Enum.ZIndexBehavior.Sibling,
DisplayOrder = 1
},
{
ModalOverlay = modalOverlay,
RedirectObj = redirectObj,
}),
})
end
function AlertOverlay:didMount()
delay(0,function()
Utility.AddSelectionParent(self.key, self.ref)
end)
SoundManager:Play("OverlayOpen")
end
function AlertOverlay:willUnmount()
Utility.RemoveSelectionGroup(self.key)
end
return AlertOverlay
@@ -0,0 +1,82 @@
--[[
A simple base screen for Roact components.
Props:
BackPageTitle : string - The title of the parent page
Content : Roact.Component - The content of the frame
]]
local RobloxGui = game:GetService("CoreGui").RobloxGui
local Modules = RobloxGui.Modules
local Roact = require(Modules.Common.Roact)
local GlobalSettings = require(Modules.Shell.GlobalSettings)
local ContextActionEvent = require(Modules.Shell.Components.ContextActionEvent)
local BaseScreen = Roact.PureComponent:extend("BaseScreen")
local BACK_IMAGE = "rbxasset://textures/ui/Shell/Icons/BackIcon@1080.png"
local BACK_IMAGE_SIZE = 48
function BaseScreen:init()
self.onBack = function(actionName, inputState, inputObject)
if inputState == Enum.UserInputState.Begin then
self._seenPressed = true
elseif inputState == Enum.UserInputState.End and self._seenPressed then
self.props.onUnmount()
end
end
self.onCreate = function(rbx)
end
end
function BaseScreen:render()
local backPageTitle = self.props.BackPageTitle
local onCreate = self.props.OnCreate or self.onCreate
return Roact.createElement("Frame",
{
[Roact.Ref] = function(rbx)
onCreate(rbx)
end,
Size = UDim2.new(1, 0, 1, 0),
Position = UDim2.new(0.5,0,0.5,0),
AnchorPoint = Vector2.new(0.5,0.5),
BackgroundTransparency = 1,
},{
backImage = Roact.createElement("ImageButton",
{
Size = UDim2.new(0,BACK_IMAGE_SIZE,0,BACK_IMAGE_SIZE),
BackgroundTransparency = 1,
Image = BACK_IMAGE,
Selectable = false,
[Roact.Event.Activated] = self.onBack
}),
BackPageTitleLabel = Roact.createElement("TextLabel",
{
Size = UDim2.new(0, 0, 0, BACK_IMAGE_SIZE),
Position = UDim2.new(0,BACK_IMAGE_SIZE+8,0,BACK_IMAGE_SIZE/2),
AnchorPoint = Vector2.new(0,0.5),
BackgroundTransparency = 1,
Font = GlobalSettings.RegularFont,
FontSize = GlobalSettings.ButtonSize,
TextXAlignment = Enum.TextXAlignment.Left,
TextColor3 = GlobalSettings.WhiteTextColor,
Text = backPageTitle
}),
-- NOTE: This will need to be changed when the screen is actually connected to the tree.
BackConnector = Roact.createElement(ContextActionEvent, {
name = "GoBackTo" .. backPageTitle,
callback = self.onBack,
binds = { Enum.KeyCode.ButtonB },
}),
view = Roact.createElement("Frame",
{
Size = UDim2.new(1,0,1,-(BACK_IMAGE_SIZE+2)),
BackgroundTransparency = 1,
Position = UDim2.new(0,0,0,(BACK_IMAGE_SIZE+2)),
},{self.props.Content})
})
end
return BaseScreen
@@ -0,0 +1,156 @@
--[[
Creates a Roact component provides a vertical list menu for multiple elements
Props:
Key : Variant - The key for this category menu. The key cannot be changed after init.
InFocus : bool - Is the component in focus.
Navigator : Navigator - The Navigator object.
DefaultCategoryFocus : Variant - This is the key of the default focus button.
DefaultCategoryKey : Variant - The default selection of the category menu.
Categories : Categories - An object that provides data needed for the creating the categories
The object must have an order mapping and a StringKeys mapping for localization.
E.g.
Categories[CategoryKey].Key = "Category"
Categories[CategoryKey].Order = 1
Categories[CategoryKey].StringKey = "CategoryStringKey"
OnSelectSection : function(Key : Variant) - Callback for when a section of the key is select.
OnLeaveSection : function(Key : Variant) - Callback for when a section of the key is no longer selected.
EnterSection : function() - Callback functin to enter the menu section.
RedirectUp : function() - Callback functin when redirect up.
RedirectDown : function() - Callback functin when redirect down.
ActionPriority : int - The action priority.
]]
local Modules = game:GetService("CoreGui").RobloxGui.Modules
local Roact = require(Modules.Common.Roact)
local Utility = require(Modules.Shell.Utility)
local LocalizedStrings = require(Modules.Shell.LocalizedStrings)
local VerticalListView = require(Modules.Shell.Components.Common.VerticalListView)
local SelectorButton = require(Modules.Shell.Components.Common.SelectorButton)
local RedirectComponent = require(Modules.Shell.Components.Common.RedirectComponent)
local CategoryMenuView = Roact.PureComponent:extend("CategoryMenuView")
local BUTTON_WIDTH = 360
local BUTTON_HEIGHT = 80
function CategoryMenuView:OnSelectSection(key)
if self.state.currentSectionKey ~= key then
self:setState(
{
currentSectionKey = key,
})
end
end
function CategoryMenuView:init()
self.key = self.props.Key
self.defaultCategoryKey = self.props.DefaultCategoryKey
self.defaultCategoryFocus = self.defaultCategoryKey or self.props.DefaultCategoryFocus
self.state =
{
currentSectionKey = self.props.DefaultCategoryFocus,
}
self.getCurrentPageIndex = function()
return self.CurrentSection
end
end
function CategoryMenuView:willUpdate(nextProps, nextState)
self.defaultCategoryKey = nil
if self.props.InFocus == nextProps.InFocus then
return
end
if nextProps.InFocus then
self.defaultCategoryKey = nextState.currentSectionKey or self.props.DefaultCategoryKey
end
end
function CategoryMenuView:render()
self.categories = self.props.Categories
local buttons = {}
for k in pairs(self.categories) do
buttons[k] = Roact.createElement(SelectorButton,
{
Size = UDim2.new(0, BUTTON_WIDTH, 0, BUTTON_HEIGHT),
AnchorPoint = Vector2.new(0, 0),
Text = LocalizedStrings:LocalizedString(self.categories[k].StringKey),
Key = k,
LayoutOrder = self.categories[k].Order,
Focused = self.state.currentSectionKey == k,
Selected = self.defaultCategoryKey == k,
OnSelectionGained = function(key)
self:OnSelectSection(key)
if self.props.OnSelectSection then
self.props.OnSelectSection(key)
end
end,
OnSelectionLost = self.props.OnLeaveSection,
OnActivated = self.props.EnterSection,
})
end
local navObj = Roact.createElement(RedirectComponent,
{
ActionPriority = self.props.ActionPriority,
Key = self.key,
InFocus = self.props.InFocus,
RedirectRight = self.props.EnterSection,
RedirectUp = self.props.RedirectUp,
RedirectDown = self.props.RedirectDown,
})
local buttonView = Roact.createElement(VerticalListView,
{
PaddingTop = UDim.new(0.005, 0),
PaddingBottom = UDim.new(0.005, 0),
Spacing = UDim.new(0.025, 0),
ScrollBarThickness = 0,
Items = buttons,
HorizontalAlignment = Enum.HorizontalAlignment.Left,
ScrollingEnabled = false,
})
return Roact.createElement("Frame",
{
Size = UDim2.new(1, 0, 1, 0),
AnchorPoint = Vector2.new(0, 0),
Position = UDim2.new(0, 0, 0, 0),
BackgroundTransparency = 1,
Selectable = false,
[Roact.Ref] = function(rbx)
self.ref = rbx
end,
},{
NavObj = navObj,
ButtonView = buttonView,
})
end
function CategoryMenuView:didMount()
delay(0, function()
if self.props.InFocus and self.ref then
Utility.RemoveSelectionGroup(self.key)
Utility.AddSelectionParent(self.key, self.ref)
end
end)
end
function CategoryMenuView:didUpdate(previousProps, previousState)
if self.props.InFocus == previousProps.InFocus then
return
end
if self.props.InFocus and self.ref then
Utility.RemoveSelectionGroup(self.key)
Utility.AddSelectionParent(self.key, self.ref)
else
Utility.RemoveSelectionGroup(self.key)
end
end
function CategoryMenuView:willUnmount()
Utility.RemoveSelectionGroup(self.key)
end
return CategoryMenuView
@@ -0,0 +1,33 @@
--[[
Creates a vertical divider
Props:
Position : UDim2 - The position of the divider.
DividerWidth : UDim - The width of the divider.
DividerLength : UDim - The length of the divider.
Color : Color3 - The color of the divider. Default GlobalSettings.PageDivideColor
FillDirection : FillDirection - The direction of the divider.
]]
local Modules = game:GetService("CoreGui").RobloxGui.Modules
local Roact = require(Modules.Common.Roact)
local GlobalSettings = require(Modules.Shell.GlobalSettings)
return function(props)
local color = props.Color or GlobalSettings.PageDivideColor
local dividerLength = props.DividerLength
local dividerWidth = props.DividerWidth or UDim.new(0,2)
local position = props.Position
local size
if props.FillDirection == Enum.FillDirection.Horizontal then
size = UDim2.new(dividerLength.Scale, dividerLength.Offset, dividerWidth.Scale, dividerWidth.Offset)
else
size = UDim2.new(dividerWidth.Scale, dividerWidth.Offset, dividerLength.Scale, dividerLength.Offset)
end
return Roact.createElement("Frame",{
BackgroundColor3 = color,
BorderSizePixel = 0,
Size = size,
Position = position,
})
end
@@ -0,0 +1,149 @@
--[[
A component that is used for navigation and redirects.
Note: The parent must be a scolling
Props:
Key : String
InFocus : bool
Scale : Vector2
RedirectBack : function()
RedirectLeft : function(Ref<GuiObject>) - If nil,
RedirectRight : function(Ref<GuiObject>)
RedirectUp : function(Ref<GuiObject>)
RedirectDown : function(Ref<GuiObject>)
ActionPriority : The action priority
]]
local RobloxGui = game:GetService("CoreGui").RobloxGui
local Modules = RobloxGui.Modules
local Roact = require(Modules.Common.Roact)
local ContextActionService = game:GetService("ContextActionService")
local RedirectComponent = Roact.PureComponent:extend("RedirectComponent")
local BACK_KEY = Enum.KeyCode.ButtonB
local DEFAULT_ACTION_PRIORITY = 2000
local function addBackButton(self)
self.exitActionName = "ExitSection"..self.Key
ContextActionService:UnbindCoreAction(self.exitActionName)
if self.props.ActionPriority then
ContextActionService:BindCoreActionAtPriority(self.exitActionName, self.exitAction, false, DEFAULT_ACTION_PRIORITY + self.props.ActionPriority, BACK_KEY)
else
ContextActionService:BindCoreAction(self.exitActionName, self.exitAction, false, BACK_KEY)
end
end
local function removeBackButton(self)
if self.exitActionName ~= nil then
ContextActionService:UnbindCoreAction(self.exitActionName)
self.exitActionName = nil
end
end
function RedirectComponent:init()
self.Key = self.props.Key or "RedirectComponent"
self.exitActionName = nil
self.backPressed = false
self.exitAction = function(actionName, inputState, inputObject)
if not self.props.InFocus then
return Enum.ContextActionResult.Pass
end
if inputState == Enum.UserInputState.Begin then
self.backPressed = true
elseif inputState == Enum.UserInputState.End and self.backPressed then
self.backPressed = false
self.props.RedirectBack()
end
end
end
function RedirectComponent:render()
local props = self.props
local scale = props.Scale or Vector2.new(1,1)
if props.InFocus and props.RedirectBack then
addBackButton(self)
else
removeBackButton(self)
end
local redirectLeftButton;
if props.InFocus and props.RedirectLeft then
redirectLeftButton = Roact.createElement('TextButton',
{
Position = UDim2.new(-scale.X/2, -1, 0.5, 0),
Size = UDim2.new(0, 2, 1+scale.Y, 0),
AnchorPoint = Vector2.new(0.5,0.5),
BackgroundTransparency = 1,
Text = "",
[Roact.Event.SelectionGained] = function(rbx)
props.RedirectLeft(rbx)
end,
})
end
local redirectRightButton;
if props.InFocus and props.RedirectRight then
redirectRightButton = Roact.createElement('TextButton',
{
Position = UDim2.new(1+scale.X/2, 1, 0.5, 0),
Size = UDim2.new(0, 2, 1+scale.Y, 0),
AnchorPoint = Vector2.new(0.5,0.5),
BackgroundTransparency = 1,
Text = "",
[Roact.Event.SelectionGained] = function(rbx)
props.RedirectRight(rbx)
end,
})
end
local redirectUpButton;
if props.InFocus and props.RedirectUp then
redirectUpButton = Roact.createElement('TextButton',
{
Position = UDim2.new(0.5, 0, -scale.Y/2, -1),
Size = UDim2.new(1+scale.X, 0, 0, 2),
AnchorPoint = Vector2.new(0.5,0.5),
BackgroundTransparency = 1,
Text = "",
[Roact.Event.SelectionGained] = function(rbx)
props.RedirectUp(rbx)
end,
})
end
local redirectDownButton;
if props.InFocus and props.RedirectDown then
redirectDownButton = Roact.createElement('TextButton',
{
Position = UDim2.new(0.5, 0, 1+scale.Y/2, 1),
Size = UDim2.new(1+scale.X, 0, 0, 2),
AnchorPoint = Vector2.new(0.5,0.5),
BackgroundTransparency = 1,
Text = "",
[Roact.Event.SelectionGained] = function(rbx)
props.RedirectDown(rbx)
end,
})
end
return Roact.createElement('ScrollingFrame',
{
Position = UDim2.new(0.5, 0, 0.5, 0),
Size = UDim2.new(1, 0, 1, 0),
AnchorPoint = Vector2.new(0.5,0.5),
BackgroundTransparency = 1,
Selectable = false,
ScrollingEnabled = false,
ScrollBarThickness = 0,
CanvasSize = UDim2.new(0, 0, 1, 0),
},{
RedirectLeftButton = redirectLeftButton,
RedirectRightButton = redirectRightButton,
RedirectUpButton = redirectUpButton,
RedirectDownButton = redirectDownButton,
})
end
function RedirectComponent:willUnmount()
removeBackButton(self)
end
return RedirectComponent
@@ -0,0 +1,206 @@
--[[
Creates a Roact component that is a rounded button
Props:
Button : dictionary - Config for the button.
.Image : Content - The image of the button.
.Size : UDim2 - Size of the button.
.Position : UDim2 - Position of the button.
.AnchorPoint : UDim2 - The anchor point of the button.
.ZIndex: int - Determines the order in which GUI objects are rendered, with 10 being in front and 1 in back.
.LayoutOrder: int - Controls the sorting priority of this button.
.Selectable : bool - Whether or not this object should be selectable using joysticks (controller).
Text : dictionary - A map of props for the text
.Text : string - The label of the button.
.Size : UDim2 - Size of the button.
.Position : UDim2 - Position of the button.
.AnchorPoint : The anchor point of the button.
.Font : Font - The font used to display the given text.
.TextSize : float - The font size in pixels.
.TextXAlignment : TextXAlignment - Sets where text is placed on the X axis within the TextLabel.
.ZIndex: int - Determines the order in which GUI objects are rendered, with 10 being in front and 1 in back.
Focused : bool - Is the button in focus.
Disabled : bool - Is the button disabled. By default a disabled button will not be selectable.
Selected : bool - Should the button be selected.
OnSelectionGained : bool function() -
Fired when the GuiObject is being focused on with the Gamepad selector.
Return true if it should be focused. False otherwise.
OnSelectionLost : bool function() -
Fired when the Gamepad selector stops focusing on the GuiObject.
Return false if it should be un-focused. True otherwise.
OnActivated : function() - Fires when the button is activated.
HideSelectionImage : bool - Whether or not to hide the selection object
DefaultProps : dictionary a map for the default props of the button.
.ImageColor3 : Color3
.ImageTransparency : float
.TextColor3 : Color3
FocusedProps : dictionary a map for the focused props of the button.
.ImageColor3 : Color3
.ImageTransparency : float
.TextColor3 : Color3
DisabledProps : dictionary a map for the disabled props of the button.
.ImageColor3 : Color3
.ImageTransparency : float
.TextColor3 : Color3
]]
local Modules = game:GetService("CoreGui").RobloxGui.Modules
local Roact = require(Modules.Common.Roact)
local Immutable = require(Modules.Common.Immutable)
local Utility = require(Modules.Shell.Utility)
local GlobalSettings = require(Modules.Shell.GlobalSettings)
local SoundComponent = require(Modules.Shell.Components.Common.SoundComponent)
local RoundedButton = Roact.PureComponent:extend("RoundedButton")
function RoundedButton:init()
self.selectionImageObject = Utility.Create "ImageLabel"
{
Name = "SelectorImage",
Image = GlobalSettings.Images.ButtonSelector,
Position = UDim2.new(0, -7, 0, -7),
Size = UDim2.new(1, 14, 1, 14),
BackgroundTransparency = 1,
ScaleType = Enum.ScaleType.Slice,
SliceCenter = Rect.new(31, 31, 63, 63),
}
self.defaultProps =
{
ImageColor3 = GlobalSettings.Colors.WhiteButton,
ImageTransparency = 0.8,
TextColor3 = GlobalSettings.Colors.WhiteText,
}
self.focusedProps =
{
ImageColor3 = GlobalSettings.Colors.BlueButton,
ImageTransparency = 0,
TextColor3 = GlobalSettings.Colors.TextSelected,
}
self.disabledProps =
{
ImageColor3 = GlobalSettings.Colors.WhiteButton,
ImageTransparency = 1,
TextColor3 = GlobalSettings.Colors.WhiteText,
}
self.buttonImage = GlobalSettings.Images.ButtonDefault
--TODO: Change to new Ref API
self.onCreate = function(rbx)
self.ref = rbx
end
end
function RoundedButton:render()
local button = self.props.Button or {}
local text = self.props.Text or {}
local inputDefaultProps = self.props.DefaultProps or {}
local defaultProps = {}
for k in pairs(self.defaultProps) do
defaultProps[k] = inputDefaultProps[k] or self.defaultProps[k]
end
local inputFocusedProps = self.props.FocusedProps or {}
local focusedProps = {}
for k in pairs(self.focusedProps) do
focusedProps[k] = inputFocusedProps[k] or self.focusedProps[k]
end
local inputDisabledProps = self.props.DisabledProps or {}
local disabledProps = {}
for k in pairs(self.disabledProps) do
disabledProps[k] = inputDisabledProps[k] or self.disabledProps[k]
end
if self.props.HideSelectionImage then
self.selectionImageObject.Visible = false
else
self.selectionImageObject.Visible = true
end
local selectable = true
if button.Selectable == false then
selectable = false
end
local currentProps = defaultProps
if self.props.Disabled then
currentProps = disabledProps
selectable = button.Selectable or false
elseif self.props.Focused then
currentProps = focusedProps
end
local baseButtonProps =
{
Image = self.buttonImage,
Size = UDim2.new(1, 0, 1, 0),
Position = UDim2.new(0.5, 0, 0.5, 0),
AnchorPoint = Vector2.new(0.5, 0.5),
Selectable = selectable,
[Roact.Ref] = self.onCreate,
[Roact.Event.SelectionGained] = self.props.OnSelectionGained,
[Roact.Event.SelectionLost] = self.props.OnSelectionLost,
[Roact.Event.Activated] = self.props.OnActivated,
ImageColor3 = currentProps.ImageColor3,
ImageTransparency = currentProps.ImageTransparency,
ScaleType = Enum.ScaleType.Slice,
SliceCenter = Rect.new(8, 8, 9, 9),
SelectionImageObject = self.selectionImageObject,
BackgroundTransparency = 1,
}
local buttonProps = Immutable.JoinDictionaries(baseButtonProps, button)
local baseTextProps =
{
Text = "",
Size = UDim2.new(1, 0, 1, 0),
Position = UDim2.new(0.5, 0, 0.5, 0),
AnchorPoint = Vector2.new(0.5, 0.5),
Font = GlobalSettings.Fonts.Regular,
TextSize = GlobalSettings.TextSizes.Button,
TextXAlignment = Enum.TextXAlignment.Left,
TextColor3 = currentProps.TextColor3,
TextTransparency = currentProps.TextTransparency,
BackgroundTransparency = 1,
}
local textProps = Immutable.JoinDictionaries(baseTextProps, text)
local textLabel = Roact.createElement("TextLabel", textProps)
local moveSelection = Roact.createElement(SoundComponent,
{
SoundName = "MoveSelection",
}
)
local children = self.props[Roact.Children] or {}
return Roact.createElement("ImageButton",
buttonProps,
Immutable.JoinDictionaries(
{
Label = textLabel,
MoveSelection = moveSelection,
}, children)
)
end
function RoundedButton:didMount()
delay(0, function()
if self.props.Selected then
Utility.SetSelectedCoreObject(self.ref)
end
end)
end
function RoundedButton:didUpdate(previousProps, previousState)
if not previousProps.Selected and self.props.Selected then
Utility.SetSelectedCoreObject(self.ref)
end
end
return RoundedButton
@@ -0,0 +1,109 @@
--[[
Creates a Roact component that is a category selector button
Props:
Key : Variant - The key for the selector button.
Text : String - The label of the button.
Size : UDim2 - The size of the this button.
AnchorPoint : Vector2 - The anchor point of the button.
Position : UDim2 - The position of the button.
LayoutOrder : int - The layout order of the button.
Focused : bool - Is the button in focus.
Disabled : bool - Is the button disabled. By default a disabled button will not be selectable.
Selected : bool - Should the button be selected.
OnSelectionGained : function(key : Variant) - Fired when the GuiObject
is being focused on with the Gamepad selector.
OnSelectionLost : function(key : Variant) - Fired when the Gamepad selector stops focusing on the GuiObject.
OnActivated : function(key : Variant) - Fires when the button is activated.
]]
local Modules = game:GetService("CoreGui").RobloxGui.Modules
local Roact = require(Modules.Common.Roact)
local Immutable = require(Modules.Common.Immutable)
local GlobalSettings = require(Modules.Shell.GlobalSettings)
local RoundedButton = require(Modules.Shell.Components.Common.RoundedButton)
local SelectorButton = Roact.PureComponent:extend("SelectorButton")
local SELECTOR_ICON_SIZE_X = 15
local SELECTOR_ICON_SIZE_Y = 30
local SELECTOR_OFFSET_X = 14
function SelectorButton:init()
self.key = self.props.Key
self.onSelectionGained = function()
if self.props.OnSelectionGained then self.props.OnSelectionGained(self.key) end
end
self.onSelectionLost = function()
if self.props.OnSelectionLost then self.props.OnSelectionLost(self.key) end
end
self.onActivated = function()
if self.props.OnActivated then self.props.OnActivated(self.key) end
end
end
function SelectorButton:render()
local buttonProps = {}
local textProps = {}
local size = self.props.Size
local position = self.props.Position
local anchorPoint = self.props.AnchorPoint
local focused = self.props.Focused
local disabled = self.props.Disabled
local selected = self.props.Selected
self.layoutOrder = self.props.LayoutOrder
buttonProps.Size = UDim2.new(1, 0, 1, 0)
buttonProps.AnchorPoint = Vector2.new(0.5, 0.5)
buttonProps.Position = UDim2.new(0.5, 0, 0.5, 0)
textProps.Text = self.props.Text
textProps.Size = UDim2.new(1, 0, 1, 0)
textProps.AnchorPoint = Vector2.new(0, 0.5)
textProps.Position = UDim2.new(0, 24, 0.5, 0)
local button = Roact.createElement(RoundedButton,
{
Button = buttonProps,
Text = textProps,
Focused = focused,
Disabled = disabled,
Selected = selected,
OnSelectionGained = self.onSelectionGained,
OnSelectionLost = self.onSelectionLost,
OnActivated = self.onActivated,
})
local selector;
if focused == true then
selector = Roact.createElement("ImageLabel",
{
Size = UDim2.new(0, SELECTOR_ICON_SIZE_X, 0, SELECTOR_ICON_SIZE_Y),
Image = GlobalSettings.Images.RightArrow,
Position = UDim2.new(1, SELECTOR_OFFSET_X, 0.5, 0),
AnchorPoint = Vector2.new(0, 0.5),
BackgroundTransparency = 1,
})
end
local children = self.props[Roact.Children] or {}
return Roact.createElement("Frame",
{
Size = size,
Position = position,
AnchorPoint = anchorPoint,
LayoutOrder = self.layoutOrder,
BackgroundTransparency = 1,
},Immutable.JoinDictionaries(
{
Button = button,
Selector = selector,
}, children)
)
end
return SelectorButton
@@ -0,0 +1,20 @@
--[[
Creates a component with a shadow image
]]
local Modules = game:GetService("CoreGui").RobloxGui.Modules
local Roact = require(Modules.Common.Roact)
local GlobalSettings = require(Modules.Shell.GlobalSettings)
return function(props)
return Roact.createElement("ImageLabel", {
Name = 'Shadow',
Image = GlobalSettings.Images.Shadow,
Size = UDim2.new(1,3,1,3),
Position = UDim2.new(0,0,0,0),
ScaleType = Enum.ScaleType.Slice,
SliceCenter = Rect.new(10,10,28,28),
BackgroundTransparency = 1,
ZIndex = props.ZIndex or 1,
})
end
@@ -0,0 +1,260 @@
--[[
Creates a component for sidebar
Props:
buttons : array An array of the buttons to be added on the sidebar.
key - string The text to be shown on the sidebar button.
value - function() A callback which will be called when the button is activated.
text: string - The text to be shown on the sidebar.
inFocus: bool - The boolean which indicates whether the sidebar is open.
selectIndex: int - The button index we try to select when the sidebar is open.
paddingTop : UDim - The padding to apply on the top side relative to the sidebar's normal size.
paddingBottom : UDim - The padding to apply on the bottom side relative to the sidebar's normal size.
displayOrder: int - The order that the sidebar ScreenGui is drawn.
onRemoveFocus : function() - Callback function when the remove focus from sidebar
onClose: function() - Callback function when the sidebar is closed.
actionPriority : int - The action priority on sidebar.
]]
local CoreGui = game:GetService("CoreGui")
local Modules = CoreGui.RobloxGui.Modules
local Roact = require(Modules.Common.Roact)
local GlobalSettings = require(Modules.Shell.GlobalSettings)
local RoactMotion = require(Modules.LuaApp.RoactMotion)
local SoundManager = require(Modules.Shell.SoundManager)
local Components = Modules.Shell.Components
local SoundComponent = require(Modules.Shell.Components.Common.SoundComponent)
local ContextActionEvent = require(Components.ContextActionEvent)
local Utility = require(Modules.Shell.Utility)
local INSET_X = 65
local BUTTON_SIZE_Y = 75
local SIDEBAR_SELECTION_GROUP_NAME = "SideBar"
local SideBar = Roact.PureComponent:extend("SideBar")
function SideBar:init()
self.groupKey = SIDEBAR_SELECTION_GROUP_NAME
self.buttonImage = GlobalSettings.Images.ButtonDefault
self.selectionImageObject = Utility.Create "ImageLabel"({
Name = "SelectorImage",
BackgroundTransparency = 1,
Visible = false
})
self.defaultProps = {
buttonColor3 = GlobalSettings.Colors.WhiteButton,
buttonTransparency = 1,
textColor3 = GlobalSettings.Colors.WhiteText,
}
self.focusedProps = {
buttonColor3 = GlobalSettings.Colors.BlueButton,
buttonTransparency = 0,
textColor3 = GlobalSettings.Colors.TextSelected,
}
end
function SideBar:render()
local props = self.props
local onClose = function()
self.buttons = {}
Utility.RemoveSelectionGroup(self.groupKey)
if props.onClose then
props.onClose()
end
end
local contents = {
--Make it inside the title safe container
UIPadding = Roact.createElement("UIPadding", {
PaddingTop = props.paddingTop or UDim.new(0, 156),
PaddingBottom = props.paddingBottom or UDim.new(0, 39)
})
}
if props.buttons then
local index = 0
for _, buttonObj in ipairs(props.buttons) do
index = index + 1
local btIndex = index
local focused = self.state.selectedIndex and self.state.selectedIndex == btIndex
local currProps = focused and self.focusedProps or self.defaultProps
local btText = Roact.createElement("TextLabel", {
Size = UDim2.new(1, -INSET_X, 1, 0),
Position = UDim2.new(0, INSET_X, 0, 0),
AnchorPoint = Vector2.new(0, 0),
Text = buttonObj.text,
TextSize = GlobalSettings.TextSizes.Medium,
TextXAlignment = Enum.TextXAlignment.Left,
TextColor3 = currProps.textColor3,
Font = GlobalSettings.RegularFont,
BackgroundTransparency = 1,
})
local moveSelection = Roact.createElement(SoundComponent, {
SoundName = "MoveSelection",
})
contents["Button"..btIndex] = Roact.createElement("ImageButton", {
Image = self.buttonImage,
Position = UDim2.new(0.5, 0, 0.5, 0),
AnchorPoint = Vector2.new(0.5, 0.5),
Size = UDim2.new(1, -1, 0, BUTTON_SIZE_Y),
LayoutOrder = btIndex,
ImageColor3 = currProps.buttonColor3,
ImageTransparency = currProps.buttonTransparency,
ScaleType = Enum.ScaleType.Slice,
SliceCenter = Rect.new(8, 8, 9, 9),
SelectionImageObject = self.selectionImageObject,
BackgroundTransparency = 1,
[Roact.Event.SelectionGained] = function()
self:setState({
selectedIndex = btIndex
})
end,
[Roact.Event.SelectionLost] = function()
self:setState({
selectedIndex = Roact.None
})
end,
[Roact.Event.Activated] = function()
SoundManager:Play("ButtonPress")
onClose()
buttonObj.callback()
end,
[Roact.Ref] = function(bt)
self.buttons = self.buttons or {}
self.buttons[btIndex] = bt
end,
}, {
ButtonText = btText,
MoveSelection = moveSelection,
})
end
if index > 0 then
contents.UIListLayout = Roact.createElement("UIListLayout", {
Padding = UDim.new(0, 0),
SortOrder = Enum.SortOrder.LayoutOrder,
HorizontalAlignment = Enum.HorizontalAlignment.Left,
VerticalAlignment = Enum.VerticalAlignment.Top,
})
end
else
contents.TextLabel = Roact.createElement("TextLabel", {
Size = UDim2.new(1, -INSET_X - 100, 1, 0),
Position = UDim2.new(0, INSET_X, 0, 0),
BorderSizePixel = 0,
BackgroundTransparency = 1,
Text = props.text,
TextXAlignment = Enum.TextXAlignment.Left,
TextYAlignment = Enum.TextYAlignment.Top,
TextColor3 = GlobalSettings.WhiteTextColor,
Font = GlobalSettings.RegularFont,
FontSize = GlobalSettings.DescriptionSize,
TextWrapped = true,
})
end
local inFocus = props.inFocus
local modalBackgroundTransparency = inFocus and GlobalSettings.ModalBackgroundTransparency or 1
local containerPositionXScale = inFocus and 0.7 or 1
if not inFocus then
self.seenPressed = false
end
return Roact.createElement(RoactMotion.SimpleMotion, {
defaultStyle = {
modalBackgroundTransparency = 1,
containerPositionXScale = 1,
},
style = {
modalBackgroundTransparency = RoactMotion.spring(modalBackgroundTransparency, 600, 60),
containerPositionXScale = RoactMotion.spring(containerPositionXScale, 600, 60),
},
onRested = not inFocus and onClose,
render = function(values)
return Roact.createElement(Roact.Portal, { target = CoreGui }, {
SideBarGui = Roact.createElement("ScreenGui", {
ZIndexBehavior = Enum.ZIndexBehavior.Sibling,
DisplayOrder = props.displayOrder or 1
}, {
BackConnector = inFocus and Roact.createElement(ContextActionEvent, {
name = "CloseSideBar",
callback = function(actionName, inputState, inputObject)
if inputObject.KeyCode == Enum.KeyCode.ButtonB then
if inputState == Enum.UserInputState.Begin then
self.seenPressed = true
elseif inputState == Enum.UserInputState.End and self.seenPressed then
self:setState({
selectedIndex = Roact.None
})
if props.onRemoveFocus then
props.onRemoveFocus()
end
end
end
end,
binds = { Enum.UserInputType.Gamepad1, Enum.UserInputType.Gamepad2, Enum.UserInputType.Gamepad3, Enum.UserInputType.Gamepad4 },
actionPriority = props.actionPriority
}),
ModalOverlay = Roact.createElement("Frame", {
Size = UDim2.new(1, 0, 1, 0),
BackgroundTransparency = values.modalBackgroundTransparency,
BackgroundColor3 = GlobalSettings.ModalBackgroundColor,
BorderSizePixel = 0,
}, {
SideBarContainer = Roact.createElement("Frame", {
Size = UDim2.new(0.3, 0, 1, 0),
Position = UDim2.new(values.containerPositionXScale, 0, 0, 0),
BorderSizePixel = 0,
BackgroundColor3 = GlobalSettings.OverlayColor,
[Roact.Ref] = function(container)
self.container = container
end
}, contents)
})
}),
})
end
})
end
function SideBar:didMount()
delay(0, function()
if self.props.inFocus and self.container then
Utility.RemoveSelectionGroup(self.groupKey)
Utility.AddSelectionParent(self.groupKey, self.container)
local trySelectIndex = self.props.selectIndex or 1
if self.buttons and self.buttons[trySelectIndex] then
if not self.state.selectedIndex then
Utility.SetSelectedCoreObject(self.buttons[trySelectIndex])
end
else
Utility.SetSelectedCoreObject(nil)
end
end
end)
end
function SideBar:didUpdate(previousProps, previousState)
if self.props.inFocus == previousProps.inFocus then
return
end
if self.props.inFocus and self.container then
Utility.RemoveSelectionGroup(self.groupKey)
Utility.AddSelectionParent(self.groupKey, self.container)
local trySelectIndex = self.props.selectIndex or 1
if self.buttons and self.buttons[trySelectIndex] then
if not self.state.selectedIndex then
Utility.SetSelectedCoreObject(self.buttons[trySelectIndex])
end
else
Utility.SetSelectedCoreObject(nil)
end
else
Utility.RemoveSelectionGroup(self.groupKey)
end
end
function SideBar:willUnmount()
Utility.RemoveSelectionGroup(self.groupKey)
end
return SideBar
@@ -0,0 +1,20 @@
--[[
A simple sound component
Props:
SoundName : string - The name of the sound
]]
local RobloxGui = game:GetService("CoreGui").RobloxGui
local Modules = RobloxGui.Modules
local Roact = require(Modules.Common.Roact)
local GlobalSettings = require(Modules.Shell.GlobalSettings)
return function(props)
local soundName = props.SoundName
local soundsUrl = GlobalSettings.Sounds[soundName]
return Roact.createElement('Sound',
{
SoundId = soundsUrl
})
end
@@ -0,0 +1,49 @@
--[[
Creates a component as a spinner
]]
local Modules = game:GetService("CoreGui").RobloxGui.Modules
local Roact = require(Modules.Common.Roact)
local Components = Modules.Shell.Components
local RenderStep = require(Components.RenderStep)
local SPINNER_IMAGE = 'rbxasset://textures/ui/Shell/Icons/LoadingSpinner@1080.png'
local Spinner = Roact.PureComponent:extend("Spinner")
function Spinner:init()
self.state = {
rotation = 0
}
self.speed = 360
self.update = function(dt)
self:setState({
rotation = self.state.rotation + dt * self.speed
})
end
end
function Spinner:render()
local props = self.props
local state = self.state
local rotation = state.rotation
self.speed = props.speed or 360
return Roact.createElement("ImageLabel", {
Rotation = rotation,
BackgroundTransparency = 1,
Size = props.Size or UDim2.new(0, 100, 0, 100),
AnchorPoint = props.AnchorPoint or Vector2.new(0.5, 0.5),
Position = props.Position or UDim2.new(0.5, 0, 0.5, 0),
Image = SPINNER_IMAGE,
ZIndex = props.ZIndex or 10,
ImageTransparency = props.ImageTransparency or 0,
},{
Render = Roact.createElement(RenderStep, {
name = tick(),
priority = Enum.RenderPriority.Input.Value,
callback = self.update,
}),
})
end
return Spinner
@@ -0,0 +1,54 @@
--[[
Creates a Roact component provides a left and a right view
Props:
Bias : number (0 - 1) - The bias for the size of left view vs the right view
LeftView : Roact.Component - The left roact element that will be a child of the SplitView
RightView : Roact.Component - The right roact element that will be a child of the SplitView
Divider : Roact.Component - The divide roact element that will divide the two sides.
]]
local Modules = game:GetService("CoreGui").RobloxGui.Modules
local Roact = require(Modules.Common.Roact)
local SplitViewLR = Roact.Component:extend("SplitViewLR")
function SplitViewLR:init()
end
function SplitViewLR:render()
local props = self.props
local bias = props.Bias
self.leftView = props.LeftView
self.rightView = props.RightView
self.divider = props.Divider
return Roact.createElement("Frame",
{
Size = UDim2.new(1,0,1,0),
BackgroundTransparency = 1,
},{
LeftFrame = Roact.createElement("Frame",
{
Size = UDim2.new(bias,0,1,0),
Position = UDim2.new(0,0,0,0),
BackgroundTransparency = 1,
},{Left = self.leftView}),
Divider = Roact.createElement("Frame",
{
Size = UDim2.new(0,0,0,0),
Position = UDim2.new(bias,0,0,0),
BackgroundTransparency = 1,
},{Divider = self.divider}),
RightFrame = Roact.createElement("Frame",
{
Size = UDim2.new(1-bias,0,1,0),
Position = UDim2.new(bias,0,0,0),
BackgroundTransparency = 1,
},{
Right = self.rightView
}),
})
end
return SplitViewLR
@@ -0,0 +1,116 @@
--[[
Create a Roact toggle button
Props:
Key : Variant - The key for the toggle button.
Text : String - The text of the button.
Size : UDim2 - The size of the this button.
AnchorPoint : Vector2 - The anchor point of the button.
Position : UDim2 - The position of the button.
Toggle : bool - Whether or not the button is toggled or not.
Focused : bool - Is the button selected.
Disabled : bool - Whether or not this button is disabled.
Selected : bool - Should the button be selected.
Selectable : bool - Whether or not the button is selectable.
OnSelectionGained : function(key : Variant) - Fired when the GuiObject is being focused on with the Gamepad selector.
OnSelectionLost : function(key : Variant) - Fired when the Gamepad selector stops focusing on the GuiObject.
OnActivated : function() - Fires when the button is activated.
]]
local Modules = game:GetService("CoreGui").RobloxGui.Modules
local Roact = require(Modules.Common.Roact)
local Immutable = require(Modules.Common.Immutable)
local GlobalSettings = require(Modules.Shell.GlobalSettings)
local RoundedButton = require(Modules.Shell.Components.Common.RoundedButton)
local ToggleButton = Roact.Component:extend("ToggleButton")
local TOGGLE_ICON_SIZE = 32
local TOGGLE_ICON_OFFSET_X = 20
function ToggleButton:init()
self.key = self.props.Key
self.state =
{
selected = false,
active = false,
}
self.onSelectionGained = function()
if self.props.OnSelectionGained then self.props.OnSelectionGained(self.key) end
end
self.onSelectionLost = function()
if self.props.OnSelectionLost then self.props.OnSelectionLost(self.key) end
end
self.onActivated = function()
if self.props.OnActivated then self.props.OnActivated() end
end
end
function ToggleButton:render()
local buttonProps = {}
local textProps = {}
local size = self.props.Size
local position = self.props.Position
local anchorPoint = self.props.AnchorPoint
local focused = self.props.Focused
local disabled = self.props.Disabled
local selected = self.props.Selected
--Default is unknown since its not on or off
local iconColor = GlobalSettings.Colors.StatusIconUnknown
if self.props.Toggle == true then
iconColor = GlobalSettings.Colors.StatusIconEnabled
elseif self.props.Toggle == false then
iconColor = GlobalSettings.Colors.StatusIconDisabled
end
buttonProps.Size = UDim2.new(1, 0, 1, 0)
buttonProps.AnchorPoint = Vector2.new(0.5, 0.5)
buttonProps.Position = UDim2.new(0.5, 0, 0.5, 0)
buttonProps.Selectable = self.props.Selectable
textProps.Text = self.props.Text
textProps.Size = UDim2.new(1, 0, 1, 0)
textProps.AnchorPoint = Vector2.new(0, 0.5)
textProps.Position = UDim2.new(0, TOGGLE_ICON_SIZE+2*TOGGLE_ICON_OFFSET_X, 0.5, 0)
local toggleImage = Roact.createElement("ImageLabel",
{
Size = UDim2.new(0, TOGGLE_ICON_SIZE, 0, TOGGLE_ICON_SIZE),
Image = GlobalSettings.Images.EnabledStatusIcon,
ImageColor3 = iconColor,
Position = UDim2.new(0, TOGGLE_ICON_OFFSET_X, 0.5, 0),
AnchorPoint = Vector2.new(0, 0.5),
BackgroundTransparency = 1,
})
local button = Roact.createElement(RoundedButton,
{
Button = buttonProps,
Text = textProps,
Focused = focused,
Disabled = disabled,
Selected = selected,
OnSelectionGained = self.onSelectionGained,
OnSelectionLost = self.onSelectionLost,
OnActivated = self.onActivated,
},{
ToggleImage = toggleImage,
})
local children = self.props[Roact.Children] or {}
return Roact.createElement("Frame",
{
Size = size,
Position = position,
AnchorPoint = anchorPoint,
BackgroundTransparency = 1,
}, Immutable.JoinDictionaries(
{
Button = button,
}, children))
end
return ToggleButton
@@ -0,0 +1,92 @@
--[[
Creates a Image Loader
Props:
rbxuid : int - Roblox user id.
thumbnailType : Enum.ThumbnailType - Describes the type of user thumbnail that should be returned by GetUserThumbnailAsync.
thumbnailSize : Enum.ThumbnailSize - Describes the resolution of a user thumbnail being returned by GetUserThumbnailAsync.
Size : UDim2 - The thumbnail image size.
Position : UDim2 - The thumbnail image position.
BackgroundTransparency : float - Transparency of the thumbnail image background.
BackgroundColor3 : Color3 - Color of the thumbnail image background.
hasThumbnailData : bool - Whether we have the corresponding thumbnail data in store.
imageUrl : string - The imageUrl for the thumbnail.
isFetching : bool - Whether we are fetching the thumbnail.
]]
local Modules = game:GetService("CoreGui").RobloxGui.Modules
local Components = Modules.Shell.Components
local Roact = require(Modules.Common.Roact)
local GlobalSettings = require(Modules.Shell.GlobalSettings)
local RoactRodux = require(Modules.Common.RoactRodux)
local ApiFetchUserThumbnail = require(Modules.Shell.Thunks.ApiFetchUserThumbnail)
local Spinner = require(Components.Common.Spinner)
local memoize = require(Modules.Common.memoize)
local RETRIES = 6
local UserThumbnailLoader = Roact.PureComponent:extend("UserThumbnailLoader")
function UserThumbnailLoader:render()
local props = self.props
local rbxuid = props.rbxuid
local children = {}
local imageUrl = ""
if rbxuid and rbxuid > 0 then
local hasThumbnailData = props.hasThumbnailData
--TODO: Try refetch if last fetched failed after some interval
if hasThumbnailData and props.imageUrl then
imageUrl = props.imageUrl
else
props.fetchImage(rbxuid, props.thumbnailType, props.thumbnailSize)
end
if props.showSpinner then
children.Spinner = props.isFetching and Roact.createElement(Spinner)
end
else
children.XboxDefaultProfileImage = Roact.createElement("ImageLabel", {
Size = UDim2.new(0.5, 0, 0.5, 0),
Position = UDim2.new(0.25, 0, 0.25, 0),
BackgroundTransparency = 1,
Image = GlobalSettings.Images.DefaultProfile,
})
end
return Roact.createElement("ImageLabel", {
Image = imageUrl,
Size = props.size or UDim2.new(1, 0, 1, 0),
Position = props.position or UDim2.new(0, 0, 0, 0),
BackgroundTransparency = props.backgroundTransparency or 0,
BorderSizePixel = 0,
BackgroundColor3 = props.backgroundColor3 or GlobalSettings.Colors.CharacterBackground,
}, children)
end
local getThumbnailData = memoize(function(thumbnailData)
return {
hasThumbnailData = thumbnailData ~= nil,
isFetching = thumbnailData and thumbnailData.isFetching,
imageUrl = thumbnailData and thumbnailData.imageUrl,
}
end)
local function mapStateToProps(state, props)
local rbxuid = props.rbxuid
local thumbnailType = props.thumbnailType
local thumbnailSize = props.thumbnailSize
local thumbnailData;
if rbxuid and rbxuid > 0 and thumbnailType and thumbnailSize then
local thumbnailId = table.concat{ rbxuid, thumbnailType.Name, thumbnailSize.Name }
thumbnailData = state.UserThumbnails[thumbnailId]
end
return getThumbnailData(thumbnailData)
end
local function mapDispatchToProps(dispatch)
return {
fetchImage = function(rbxuid, thumbnailType, thumbnailSize)
return dispatch(ApiFetchUserThumbnail(rbxuid, thumbnailType, thumbnailSize, RETRIES))
end
}
end
return RoactRodux.UNSTABLE_connect2(mapStateToProps, mapDispatchToProps)(UserThumbnailLoader)
@@ -0,0 +1,204 @@
--[[
Allows the selector to navigate between different pages/levels.
How to use:
The Navigator should be created at the root of the pages/levels.
- Call EnterRootSection(rootKey) to set the root page of the Navigator.
- Call Set(pageKey, defaultGuiObj) to set the default selection object for the given pageKey.
- Call Destruct() when done using the Navigator to clean up the event connections.
To Enter a page:
1. Call SetNextPage(nextPageKey) to set the destination for the next page/level.
2. Call EnterSection() to enter the next selection page/level.
Set(pageKey, defaultGuiObj) needs to be called to set the default guiobject selection.
Otherwise, the selection will fail and this function will return false.
This function can be called anywhere to enter the next level.
Eg. - Call by a GuiObject on selection.
- Call by an event.
- Call by an action with Rodux.
To Exit a page and return to the previous:
1. Call ExitSection() to return to the previous page/level.
This function can be called from anywhere.
It will return false if it is unable to select the next that was set
]]
local Modules = game:GetService("CoreGui").RobloxGui.Modules
local Utility = require(Modules.Shell.Utility)
local ContextActionService = game:GetService('ContextActionService')
local Nav = {}
Nav.__index = Nav
local function AddEnterButtonHelper(self,keycode)
local enterActionName = "EnterSection"..tostring(keycode)
self._actions.EnterSection[keycode] = enterActionName
ContextActionService:BindCoreAction(enterActionName,
function(actionName, inputState, inputObject)
return self:_enterAction(actionName, inputState, inputObject)
end
,false,keycode)
end
local function AddExitButtonHelper(self,keycode)
local exitActionName = "ExitSection"..tostring(keycode)
self._actions.ExitSection[keycode] = exitActionName
ContextActionService:BindCoreAction(exitActionName,
function(actionName, inputState, inputObject)
return self:_exitAction(actionName, inputState, inputObject)
end
,false,keycode)
end
-- By default, the ButtonB binds to back
-- Needs to be called before this object can be used.
function Nav.new()
local self = {}
self._levels = {}
self._guiobjs = {}
self._nextPageKey = ""
self._actions = {}
self._seenPressed = {}
self._actions.EnterSection = {}
self._actions.ExitSection = {}
AddExitButtonHelper(self, Enum.KeyCode.ButtonB)
setmetatable(self, Nav)
return self
end
--[[
Sets a custom key press to enter the next page.
args:
keycode : Enum.KeyCode - The key code of the button.
]]
function Nav:AddEnterButton(keycode)
AddEnterButtonHelper(self,keycode)
end
--[[
Sets a custom key press to exit to the previous page.
args:
keycode : Enum.KeyCode - The key code of the button.
]]
function Nav:AddExitButton(keycode)
AddExitButtonHelper(self,keycode)
end
--[[
Sets and enters the root page. Must be called before the navigator can be used.
args:
rootKey : Variant - The key for the page.
]]
function Nav:EnterRootSection(rootKey)
if self._guiobjs[rootKey] == nil then
return false
end
self._levels = {}
--Stack push to the first element
table.insert(self._levels,rootKey)
Utility.SetSelectedCoreObject(self._guiobjs[rootKey])
return true
end
--[[
Enters the next page
return:
True : if it can enter the next page.
False: otherwise.
]]
function Nav:EnterSection()
local nextPageKey = self._nextPageKey
if nextPageKey == nil or self._guiobjs[nextPageKey] == nil then
return false
end
--Stack push
table.insert(self._levels,nextPageKey)
Utility.SetSelectedCoreObject(self._guiobjs[nextPageKey])
self._nextPageKey = nil
return true
end
--[[
Exits to the previous page
return:
True : if it can enter the previous page
False: otherwise.
]]
function Nav:ExitSection()
--stack pop
table.remove(self._levels)
-- stack peak
local key = self._levels[#self._levels]
if self._guiobjs[key] == nil then
return false
end
Utility.SetSelectedCoreObject(self._guiobjs[key])
return true
end
function Nav:_enterAction(actionName, inputState, inputObject)
-- There is no next page to go to
if self._nextPageKey == nil then
return Enum.ContextActionResult.Pass
end
if inputState == Enum.UserInputState.Begin then
self._seenPressed[inputObject.KeyCode] = true
return Enum.ContextActionResult.Sink
elseif inputState == Enum.UserInputState.End and self._seenPressed[inputObject.KeyCode] then
self._seenPressed[inputObject.KeyCode] = false
self:EnterSection()
return Enum.ContextActionResult.Sink
end
end
function Nav:_exitAction(actionName, inputState, inputObject)
--If we are current at the root page
if(#self._levels <= 1) then
-- There is nothing to go back to.
return Enum.ContextActionResult.Pass
end
if inputState == Enum.UserInputState.Begin then
self._seenPressed[inputObject.KeyCode] = true
elseif inputState == Enum.UserInputState.End and self._seenPressed[inputObject.KeyCode] then
self._seenPressed[inputObject.KeyCode] = false
self:ExitSection()
end
return Enum.ContextActionResult.Sink
end
--[[
Sets the default object to select.
args:
pageKey : Variant - The key for the page.
defaultGuiObj : GuiObject - the default selected object for the page.
]]
function Nav:Set(pageKey, defaultGuiObj)
self._guiobjs[pageKey] = defaultGuiObj
end
--[[
Sets the key of the next page.
args:
nextPageKey : Variant - The key for the next page.
]]
function Nav:SetNextPage(nextPageKey)
self._nextPageKey = nextPageKey
end
-- Needs to be called to clean up event connection.
function Nav:Destruct()
for _,v in pairs(self._actions.EnterSection) do
ContextActionService:UnbindCoreAction(v)
end
for _,v in pairs(self._actions.ExitSection) do
ContextActionService:UnbindCoreAction(v)
end
end
return Nav
@@ -0,0 +1,64 @@
--[[
Creates a Roact component provides a vertical list view for multiple elements
Props:
PaddingTop : UDim - The top padding for the list
PaddingBottom : UDim - The bottom padding for the list
Spacing : UDim - The spacing between each item of the list
HorizontalAlignment : HorizontalAlignment - Determines how grid is placed within
it's parent's container in the x direction.
Can be Left, Center, or Right.
ScrollingEnabled : bool - Determines whether or not scrolling is allowed on the frame.
If false, no scroll bars will be rendered.
ScrollBarThickness : number - How thick the scroll bar appears.
This applies to both the horizontal and vertical scroll bars.
If set to 0, no scroll bars are rendered.
CanvasPosition : Vector2 - The location within the canvas, in pixels,
that should be drawn at the top left of the scroll frame.
CanvasSize : UDim2 - Determines the size of the area that is scrollable.
The UDim2 is calculated using the parent gui's size,
similar to the regular Size property on gui objects.
Items: array<Roact.Component> - The items in the list view
]]
local Modules = game:GetService("CoreGui").RobloxGui.Modules
local Roact = require(Modules.Common.Roact)
return function(props)
local paddingTop = props.PaddingTop
local paddingBottom = props.PaddingBottom
local spacing = props.Spacing
local horizontalAlignment = props.HorizontalAlignment
local scrollBarThickness = props.ScrollBarThickness
local scrollingEnabled = props.ScrollingEnabled
local canvasPosition = props.CanvasPosition
local canvasSize = props.CanvasSize
local ListItems = {}
ListItems["UIPadding"] = Roact.createElement("UIPadding",
{
PaddingTop = paddingTop,
PaddingBottom = paddingBottom,
})
ListItems["UIListLayout"] = Roact.createElement("UIListLayout",
{
Padding = spacing,
FillDirection = Enum.FillDirection.Vertical,
SortOrder = Enum.SortOrder.LayoutOrder,
HorizontalAlignment = horizontalAlignment,
VerticalAlignment = Enum.VerticalAlignment.Top,
CanvasPosition = canvasPosition,
CanvasSize = canvasSize,
})
for k,v in pairs(props.Items) do
ListItems[k] = v
end
return Roact.createElement("ScrollingFrame",
{
Size = UDim2.new(1,0,1,0),
ScrollBarThickness = scrollBarThickness,
BackgroundTransparency = 1,
Selectable = false,
ScrollingEnabled = scrollingEnabled,
},ListItems)
end
@@ -0,0 +1,252 @@
--[[
Creates a Roact component for inifinite scrolling
Props:
scrollingFrameProps : dictionary - props for the scrolling frame
.Selectable : bool - Whether or not this object should be selectable using joysticks (controller).
.ClipsDescendants : bool - Determines whether Roblox will render any portions of its GUI descendants that are outside of its own borders.
items : Array - An array of the input item data which is used to construct item component.
itemSize : Vector2 - The size for each item in the scrolling frame.
itemsPaddingOffset : int - The padding between each item.
scrollingDirection : Enum.ScrollingDirection - The scrolling direction of the scrolling frame, can't be Enum.ScrollingDirection.XY.
itemOffsetStart: int - The minimum distance of the selected guiobject to the window start border.
itemOffsetEnd: int - The minimum distance of the selected guiobject to the window end border.
customScrollDist: dictionary - Custom distances to trigger the scroll.
generateKey : function() - Used to generate a name for the item.
renderItem : function() -
Input: item data, item index and an onSelectionGained callback
Output: a Roact Component
State:
viewStart: int - The item start index.
viewSize: int - The number of items can be put in the window.
paddingStart: int - The padding to apply on the top / left side of the scrolling frame.
]]
local GuiService = game:GetService("GuiService")
local Modules = game:GetService("CoreGui").RobloxGui.Modules
local Utility = require(Modules.Shell.Utility)
local Roact = require(Modules.Common.Roact)
local WindowedScrollingFrame = Roact.PureComponent:extend("WindowedScrollingFrame")
local function clipCanvasPosition(scrollingFrame, canvasPos)
local canvasPosX = canvasPos.X
local canvasPosY = canvasPos.Y
canvasPosX = math.min(canvasPosX, scrollingFrame.CanvasSize.X.Offset - scrollingFrame.AbsoluteWindowSize.X)
canvasPosX = math.max(0, canvasPosX)
canvasPosY = math.min(canvasPosY, scrollingFrame.CanvasSize.Y.Offset - scrollingFrame.AbsoluteWindowSize.Y)
canvasPosY = math.max(0, canvasPosY)
return Vector2.new(canvasPosX, canvasPosY)
end
function WindowedScrollingFrame:init()
self.state = {
viewStart = 0,
viewSize = 0,
paddingStart = 0,
}
self.scrollingFrameRef = function(rbx)
self.scrollingFrame = rbx
end
end
function WindowedScrollingFrame:onSelectionChanged(selectedItem)
if not self.scrollingFrame then
return
end
if selectedItem == nil or selectedItem == self.savedSelectedObject or not selectedItem:IsDescendantOf(self.scrollingFrame) then
return
end
self.savedSelectedObject = selectedItem
local scrollingFrame = self.scrollingFrame
local scrollingDirection = self.props.scrollingDirection or Enum.ScrollingDirection.Y
local absoluteWindowSize = scrollingFrame.AbsoluteWindowSize
local canvasPosition = scrollingFrame.CanvasPosition
local axisKey = "X"
if scrollingDirection == Enum.ScrollingDirection.Y then
axisKey = "Y"
end
-- If our scrolling frame has zero height / width, let's not bother trying to
-- recompute our sizing
if absoluteWindowSize[axisKey] == 0 then
return
end
local itemOffsetStart = self.props.itemOffsetStart or 0
local itemOffsetEnd = self.props.itemOffsetEnd or 0
local customScrollDist = self.props.customScrollDist or {}
--If the selected guiobject is off-window, we move it back into the window instantly
--Then make the motion
local instantPos;
local tweenTargetPos;
if scrollingDirection == Enum.ScrollingDirection.Y then
local topDistance = selectedItem.AbsolutePosition.Y - scrollingFrame.AbsolutePosition.Y
local bottomDistance = (scrollingFrame.AbsolutePosition + scrollingFrame.AbsoluteWindowSize - selectedItem.AbsolutePosition - selectedItem.AbsoluteSize).Y
local minDistTop = itemOffsetStart
local minDistBottom = itemOffsetEnd
if topDistance < (customScrollDist.Top or minDistTop) then
if topDistance < 0 then
instantPos = Vector2.new(canvasPosition.X, canvasPosition.Y + topDistance)
end
tweenTargetPos = Vector2.new(canvasPosition.X, canvasPosition.Y - (minDistTop - topDistance))
elseif bottomDistance < (customScrollDist.Bottom or minDistBottom) then
if bottomDistance < 0 then
instantPos = Vector2.new(canvasPosition.X, canvasPosition.Y - bottomDistance)
end
tweenTargetPos = Vector2.new(canvasPosition.X, canvasPosition.Y + minDistBottom - bottomDistance)
end
elseif scrollingDirection == Enum.ScrollingDirection.X then
local leftDistance = selectedItem.AbsolutePosition.X - scrollingFrame.AbsolutePosition.X
local rightDistance = (scrollingFrame.AbsolutePosition + scrollingFrame.AbsoluteWindowSize - selectedItem.AbsolutePosition - selectedItem.AbsoluteSize).X
local minDistLeft = itemOffsetStart
local minDistRight = itemOffsetEnd
if leftDistance < (customScrollDist.Left or minDistLeft) then
if leftDistance < 0 then
instantPos = Vector2.new(canvasPosition.X + leftDistance, canvasPosition.Y)
end
tweenTargetPos = Vector2.new(canvasPosition.X - (minDistLeft - leftDistance), canvasPosition.Y)
elseif rightDistance < (customScrollDist.Right or minDistRight) then
if rightDistance < 0 then
instantPos = Vector2.new(canvasPosition.X - rightDistance, canvasPosition.Y)
end
tweenTargetPos = Vector2.new(canvasPosition.X + minDistRight - rightDistance, canvasPosition.Y)
end
end
if instantPos then
instantPos = clipCanvasPosition(scrollingFrame, instantPos)
Utility.PropertyTweener(scrollingFrame, "CanvasPosition", instantPos, instantPos, 0, Utility.EaseOutQuad, true, function()
if tweenTargetPos then
tweenTargetPos = clipCanvasPosition(scrollingFrame, tweenTargetPos)
Utility.PropertyTweener(scrollingFrame, "CanvasPosition", instantPos, tweenTargetPos, 0.2, Utility.EaseOutQuad, true)
end
end)
end
if not instantPos and tweenTargetPos then
tweenTargetPos = clipCanvasPosition(scrollingFrame, tweenTargetPos)
Utility.PropertyTweener(scrollingFrame, "CanvasPosition", canvasPosition, tweenTargetPos, 0.2, Utility.EaseOutQuad, true)
end
end
function WindowedScrollingFrame:updateViewBounds()
if not self.scrollingFrame then
return
end
local scrollingFrame = self.scrollingFrame
local itemSize = self.props.itemSize
local itemsPaddingOffset = self.props.itemsPaddingOffset or 0
local scrollingDirection = self.props.scrollingDirection or Enum.ScrollingDirection.Y
local absoluteWindowSize = scrollingFrame.AbsoluteWindowSize
local canvasPosition = scrollingFrame.CanvasPosition
local axisKey = "X"
if scrollingDirection == Enum.ScrollingDirection.Y then
axisKey = "Y"
end
-- If our scrolling frame has zero height / width, let's not bother trying to
-- recompute our sizing
if absoluteWindowSize[axisKey] == 0 then
return
end
canvasPosition = clipCanvasPosition(scrollingFrame, canvasPosition)
local itemTotalSize = (itemSize[axisKey] + itemsPaddingOffset)
local viewSize = math.ceil(absoluteWindowSize[axisKey] / itemTotalSize) + 1
local viewStart = math.floor(canvasPosition[axisKey] / itemTotalSize)
local paddingStart = math.max(0, (viewStart - 1) * itemTotalSize)
local shouldUpdate = viewSize ~= self.state.viewSize or viewStart ~= self.state.viewStart or paddingStart ~= self.state.paddingStart
if shouldUpdate then
self:setState({
viewStart = viewStart,
viewSize = viewSize,
paddingStart = paddingStart,
})
end
end
function WindowedScrollingFrame:render()
local items = self.props.items
local generateKey = self.props.generateKey
local renderItem = self.props.renderItem
local itemSize = self.props.itemSize
local itemsPaddingOffset = self.props.itemsPaddingOffset or 0
local scrollingDirection = self.props.scrollingDirection or Enum.ScrollingDirection.Y
assert(scrollingDirection ~= Enum.ScrollingDirection.XY, "Can't set ScrollingDirection as XY.")
local children = {}
children.UIListLayout = Roact.createElement("UIListLayout", {
Padding = UDim.new(0, itemsPaddingOffset),
SortOrder = Enum.SortOrder.LayoutOrder,
FillDirection = scrollingDirection == Enum.ScrollingDirection.Y and Enum.FillDirection.Vertical or Enum.FillDirection.Horizontal
})
if scrollingDirection == Enum.ScrollingDirection.Y then
children.UIPadding = Roact.createElement("UIPadding", {
PaddingTop = UDim.new(0, self.state.paddingStart)
})
elseif scrollingDirection == Enum.ScrollingDirection.X then
children.UIPadding = Roact.createElement("UIPadding", {
PaddingLeft = UDim.new(0, self.state.paddingStart)
})
end
local lowerBound = math.max(1, self.state.viewStart)
local upperBound = math.min(#items, self.state.viewStart + self.state.viewSize)
for i = lowerBound, upperBound do
local key = generateKey and generateKey(i) or i
children[key] = renderItem(items[i], i)
end
local scrollingFrameProps = self.props.scrollingFrameProps or {}
local canvasSize = nil
if scrollingDirection == Enum.ScrollingDirection.Y then
canvasSize = UDim2.new(1, 0, 0, #items * itemSize.Y + (#items - 1) * itemsPaddingOffset)
elseif scrollingDirection == Enum.ScrollingDirection.X then
canvasSize = UDim2.new(0, #items * itemSize.X + (#items - 1) * itemsPaddingOffset, 1, 0)
end
return Roact.createElement("ScrollingFrame", {
Size = UDim2.new(1, 0, 1, 0),
ScrollingEnabled = false, --Don't let the default select logic affect canvas position
CanvasSize = canvasSize,
Selectable = scrollingFrameProps.selectable or false,
ScrollBarThickness = 0,
ClipsDescendants = scrollingFrameProps.clipsDescendants,
BackgroundTransparency = 1,
ScrollingDirection = scrollingDirection,
[Roact.Ref] = self.scrollingFrameRef,
[Roact.Change.CanvasPosition] = function() self:updateViewBounds() end,
[Roact.Change.AbsoluteSize] = function() self:updateViewBounds() end,
}, children)
end
function WindowedScrollingFrame:didMount()
self:updateViewBounds()
end
function WindowedScrollingFrame:didUpdate(prevProps, prevState)
if not prevProps.inFocus and self.props.inFocus then
self.conn = GuiService:GetPropertyChangedSignal("SelectedCoreObject"):connect(function()
self:onSelectionChanged(GuiService.SelectedCoreObject)
end)
elseif prevProps.inFocus and not self.props.inFocus then
Utility.DisconnectEvent(self.conn)
end
if self.props ~= prevProps then
self:updateViewBounds()
end
end
return WindowedScrollingFrame