-- 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