886 lines
30 KiB
Lua
886 lines
30 KiB
Lua
-- Written by Kip Turner, Copyright Roblox 2015
|
|
|
|
local CoreGui = game:GetService("CoreGui")
|
|
local GuiRoot = CoreGui:FindFirstChild("RobloxGui")
|
|
local Modules = GuiRoot:FindFirstChild("Modules")
|
|
local ShellModules = Modules:FindFirstChild("Shell")
|
|
|
|
local Utility = require(ShellModules:FindFirstChild('Utility'))
|
|
local GuiService = game:GetService('GuiService')
|
|
|
|
local function ScrollingGrid(config)
|
|
local this = {}
|
|
this.Enum =
|
|
{
|
|
ScrollDirection = {["Vertical"] = 1; ["Horizontal"] = 2;};
|
|
StartCorner = {["UpperLeft"] = 1; ["UpperRight"] = 2; ["BottomLeft"] = 3; ["BottomRight"] = 4;};
|
|
}
|
|
|
|
this.GridItems = {}
|
|
this.ItemSet = {}
|
|
|
|
--Config
|
|
config = config or {}
|
|
this.ScrollDirection = (config.ScrollDirection and this.Enum.ScrollDirection[config.ScrollDirection]) or this.Enum.ScrollDirection.Vertical
|
|
this.StartCorner = (config.StartCorner and this.Enum.StartCorner[config.StartCorner]) or this.Enum.StartCorner.UpperLeft
|
|
this.FixedRowColumnCount = config.FixedRowColumnCount
|
|
this.CellSize = config.CellSize or Vector2.new(100,100)
|
|
this.Padding = config.Padding or Vector2.new(0,0)
|
|
this.Spacing = config.Spacing or Vector2.new(0,0)
|
|
--Whether the content in scorlling area is dynamic
|
|
this.Dynamic = config.Dynamic or false
|
|
|
|
--build the base guis
|
|
local ContainerAttributes =
|
|
{
|
|
Size = UDim2.new(1, 0, 1, 0);
|
|
Position = UDim2.new(0, 0, 0, 0);
|
|
CanvasSize = UDim2.new(1, 0, 1, 0);
|
|
Name = "ScrollingGridContainer";
|
|
BackgroundTransparency = 1;
|
|
ClipsDescendants = true;
|
|
Visible = true;
|
|
ScrollingEnabled = false;
|
|
ScrollBarThickness = 0;
|
|
Selectable = false;
|
|
}
|
|
|
|
for key, value in pairs(config) do
|
|
if ContainerAttributes[key] ~= nil then
|
|
ContainerAttributes[key] = value
|
|
end
|
|
end
|
|
|
|
local container = Utility.Create'ScrollingFrame'(ContainerAttributes)
|
|
|
|
|
|
this.Container = container
|
|
|
|
this.DefaultSelection = this.Container
|
|
|
|
this.Container:GetPropertyChangedSignal('AbsoluteSize'):connect(function()
|
|
this:RecalcLayout()
|
|
end)
|
|
|
|
--[Common APIs]--
|
|
function this:GetPadding()
|
|
return self.Padding
|
|
end
|
|
|
|
function this:SetPadding(newPadding)
|
|
if newPadding ~= self.Padding then
|
|
self.Padding = newPadding
|
|
self:RecalcLayout()
|
|
end
|
|
end
|
|
|
|
function this:GetSpacing()
|
|
return self.Spacing
|
|
end
|
|
|
|
function this:SetSpacing(newSpacing)
|
|
if newSpacing ~= self.Spacing then
|
|
self.Spacing = newSpacing
|
|
self:RecalcLayout()
|
|
end
|
|
end
|
|
|
|
function this:GetCellSize()
|
|
return self.CellSize
|
|
end
|
|
|
|
function this:GetGridItemSize()
|
|
return self.CellSize
|
|
end
|
|
|
|
function this:SetCellSize(cellSize)
|
|
if cellSize ~= self.CellSize then
|
|
self.CellSize = cellSize
|
|
self:RecalcLayout()
|
|
end
|
|
end
|
|
|
|
function this:GetScrollDirection()
|
|
return self.ScrollDirection
|
|
end
|
|
|
|
function this:SetScrollDirection(newDirection)
|
|
if newDirection ~= self.ScrollDirection then
|
|
self.ScrollDirection = newDirection
|
|
self:RecalcLayout()
|
|
end
|
|
end
|
|
|
|
function this:GetStartCorner()
|
|
return self.StartCorner
|
|
end
|
|
|
|
function this:SetStartCorner(newStartCorner)
|
|
if newStartCorner ~= self.StartCorner then
|
|
self.StartCorner = newStartCorner
|
|
self:RecalcLayout()
|
|
end
|
|
end
|
|
|
|
function this:GetRowColumnConstraint()
|
|
return self.FixedRowColumnCount
|
|
end
|
|
|
|
function this:SetRowColumnConstraint(fixedRowColumnCount)
|
|
if fixedRowColumnCount < 1 then
|
|
fixedRowColumnCount = nil
|
|
end
|
|
if fixedRowColumnCount ~= self.FixedRowColumnCount then
|
|
self.FixedRowColumnCount = fixedRowColumnCount
|
|
self:RecalcLayout()
|
|
end
|
|
end
|
|
|
|
|
|
function this:GetClipping()
|
|
return self.Container.ClipsDescendants
|
|
end
|
|
|
|
function this:SetClipping(clippingEnabled)
|
|
self.Container.ClipsDescendants = clippingEnabled;
|
|
end
|
|
|
|
function this:GetVisible()
|
|
return self.Container.Visible
|
|
end
|
|
|
|
function this:SetVisible(isVisible)
|
|
self.Container.Visible = isVisible;
|
|
end
|
|
|
|
function this:GetSize()
|
|
return self.Container.Size
|
|
end
|
|
|
|
function this:SetSize(size)
|
|
self.Container.Size = size
|
|
end
|
|
|
|
function this:GetPosition()
|
|
return self.Container.Position
|
|
end
|
|
|
|
function this:SetPosition(position)
|
|
self.Container.Position = position
|
|
end
|
|
|
|
function this:GetParent()
|
|
return self.Container.Parent
|
|
end
|
|
|
|
function this:SetParent(parent)
|
|
self.Container.Parent = parent
|
|
end
|
|
|
|
function this:GetGuiObject()
|
|
return self.Container
|
|
end
|
|
|
|
-- Default selection handles the case of removing the last item in the grid while it is selected
|
|
-- Set to nil if do not want a default selection
|
|
function this:SetDefaultSelection(selectionObject)
|
|
self.DefaultSelection = selectionObject
|
|
end
|
|
|
|
function this:ResetDefaultSelection()
|
|
self.DefaultSelection = self.Container
|
|
end
|
|
|
|
function this:ContainsItem(gridItem)
|
|
return self.ItemSet[gridItem] ~= nil
|
|
end
|
|
|
|
function this:FindAncestorGridItem(object)
|
|
if object ~= nil then
|
|
if self:ContainsItem(object) then
|
|
return object
|
|
end
|
|
return self:FindAncestorGridItem(object.Parent)
|
|
end
|
|
end
|
|
|
|
function this:Get2DGridIndex(index)
|
|
-- 0 base index
|
|
local zerobasedIndex = index - 1
|
|
local rows, columns = self:GetNumRowsColumns()
|
|
local row, column
|
|
|
|
-- TODO: implement StartCorner here
|
|
if self.ScrollDirection == self.Enum.ScrollDirection.Vertical then
|
|
row = math.floor(zerobasedIndex / columns)
|
|
column = zerobasedIndex % columns
|
|
else
|
|
column = math.floor(zerobasedIndex / rows)
|
|
row = zerobasedIndex % rows
|
|
end
|
|
|
|
return row, column
|
|
end
|
|
|
|
function this:GetGridPosition(row, column)
|
|
local cellSize = self:GetCellSize()
|
|
local spacing = self:GetSpacing()
|
|
local padding = self:GetPadding()
|
|
return UDim2.new(0, padding.X + column * cellSize.X + column * spacing.X,
|
|
0, padding.Y + row * cellSize.Y + row * spacing.Y)
|
|
end
|
|
|
|
function this:GetCanvasPositionForOffscreenItem(selectedObject)
|
|
-- NOTE: using <= and >= instead of < and > because scrollingframe
|
|
-- code may automatically bump it while we are observing the change
|
|
if selectedObject and self.Container and self:ContainsItem(selectedObject) then
|
|
if self.ScrollDirection == self.Enum.ScrollDirection.Vertical then
|
|
if selectedObject.AbsolutePosition.Y <= self.Container.AbsolutePosition.Y then
|
|
return Utility.ClampCanvasPosition(self.Container, Vector2.new(0, selectedObject.Position.Y.Offset)) -- - selectedObject.AbsoluteSize.Y/2))
|
|
elseif selectedObject.AbsolutePosition.Y + selectedObject.AbsoluteSize.Y >= self.Container.AbsolutePosition.Y + self.Container.AbsoluteWindowSize.Y then
|
|
return Utility.ClampCanvasPosition(self.Container, Vector2.new(0, -(self.Container.AbsoluteWindowSize.Y - selectedObject.Position.Y.Offset - selectedObject.AbsoluteSize.Y) )) --+ selectedObject.AbsoluteSize.Y/2))
|
|
end
|
|
else -- Horizontal
|
|
if selectedObject.AbsolutePosition.X <= self.Container.AbsolutePosition.X then
|
|
return Utility.ClampCanvasPosition(self.Container, Vector2.new(selectedObject.Position.X.Offset, 0))
|
|
elseif selectedObject.AbsolutePosition.X + selectedObject.AbsoluteSize.X >= self.Container.AbsolutePosition.X + self.Container.AbsoluteWindowSize.X then
|
|
return Utility.ClampCanvasPosition(self.Container, Vector2.new(-(self.Container.AbsoluteWindowSize.X - selectedObject.Position.X.Offset - selectedObject.AbsoluteSize.X), 0))
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
function this:Destroy()
|
|
if self.Container then
|
|
self.Container:Destroy()
|
|
end
|
|
end
|
|
|
|
|
|
----Make API based on the scrolling grid type
|
|
if not this.Dynamic then
|
|
function this:GetFirstVisibleItem()
|
|
local firstVisibleItem = nil
|
|
for i = #self.GridItems, 1, -1 do
|
|
local item = self.GridItems[i]
|
|
if item then
|
|
if item.Position.X.Offset >= self.Container.CanvasPosition.X and
|
|
item.Position.X.Offset + item.AbsoluteSize.X <= self.Container.CanvasPosition.X + self.Container.AbsoluteWindowSize.X then
|
|
firstVisibleItem = item
|
|
end
|
|
end
|
|
end
|
|
return firstVisibleItem
|
|
end
|
|
|
|
function this:SortItems(sortFunc)
|
|
table.sort(self.GridItems, sortFunc)
|
|
self:RecalcLayout()
|
|
|
|
local selectedObject = self:FindAncestorGridItem(GuiService.SelectedCoreObject)
|
|
if selectedObject and self:ContainsItem(selectedObject) then
|
|
local thisPos = self:GetCanvasPositionForOffscreenItem(selectedObject)
|
|
if thisPos then
|
|
Utility.PropertyTweener(self.Container, 'CanvasPosition', thisPos, thisPos, 0, Utility.EaseOutQuad, true)
|
|
end
|
|
end
|
|
end
|
|
|
|
function this:AddItem(gridItem)
|
|
if not self:ContainsItem(gridItem) then
|
|
table.insert(self.GridItems, gridItem)
|
|
self.ItemSet[gridItem] = true
|
|
gridItem.Parent = self.Container
|
|
if GuiService.SelectedCoreObject == self.DefaultSelection then
|
|
Utility.SetSelectedCoreObject(gridItem)
|
|
end
|
|
self:RecalcLayout()
|
|
end
|
|
end
|
|
|
|
function this:RemoveItem(gridItem)
|
|
if self:ContainsItem(gridItem) then
|
|
for i, otherItem in pairs(self.GridItems) do
|
|
if otherItem == gridItem then
|
|
table.remove(self.GridItems, i)
|
|
-- Assign a new selection
|
|
if GuiService.SelectedCoreObject == gridItem then
|
|
GuiService.SelectedCoreObject = self.GridItems[i] or self.GridItems[i-1] or self.GridItems[1] or self.DefaultSelection
|
|
end
|
|
-- Clean-up
|
|
self.ItemSet[gridItem] = nil
|
|
gridItem.Parent = nil
|
|
self:RecalcLayout()
|
|
return
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
function this:RemoveAllItems()
|
|
local wasSelected = false
|
|
do
|
|
local currentSelection = GuiService.SelectedCoreObject
|
|
while currentSelection ~= nil and wasSelected == false do
|
|
wasSelected = wasSelected or self:ContainsItem(currentSelection)
|
|
currentSelection = currentSelection.Parent
|
|
end
|
|
end
|
|
for i = #self.GridItems, 1, -1 do
|
|
local removed = table.remove(self.GridItems, i)
|
|
self.ItemSet[removed] = nil
|
|
removed.Parent = nil
|
|
end
|
|
|
|
if wasSelected then
|
|
GuiService.SelectedCoreObject = self.Container
|
|
end
|
|
|
|
self:RecalcLayout()
|
|
self.Container.CanvasPosition = Vector2.new(0, 0)
|
|
end
|
|
|
|
function this:GetNumRowsColumns()
|
|
local rows, columns = 0, 0
|
|
|
|
local windowSize = self.Container.AbsoluteWindowSize
|
|
local padding = self:GetPadding()
|
|
local cellSize = self:GetCellSize()
|
|
local cellSpacing = self:GetSpacing()
|
|
local adjustedWindowSize = Utility.ClampVector2(Vector2.new(0, 0), windowSize - padding, windowSize - padding)
|
|
local absoluteCellSize = Utility.ClampVector2(Vector2.new(1,1), cellSize + cellSpacing, cellSize + cellSpacing)
|
|
local windowSizeCalc = (adjustedWindowSize + cellSpacing) / absoluteCellSize
|
|
|
|
if self.ScrollDirection == self.Enum.ScrollDirection.Vertical then
|
|
columns = math.max(1, self:GetRowColumnConstraint() or math.floor(windowSizeCalc.x))
|
|
rows = math.ceil(math.max(1, #self.GridItems) / columns)
|
|
else
|
|
rows = math.max(1, self:GetRowColumnConstraint() or math.floor(windowSizeCalc.y))
|
|
columns = math.ceil(math.max(1, #self.GridItems) / rows)
|
|
end
|
|
|
|
return rows, columns
|
|
end
|
|
|
|
function this:RecalcLayout()
|
|
local padding = self:GetPadding()
|
|
local cellSpacing = self:GetSpacing()
|
|
local gridItemSize = self:GetGridItemSize()
|
|
local rows, columns = self:GetNumRowsColumns()
|
|
|
|
if self.ScrollDirection == self.Enum.ScrollDirection.Vertical then
|
|
self.Container.CanvasSize = UDim2.new(self.Container.Size.X.Scale, self.Container.Size.X.Offset, 0, padding.Y * 2 + rows * gridItemSize.Y + (math.max(0, rows - 1)) * cellSpacing.Y)
|
|
else
|
|
self.Container.CanvasSize = UDim2.new(0, padding.X * 2 + columns * gridItemSize.X + (math.max(0, columns - 1)) * cellSpacing.X, self.Container.Size.Y.Scale, self.Container.Size.Y.Offset)
|
|
end
|
|
|
|
local grid2DtoIndex = {}
|
|
for i = 1, #self.GridItems do
|
|
local row, column = self:Get2DGridIndex(i)
|
|
local gridItem = self.GridItems[i]
|
|
|
|
gridItem.Size = UDim2.new(0, gridItemSize.X, 0, gridItemSize.Y)
|
|
gridItem.Position = self:GetGridPosition(row, column, gridItemSize)
|
|
|
|
grid2DtoIndex[row] = grid2DtoIndex[row] or {}
|
|
grid2DtoIndex[row][column] = gridItem
|
|
end
|
|
|
|
for rowNum, row in pairs(grid2DtoIndex) do
|
|
for columnNum, column in pairs(row) do
|
|
local gridItem = grid2DtoIndex[rowNum][columnNum]
|
|
if gridItem then
|
|
if self.ScrollDirection == self.Enum.ScrollDirection.Vertical then
|
|
gridItem.NextSelectionUp = grid2DtoIndex[rowNum - 1] and grid2DtoIndex[rowNum - 1][columnNum] or nil
|
|
gridItem.NextSelectionDown = grid2DtoIndex[rowNum + 1] and grid2DtoIndex[rowNum + 1][columnNum] or nil
|
|
if gridItem.NextSelectionDown == nil and grid2DtoIndex[rowNum + 1] ~= nil then
|
|
gridItem.NextSelectionDown = self.GridItems[#self.GridItems]
|
|
end
|
|
gridItem.NextSelectionLeft = nil
|
|
gridItem.NextSelectionRight = nil
|
|
else
|
|
gridItem.NextSelectionLeft = grid2DtoIndex[rowNum] and grid2DtoIndex[rowNum][columnNum - 1] or gridItem
|
|
gridItem.NextSelectionRight = grid2DtoIndex[rowNum] and grid2DtoIndex[rowNum][columnNum + 1] or nil
|
|
if gridItem.NextSelectionRight == nil then
|
|
if grid2DtoIndex[0] and grid2DtoIndex[0][columnNum + 1] then
|
|
-- Move to the last position
|
|
gridItem.NextSelectionRight = self.GridItems[#self.GridItems]
|
|
else
|
|
-- Avoid selector from moving to other selectable objects
|
|
gridItem.NextSelectionRight = gridItem
|
|
end
|
|
end
|
|
gridItem.NextSelectionUp = nil
|
|
gridItem.NextSelectionDown = nil
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
do
|
|
this:RecalcLayout()
|
|
local lastSelectedObject = nil
|
|
GuiService:GetPropertyChangedSignal('SelectedCoreObject'):connect(function()
|
|
local selectedObject = this:FindAncestorGridItem(GuiService.SelectedCoreObject)
|
|
if selectedObject and this:ContainsItem(selectedObject) then
|
|
local upDirection = (this.ScrollDirection == this.Enum.ScrollDirection.Vertical) and 'NextSelectionUp' or 'NextSelectionLeft'
|
|
local downDirection = (this.ScrollDirection == this.Enum.ScrollDirection.Vertical) and 'NextSelectionDown' or 'NextSelectionRight'
|
|
local upObject = selectedObject[upDirection]
|
|
local downObject = selectedObject[downDirection]
|
|
|
|
local nextPos, upPos, downPos;
|
|
|
|
|
|
local gridItemSize = this:GetGridItemSize()
|
|
local thisPos = this:GetCanvasPositionForOffscreenItem(selectedObject)
|
|
|
|
if lastSelectedObject then
|
|
local lastUpObject = lastSelectedObject[upDirection]
|
|
local lastDownObject = lastSelectedObject[downDirection]
|
|
|
|
if upObject and lastUpObject == selectedObject then
|
|
upPos = this:GetCanvasPositionForOffscreenItem(upObject)
|
|
if upObject ~= selectedObject and upPos then
|
|
upPos = upPos + gridItemSize / 2
|
|
end
|
|
elseif downObject and lastDownObject == selectedObject then
|
|
downPos = this:GetCanvasPositionForOffscreenItem(downObject)
|
|
if downObject ~= selectedObject and downPos then
|
|
downPos = downPos - gridItemSize / 2
|
|
end
|
|
end
|
|
end
|
|
|
|
if upPos and (upPos.Y < this.Container.CanvasPosition.Y or upPos.X < this.Container.CanvasPosition.X) then
|
|
nextPos = upPos
|
|
elseif downPos and (downPos.Y > this.Container.CanvasPosition.Y or downPos.X > this.Container.CanvasPosition.X) then
|
|
nextPos = downPos
|
|
else
|
|
nextPos = thisPos
|
|
end
|
|
|
|
if nextPos then
|
|
nextPos = Utility.ClampCanvasPosition(this.Container, nextPos)
|
|
if thisPos then
|
|
-- Sort of a hack to not snap on the last one
|
|
if (upObject and downObject) then
|
|
Utility.PropertyTweener(this.Container, 'CanvasPosition', thisPos, thisPos, 0, Utility.EaseOutQuad, true, function()
|
|
Utility.PropertyTweener(this.Container, 'CanvasPosition', this.Container.CanvasPosition, nextPos, 0.2, Utility.EaseOutQuad, true)
|
|
end)
|
|
end
|
|
else
|
|
Utility.PropertyTweener(this.Container, 'CanvasPosition', this.Container.CanvasPosition, nextPos, 0.2, Utility.EaseOutQuad, true)
|
|
end
|
|
end
|
|
lastSelectedObject = selectedObject
|
|
else
|
|
lastSelectedObject = nil
|
|
end
|
|
end)
|
|
end
|
|
else
|
|
--[APIs for dynamic scrolling grid]--
|
|
--Set Item Call back, the item will be generated when it's in the scrolling area
|
|
this.targetCanvasPosition = Vector2.new(0, 0)
|
|
this.gridCount = config.gridCount or 0
|
|
|
|
--The selection mode decides how we cope with SelectedCoreObject change
|
|
--Middle: Keep the selection in the middle of the scrolling area if possible, make some offset on each side
|
|
--TopLeft: Keep the selection on top/left edge of scrolling area if possible
|
|
--Normal: Behave like normal scrolling grid
|
|
this.Enum.SelectionMode = {["Middle"] = 1; ["TopLeft"] = 2; ["Normal"] = 3;}
|
|
this.SelectionMode = (config.SelectionMode and this.Enum.SelectionMode[config.SelectionMode]) or this.Enum.SelectionMode.Normal
|
|
|
|
function this:GetItemVisible(item, fully)
|
|
if fully then --Whether item is fully visible
|
|
if this.ScrollDirection == this.Enum.ScrollDirection.Vertical then
|
|
return item.Position.Y.Offset >= self.Container.CanvasPosition.Y and
|
|
item.Position.Y.Offset + item.AbsoluteSize.Y <= self.Container.CanvasPosition.Y + self.Container.AbsoluteWindowSize.Y
|
|
else
|
|
return item.Position.X.Offset >= self.Container.CanvasPosition.X and
|
|
item.Position.X.Offset + item.AbsoluteSize.X <= self.Container.CanvasPosition.X + self.Container.AbsoluteWindowSize.X
|
|
end
|
|
else
|
|
if this.ScrollDirection == this.Enum.ScrollDirection.Vertical then
|
|
return item.Position.Y.Offset < self.Container.CanvasPosition.Y + self.Container.AbsoluteWindowSize.Y or
|
|
item.Position.Y.Offset + item.AbsoluteSize.Y > self.Container.CanvasPosition.Y
|
|
else
|
|
return item.Position.X.Offset < self.Container.CanvasPosition.X + self.Container.AbsoluteWindowSize.X or
|
|
item.Position.X.Offset + item.AbsoluteSize.X > self.Container.CanvasPosition.X
|
|
end
|
|
end
|
|
end
|
|
|
|
function this:SetItemCallback(callback, recalc)
|
|
self.getItemFunc = callback
|
|
if recalc then
|
|
self:RecalcLayout()
|
|
end
|
|
end
|
|
|
|
function this:GetNumRowsColumns()
|
|
local rows, columns = 0, 0
|
|
|
|
local windowSize = self.Container.AbsoluteWindowSize
|
|
local padding = self:GetPadding()
|
|
local cellSize = self:GetCellSize()
|
|
local cellSpacing = self:GetSpacing()
|
|
local adjustedWindowSize = Utility.ClampVector2(Vector2.new(0, 0), windowSize - padding, windowSize - padding)
|
|
local absoluteCellSize = Utility.ClampVector2(Vector2.new(1,1), cellSize + cellSpacing, cellSize + cellSpacing)
|
|
local windowSizeCalc = (adjustedWindowSize + cellSpacing) / absoluteCellSize
|
|
|
|
local GridItemsCount = 0
|
|
for _, item in pairs(self.GridItems) do
|
|
if item then
|
|
GridItemsCount = GridItemsCount + 1
|
|
end
|
|
end
|
|
|
|
if self.ScrollDirection == self.Enum.ScrollDirection.Vertical then
|
|
columns = math.max(1, math.floor(windowSizeCalc.x))
|
|
rows = math.ceil(math.max(1, GridItemsCount) / columns)
|
|
else
|
|
rows = math.max(1, math.floor(windowSizeCalc.y))
|
|
columns = math.ceil(math.max(1, GridItemsCount) / rows)
|
|
end
|
|
|
|
return rows, columns
|
|
end
|
|
|
|
function this:GetIndexFrom2D(row, column)
|
|
local rows, columns = self:GetNumRowsColumns()
|
|
|
|
local result = 0;
|
|
|
|
if self.ScrollDirection == self.Enum.ScrollDirection.Vertical then
|
|
result = (row-1) * columns + column
|
|
else
|
|
result = (column-1) * rows + row
|
|
end
|
|
|
|
return result
|
|
end
|
|
|
|
function this:GetItemRowColumnFromScreenPosition(x, y)
|
|
local cellSize = self:GetCellSize()
|
|
local spacing = self:GetSpacing()
|
|
local padding = self:GetPadding()
|
|
|
|
local cellWidth = (spacing.X + cellSize.X)
|
|
local cellHeight = (spacing.Y + cellSize.Y)
|
|
|
|
return math.floor(y / cellHeight) + 1, math.floor(x / cellWidth) + 1
|
|
end
|
|
|
|
function this:Add(index, gridItem)
|
|
self.GridItems[index] = gridItem
|
|
gridItem.Parent = self.Container
|
|
self.ItemSet[gridItem] = true
|
|
end
|
|
|
|
function this:Remove(index)
|
|
local gridItem = self.GridItems[index]
|
|
self.GridItems[index] = nil
|
|
if gridItem then
|
|
gridItem.Parent = nil
|
|
if GuiService.SelectedCoreObject == gridItem then
|
|
Utility.SetSelectedCoreObject(nil)
|
|
end
|
|
self.ItemSet[gridItem] = nil
|
|
end
|
|
end
|
|
|
|
function this:GetActiveItemsRange(overwriteWindowSize)
|
|
local windowSize = overwriteWindowSize or self.Container.AbsoluteWindowSize
|
|
local windowWidth = windowSize.X
|
|
local windowHeight = windowSize.Y
|
|
local canvasPosition = self.targetCanvasPosition
|
|
local x = canvasPosition.X
|
|
local y = canvasPosition.Y
|
|
local cellSize = self:GetCellSize()
|
|
local spacing = self:GetSpacing()
|
|
local padding = self:GetPadding()
|
|
|
|
local cellWidth = (spacing.X + cellSize.X)
|
|
local cellHeight = (spacing.Y + cellSize.Y)
|
|
|
|
local left = math.floor( x / cellWidth ) + 1
|
|
local right = math.ceil( (x + windowWidth) / cellWidth )
|
|
|
|
local top = math.floor( y / cellHeight ) + 1
|
|
local bottom = math.ceil( (y + windowHeight) / cellHeight )
|
|
|
|
local firstIndex = self:GetIndexFrom2D(top, left)
|
|
local lastIndex = self:GetIndexFrom2D(bottom, right)
|
|
|
|
local rows, columns = self:GetNumRowsColumns()
|
|
|
|
-- add a row/column of padding on either side
|
|
if self.ScrollDirection == self.Enum.ScrollDirection.Vertical then
|
|
lastIndex = lastIndex + columns
|
|
firstIndex = firstIndex - columns
|
|
else
|
|
lastIndex = lastIndex + rows
|
|
firstIndex = firstIndex - rows
|
|
end
|
|
|
|
-- clamp to 1...
|
|
if firstIndex < 1 then firstIndex = 1 end
|
|
if lastIndex < 1 then lastIndex = 1 end
|
|
|
|
return firstIndex, lastIndex
|
|
end
|
|
|
|
--Re-allocate the griditems based on (target) canvasposition
|
|
function this:Rewindow(overwriteWindowSize)
|
|
if self.getItemFunc then
|
|
local removeMe = {}
|
|
local moveMe = {}
|
|
|
|
for index, gridItem in pairs(self.GridItems) do
|
|
removeMe[gridItem] = index
|
|
end
|
|
|
|
local addMe = {}
|
|
local firstIndex, lastIndex = self:GetActiveItemsRange(overwriteWindowSize)
|
|
for index = firstIndex, lastIndex do
|
|
local gridItem = self.getItemFunc(index)
|
|
if gridItem then
|
|
local prevIndex = removeMe[gridItem]
|
|
if prevIndex then --It's already there.
|
|
removeMe[gridItem] = nil
|
|
if prevIndex ~= index then
|
|
moveMe[prevIndex] = index
|
|
end
|
|
else
|
|
addMe[gridItem] = index
|
|
end
|
|
end
|
|
end
|
|
|
|
for gridItem, index in pairs(removeMe) do
|
|
self:Remove(index)
|
|
end
|
|
|
|
for fromIndex, toIndex in pairs(moveMe) do
|
|
local gridItem = self.GridItems[fromIndex]
|
|
addMe[gridItem] = toIndex
|
|
self.GridItems[fromIndex] = nil
|
|
end
|
|
|
|
for gridItem, index in pairs(addMe) do
|
|
self:Add(index, gridItem)
|
|
end
|
|
|
|
for index, gridItem in pairs(self.GridItems) do
|
|
local row, column = self:Get2DGridIndex(index)
|
|
gridItem.Position = self:GetGridPosition(row, column)
|
|
end
|
|
end
|
|
end
|
|
|
|
function this:GetSelectableItem(includeContainer, prevSelectedIndex)
|
|
local windowSize = self.Container.AbsoluteWindowSize
|
|
local width = windowSize.X
|
|
local height = windowSize.Y
|
|
local centerRow, centerColumn = self:GetItemRowColumnFromScreenPosition(
|
|
self.Container.CanvasPosition.X + width / 2,
|
|
self.Container.CanvasPosition.Y + height / 2)
|
|
|
|
-- Find the item closest to the top left
|
|
local bestGridItem = nil
|
|
local score = math.huge
|
|
|
|
for i, gridItem in pairs(self.GridItems) do
|
|
local newScore = score
|
|
if prevSelectedIndex then
|
|
--If has prevSelectedIndex, select the gridItem with nearest Index
|
|
newScore = math.abs(i - prevSelectedIndex)
|
|
else
|
|
local row, column = self:Get2DGridIndex(i)
|
|
newScore = math.abs(column) + math.abs(row)
|
|
end
|
|
|
|
-- Favor on-screen items
|
|
if gridItem.Position.X.Offset < this.targetCanvasPosition.X or
|
|
gridItem.Position.Y.Offset < this.targetCanvasPosition.Y then
|
|
newScore = newScore + 10000
|
|
end
|
|
|
|
if newScore < score then
|
|
bestGridItem = gridItem
|
|
score = newScore
|
|
end
|
|
end
|
|
|
|
local selectableItem = bestGridItem or (includeContainer and self.Container)
|
|
return selectableItem
|
|
end
|
|
|
|
|
|
function this:SelectAvailableItem(includeContainer, prevSelectedIndex)
|
|
local selectableItem = self:GetSelectableItem(includeContainer, prevSelectedIndex)
|
|
if selectableItem then
|
|
Utility.SetSelectedCoreObject(selectableItem)
|
|
end
|
|
end
|
|
|
|
function this:Focus()
|
|
local selectedObject = self:FindAncestorGridItem(GuiService.SelectedCoreObject)
|
|
|
|
if not( selectedObject and self:ContainsItem(selectedObject) ) then
|
|
self:SelectAvailableItem()
|
|
end
|
|
end
|
|
|
|
function this:RemoveFocus()
|
|
if this:ContainsItem(GuiService.SelectedCoreObject) then
|
|
Utility.SetSelectedCoreObject(nil)
|
|
end
|
|
end
|
|
|
|
function this:RecalcLayout(newGridCount)
|
|
if newGridCount then
|
|
self.gridCount = newGridCount
|
|
end
|
|
local prevSelectedObject = self:FindAncestorGridItem(GuiService.SelectedCoreObject)
|
|
local wasSelected = GuiService.SelectedCoreObject == self.Container or (prevSelectedObject and self:ContainsItem(prevSelectedObject))
|
|
local prevSelectedIndex = nil
|
|
if wasSelected and prevSelectedObject then
|
|
local prevRow, prevColumn = self:GetItemRowColumnFromScreenPosition(prevSelectedObject.Position.X.Offset, prevSelectedObject.Position.Y.Offset)
|
|
prevSelectedIndex = self:GetIndexFrom2D(prevRow, prevColumn)
|
|
end
|
|
|
|
--Recalc the proper CanvasSize
|
|
local padding = self:GetPadding()
|
|
local cellSpacing = self:GetSpacing()
|
|
local gridItemSize = self:GetGridItemSize()
|
|
local rows, columns = self:GetNumRowsColumns()
|
|
|
|
--Need overwrite the gridCount as there hasn't been that many grid items in the scrolligGrid when recalc
|
|
if self.gridCount then
|
|
if self.ScrollDirection == self.Enum.ScrollDirection.Vertical then
|
|
rows = math.ceil(math.max(1, self.gridCount) / columns)
|
|
else
|
|
columns = math.ceil(math.max(1, self.gridCount) / rows)
|
|
end
|
|
end
|
|
|
|
if self.ScrollDirection == self.Enum.ScrollDirection.Vertical then
|
|
self.Container.CanvasSize = UDim2.new(self.Container.Size.X.Scale, self.Container.Size.X.Offset, 0, padding.Y * 2 + rows * gridItemSize.Y + (math.max(0, rows - 1)) * cellSpacing.Y)
|
|
else
|
|
self.Container.CanvasSize = UDim2.new(0, padding.X * 2 + columns * gridItemSize.X + (math.max(0, columns - 1)) * cellSpacing.X, self.Container.Size.Y.Scale, self.Container.Size.Y.Offset)
|
|
end
|
|
|
|
--The previous target canvasPos may become non-reachable now
|
|
self.targetCanvasPosition = Utility.ClampCanvasPosition(self.Container, self.targetCanvasPosition)
|
|
|
|
--Re allocate grid items
|
|
self:Rewindow()
|
|
|
|
local selectedObject = self:FindAncestorGridItem(GuiService.SelectedCoreObject)
|
|
if selectedObject and self:ContainsItem(selectedObject) then
|
|
--The selected object is still in the scrolling grid
|
|
local thisPos = self:GetCanvasPositionForOffscreenItem(selectedObject)
|
|
if thisPos then
|
|
self.targetCanvasPosition = thisPos
|
|
--Here use the tween to overwrite and stop all other tweens, replace this with TweenService
|
|
Utility.PropertyTweener(self.Container, 'CanvasPosition', thisPos, thisPos, 0.0, Utility.EaseOutQuad, true,
|
|
function()
|
|
self:Rewindow()
|
|
end)
|
|
end
|
|
elseif wasSelected then
|
|
--Selected object got removed, select the nearest gridItem or container
|
|
self:SelectAvailableItem(true, prevSelectedIndex)
|
|
end
|
|
end
|
|
|
|
function this:BackToInitial(duration)
|
|
if not duration then
|
|
duration = 0
|
|
end
|
|
|
|
local origin = Vector2.new(0, 0)
|
|
local overwriteWindowSize = self.Container.AbsoluteWindowSize + self.targetCanvasPosition
|
|
self.targetCanvasPosition = origin
|
|
|
|
-- Rewindow to include all items in grid up to the current position.
|
|
self:Rewindow(overwriteWindowSize)
|
|
|
|
-- Then animate the move to the origin, and after the animation, rewindow again to fit the view
|
|
Utility.PropertyTweener(self.Container, 'CanvasPosition', self.Container.CanvasPosition, origin, duration, Utility.SCurveVector2, true,
|
|
function()
|
|
self:Rewindow()
|
|
end)
|
|
end
|
|
|
|
GuiService:GetPropertyChangedSignal('SelectedCoreObject'):connect(function()
|
|
local selectedObject = this:FindAncestorGridItem(GuiService.SelectedCoreObject)
|
|
|
|
if selectedObject and this:ContainsItem(selectedObject) then
|
|
local nextPos = nil
|
|
local row, column = this:GetItemRowColumnFromScreenPosition(selectedObject.Position.X.Offset, selectedObject.Position.Y.Offset)
|
|
local cellSize = this:GetCellSize()
|
|
local spacing = this:GetSpacing()
|
|
local cellWidth = (spacing.X + cellSize.X)
|
|
local cellHeight = (spacing.Y + cellSize.Y)
|
|
|
|
if this.SelectionMode == this.Enum.SelectionMode.Middle then
|
|
local windowSize = this.Container.AbsoluteWindowSize
|
|
local width = windowSize.X
|
|
local height = windowSize.Y
|
|
local centerRow, centerColumn = this:GetItemRowColumnFromScreenPosition( this.Container.CanvasPosition.X + width / 2, this.Container.CanvasPosition.Y + height / 2)
|
|
|
|
if this.ScrollDirection == this.Enum.ScrollDirection.Vertical then
|
|
local maxaway = math.floor( (math.floor(height / cellHeight) - 1) / 2 );
|
|
|
|
if row > centerRow + maxaway then
|
|
local newCenter = row - maxaway
|
|
nextPos = Vector2.new(0, (newCenter-0.5) * cellHeight - height / 2)
|
|
elseif row < centerRow - maxaway then
|
|
local newCenter = row + maxaway
|
|
nextPos = Vector2.new(0, (newCenter-0.5) * cellHeight - height / 2)
|
|
end
|
|
else
|
|
local maxaway = math.floor( (math.floor(width / cellWidth) - 1) / 2 );
|
|
|
|
if column > centerColumn + maxaway then
|
|
local newCenter = column - maxaway
|
|
nextPos = Vector2.new((newCenter-0.5) * cellWidth - width / 2, 0)
|
|
elseif column < centerColumn - maxaway then
|
|
local newCenter = column + maxaway
|
|
nextPos = Vector2.new((newCenter-0.5) * cellWidth - width / 2, 0)
|
|
end
|
|
end
|
|
elseif this.SelectionMode == this.Enum.SelectionMode.TopLeft then
|
|
if this.ScrollDirection == this.Enum.ScrollDirection.Vertical then
|
|
nextPos = Vector2.new(0, (row-1) * cellHeight)
|
|
else
|
|
nextPos = Vector2.new((column-1) * cellWidth, 0)
|
|
end
|
|
else
|
|
nextPos = this:GetCanvasPositionForOffscreenItem(selectedObject)
|
|
end
|
|
|
|
if nextPos and nextPos.X == this.targetCanvasPosition.X and nextPos.Y == this.targetCanvasPosition.Y then
|
|
return
|
|
end
|
|
|
|
if nextPos then
|
|
local oldTargetCanvasPosition = this.targetCanvasPosition or this.Container.CanvasPosition
|
|
nextPos = Utility.ClampCanvasPosition(this.Container, nextPos)
|
|
this.targetCanvasPosition = nextPos
|
|
this:Rewindow()
|
|
Utility.PropertyTweener(this.Container, 'CanvasPosition', oldTargetCanvasPosition, nextPos, 0.2, Utility.EaseOutQuad, true)
|
|
end
|
|
end
|
|
end)
|
|
end
|
|
|
|
return this
|
|
end
|
|
|
|
return ScrollingGrid
|