add gs
This commit is contained in:
+12
@@ -0,0 +1,12 @@
|
||||
--[[
|
||||
Package link auto-generated by Rotriever
|
||||
]]
|
||||
local PackageIndex = script.Parent.Parent
|
||||
|
||||
local package = PackageIndex["roblox_cryo"]["cryo"]
|
||||
|
||||
if package.ClassName == "ModuleScript" then
|
||||
return require(package)
|
||||
end
|
||||
|
||||
return package
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
--[[
|
||||
Package link auto-generated by Rotriever
|
||||
]]
|
||||
local PackageIndex = script.Parent.Parent
|
||||
|
||||
local package = PackageIndex["roblox_roact-fit-components"]["roact-fit-components"]
|
||||
|
||||
if package.ClassName == "ModuleScript" then
|
||||
return require(package)
|
||||
end
|
||||
|
||||
return package
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
--[[
|
||||
Package link auto-generated by Rotriever
|
||||
]]
|
||||
local PackageIndex = script.Parent.Parent
|
||||
|
||||
local package = PackageIndex["roblox_otter"]["otter"]
|
||||
|
||||
if package.ClassName == "ModuleScript" then
|
||||
return require(package)
|
||||
end
|
||||
|
||||
return package
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
--[[
|
||||
Package link auto-generated by Rotriever
|
||||
]]
|
||||
local PackageIndex = script.Parent.Parent
|
||||
|
||||
local package = PackageIndex["roblox_roact"]["roact"]
|
||||
|
||||
if package.ClassName == "ModuleScript" then
|
||||
return require(package)
|
||||
end
|
||||
|
||||
return package
|
||||
+80
@@ -0,0 +1,80 @@
|
||||
--[[
|
||||
KeyPool provides a pool of objects suitable for use as map keys.
|
||||
|
||||
Create a new KeyPool, then call pool:get() to get a new key. Once you're done with it, call key:release().
|
||||
|
||||
Example:
|
||||
local pool = KeyPool.new("foo")
|
||||
...
|
||||
local key1 = pool:get()
|
||||
local key2 = pool:get()
|
||||
map[key1] = thing1
|
||||
map[key2] = thing2
|
||||
...
|
||||
map[key1] = nil
|
||||
key1:release()
|
||||
key1 = nil
|
||||
]]
|
||||
local Root = script:FindFirstAncestor("infinite-scroller").Parent
|
||||
local t = require(Root.t)
|
||||
|
||||
-- Forward declarations
|
||||
local KeyPool = {}
|
||||
KeyPool.__index = KeyPool
|
||||
|
||||
local Key = {}
|
||||
Key.__index = Key
|
||||
|
||||
-- This is Key.new, but we don't want to expose that publicly.
|
||||
local function newkey(pool, index)
|
||||
local key = {
|
||||
pool = pool,
|
||||
index = index,
|
||||
}
|
||||
|
||||
setmetatable(key, Key)
|
||||
return key
|
||||
end
|
||||
|
||||
-- KeyPool functions
|
||||
|
||||
function KeyPool.new(class)
|
||||
assert(t.string(class))
|
||||
|
||||
local pool = {
|
||||
class = class,
|
||||
available = {},
|
||||
limit = 0,
|
||||
count = 0,
|
||||
}
|
||||
|
||||
setmetatable(pool, KeyPool)
|
||||
return pool
|
||||
end
|
||||
|
||||
-- Get a currently unused key, or create a new one if everything is in use.
|
||||
function KeyPool:get()
|
||||
if self.count == 0 then
|
||||
self.limit = self.limit + 1
|
||||
return newkey(self, self.limit)
|
||||
end
|
||||
|
||||
local key = self.available[self.count]
|
||||
self.count = self.count - 1
|
||||
return key
|
||||
end
|
||||
|
||||
-- Key functions
|
||||
|
||||
function Key:__tostring()
|
||||
return self.pool.class .. "_" .. tostring(self.index)
|
||||
end
|
||||
|
||||
-- Return this key to the pool it came from. Whatever previously held this key should not keep the reference after
|
||||
-- calling this.
|
||||
function Key:release()
|
||||
self.pool.count = self.pool.count + 1
|
||||
self.pool.available[self.pool.count] = self
|
||||
end
|
||||
|
||||
return KeyPool
|
||||
+1
@@ -0,0 +1 @@
|
||||
return {}
|
||||
+31
@@ -0,0 +1,31 @@
|
||||
-- Enum for specifying the leading edge of the scroller.
|
||||
local Root = script:FindFirstAncestor("infinite-scroller").Parent
|
||||
local t = require(Root.t)
|
||||
|
||||
local Orientation = {
|
||||
Up = "Orientation.Up",
|
||||
Down = "Orientation.Down",
|
||||
Left = "Orientation.Left",
|
||||
Right = "Orientation.Right",
|
||||
}
|
||||
|
||||
local metaindex = {
|
||||
isOrientation = t.union(
|
||||
t.literal(Orientation.Up),
|
||||
t.literal(Orientation.Down),
|
||||
t.literal(Orientation.Left),
|
||||
t.literal(Orientation.Right)
|
||||
)
|
||||
}
|
||||
|
||||
setmetatable(Orientation, {
|
||||
__index = function(self, key)
|
||||
return metaindex[key] or
|
||||
error(tostring(key) .. " is not a valid member of Scroller.Orientation", 2)
|
||||
end,
|
||||
__newindex = function()
|
||||
error("Scroller.Orientation is read-only", 2)
|
||||
end,
|
||||
})
|
||||
|
||||
return Orientation
|
||||
+28
@@ -0,0 +1,28 @@
|
||||
local epsilon = 1e-15
|
||||
|
||||
return {
|
||||
nearest = function(num)
|
||||
local q, r = math.modf(num)
|
||||
if r <= -0.5 then
|
||||
return q - 1
|
||||
elseif r >= 0.5 then
|
||||
return q + 1
|
||||
else
|
||||
return q
|
||||
end
|
||||
end,
|
||||
towardsZero = function(num)
|
||||
local result, _ = math.modf(num)
|
||||
return result
|
||||
end,
|
||||
awayFromZero = function(num)
|
||||
local q, r = math.modf(num)
|
||||
if r < -epsilon then
|
||||
return q - 1
|
||||
elseif r > epsilon then
|
||||
return q + 1
|
||||
else
|
||||
return q
|
||||
end
|
||||
end,
|
||||
}
|
||||
+866
@@ -0,0 +1,866 @@
|
||||
local RunService = game:GetService("RunService")
|
||||
|
||||
local Root = script:FindFirstAncestor("infinite-scroller").Parent
|
||||
local Roact = require(Root.Roact)
|
||||
local Cryo = require(Root.Cryo)
|
||||
local t = require(Root.t)
|
||||
|
||||
local FitFrame = require(Root.FitFrame).FitFrameOnAxis
|
||||
local findNewIndices = require(script.Parent.findNewIndices)
|
||||
local Round = require(script.Parent.Round)
|
||||
local KeyPool = require(script.Parent.KeyPool)
|
||||
|
||||
local NotifyReady = require(script.Parent.NotifyReady)
|
||||
|
||||
local debugPrint = function() end
|
||||
|
||||
local Scroller = Roact.PureComponent:extend("Scroller")
|
||||
|
||||
Scroller.Orientation = require(script.Parent.Orientation)
|
||||
|
||||
local isVertical = {
|
||||
[Scroller.Orientation.Up] = true,
|
||||
[Scroller.Orientation.Down] = true,
|
||||
[Scroller.Orientation.Left] = false,
|
||||
[Scroller.Orientation.Right] = false,
|
||||
}
|
||||
|
||||
local isReverse = {
|
||||
[Scroller.Orientation.Up] = true,
|
||||
[Scroller.Orientation.Down] = false,
|
||||
[Scroller.Orientation.Left] = true,
|
||||
[Scroller.Orientation.Right] = false,
|
||||
}
|
||||
|
||||
local direction = {
|
||||
[Scroller.Orientation.Up] = -1,
|
||||
[Scroller.Orientation.Down] = 1,
|
||||
[Scroller.Orientation.Left] = -1,
|
||||
[Scroller.Orientation.Right] = 1,
|
||||
}
|
||||
|
||||
Scroller.validateProps = t.strictInterface({
|
||||
-- Required. The list of items to scroll through.
|
||||
itemList = t.array(t.any),
|
||||
|
||||
-- Required. A callback function, called with each visible item in the itemList when the list is rendered.
|
||||
renderItem = t.callback,
|
||||
|
||||
-- A function to uniquely identify list items. Calling this on the same item twice should give the same result
|
||||
-- accoring to ==.
|
||||
identifier = t.optional(t.callback),
|
||||
|
||||
-- One of the Scroller.Orientation enums. Determines the leading edge of the infinite scroll.
|
||||
orientation = t.optional(Scroller.Orientation.isOrientation),
|
||||
|
||||
-- A callback function, called when the infinite scroll reaches the leading end of the itemList (index
|
||||
-- #itemList).
|
||||
loadNext = t.optional(t.callback),
|
||||
|
||||
-- A callback function, called when the infinite scroll reaches the trailing end of the itemList (index 1).
|
||||
loadPrevious = t.optional(t.callback),
|
||||
|
||||
-- Padding between elements in the scrolling frame. The Scale is relative to the size of the scrolling frame.
|
||||
padding = t.optional(t.UDim),
|
||||
|
||||
-- The minimum number of unmounted elements to keep at the top and bottom of the list. If there are fewer than
|
||||
-- this call loadNext or loadPrevious.
|
||||
loadingBuffer = t.optional(t.numberPositive),
|
||||
|
||||
-- The amount of space above and below the view to render items in.
|
||||
mountingBuffer = t.optional(t.numberPositive),
|
||||
|
||||
-- The amount of empty space to keep at the top and bottom on the scroll.
|
||||
dragBuffer = t.optional(t.numberPositive),
|
||||
|
||||
-- An initial guess at the average size of an item.
|
||||
estimatedItemSize = t.optional(t.numberPositive),
|
||||
|
||||
-- The maximum distance to search for moved elements.
|
||||
maximumSearchDistance = t.optional(t.numberPositive),
|
||||
|
||||
-- The element to put in focus initially.
|
||||
focusIndex = t.optional(t.integer),
|
||||
|
||||
-- An arbitrary value to prevent the list from refocusing every render. Change this to cause the list to reset
|
||||
-- and refocus on the new focusIndex.
|
||||
focusLock = t.optional(t.any),
|
||||
|
||||
-- The position within the view to keep still as other things move. The Scale is relative to the size of the
|
||||
-- scrolling frame.
|
||||
anchorLocation = t.optional(t.UDim),
|
||||
|
||||
---- INTERNAL ONLY ----
|
||||
[NotifyReady] = t.any,
|
||||
})
|
||||
|
||||
-- Default values for all the infinite-scroller-specific props. Any prop not in this list will be passed on to the
|
||||
-- underlying ScrollingFrame.
|
||||
Scroller.defaultProps = {
|
||||
itemList = {},
|
||||
renderItem = {},
|
||||
identifier = function(item)
|
||||
return item
|
||||
end,
|
||||
orientation = Scroller.Orientation.Down,
|
||||
loadNext = false,
|
||||
loadPrevious = false,
|
||||
padding = UDim.new(0, 0),
|
||||
loadingBuffer = 10,
|
||||
mountingBuffer = 200,
|
||||
dragBuffer = 100,
|
||||
estimatedItemSize = 50,
|
||||
maximumSearchDistance = 100,
|
||||
focusIndex = 1,
|
||||
focusLock = {},
|
||||
anchorLocation = UDim.new(0, 0),
|
||||
[NotifyReady] = false,
|
||||
}
|
||||
|
||||
function Scroller:render()
|
||||
debugPrint("render")
|
||||
|
||||
-- Gather vertical/horizontal specific variables.
|
||||
local axis = isVertical[self.props.orientation] and {
|
||||
fillDirection = Enum.FillDirection.Vertical,
|
||||
fitDirection = FitFrame.Axis.Vertical,
|
||||
minimumSize = UDim2.new(1, 0, 0, 0),
|
||||
canvasSize = UDim2.new(0, 0, 0, self.state.size),
|
||||
paddingSide = "PaddingTop",
|
||||
} or {
|
||||
fillDirection = Enum.FillDirection.Horizontal,
|
||||
fitDirection = FitFrame.Axis.Horizontal,
|
||||
minimumSize = UDim2.new(0, 0, 1, 0),
|
||||
canvasSize = UDim2.new(0, self.state.size, 0, 0),
|
||||
paddingSide = "PaddingLeft",
|
||||
}
|
||||
|
||||
-- Remove non-standard props from list to pass on to ScrollingFrame. These are the same props given in
|
||||
-- defaultProps.
|
||||
local props = Cryo.Dictionary.join(
|
||||
self.props,
|
||||
self.propsToClear,
|
||||
{
|
||||
CanvasSize = axis.canvasSize,
|
||||
[Roact.Change.CanvasPosition] = self.onScroll,
|
||||
[Roact.Change.AbsoluteSize] = self.onResize,
|
||||
[Roact.Ref] = self:getRef(),
|
||||
}
|
||||
)
|
||||
|
||||
local children = {
|
||||
layout = Roact.createElement("UIListLayout", {
|
||||
SortOrder = Enum.SortOrder.LayoutOrder,
|
||||
FillDirection = axis.fillDirection,
|
||||
Padding = UDim.new(0, self.itemPadding),
|
||||
[Roact.Change.AbsoluteContentSize] = self.onContentResize,
|
||||
}),
|
||||
padding = Roact.createElement("UIPadding", {
|
||||
[axis.paddingSide] = UDim.new(0, self.state.padding),
|
||||
}),
|
||||
}
|
||||
|
||||
-- Trailing and leading indicies won't be set if this isn't true.
|
||||
if self.state.ready and not Cryo.isEmpty(self.props.itemList) then
|
||||
debugPrint(" Rendering elements between", self.state.trail.index, "and", self.state.lead.index)
|
||||
for n = self.state.trail.index, self.state.lead.index do
|
||||
local metadata = self:getMetadata(n)
|
||||
children[metadata.name] = Roact.createElement(FitFrame, {
|
||||
minimumSize = axis.minimumSize,
|
||||
axis = axis.fitDirection,
|
||||
FillDirection = axis.fillDirection,
|
||||
BackgroundTransparency = 1,
|
||||
LayoutOrder = isReverse[self.props.orientation] and -n or n,
|
||||
[Roact.Ref] = metadata.ref,
|
||||
}, {
|
||||
item = self.props.renderItem(self.props.itemList[n], false),
|
||||
})
|
||||
end
|
||||
end
|
||||
|
||||
return Roact.createElement("ScrollingFrame", props, children)
|
||||
end
|
||||
|
||||
function Scroller:willUpdate(nextProps, nextState)
|
||||
debugPrint("willUpdate")
|
||||
|
||||
if not nextState.ready then
|
||||
return
|
||||
end
|
||||
|
||||
self.sizeDebounce = true
|
||||
|
||||
local deletions = {}
|
||||
local additions = {}
|
||||
|
||||
if not Cryo.isEmpty(self.props.itemList) and self.state.lead then
|
||||
for n = self.state.trail.index, self.state.lead.index do
|
||||
local id = self.props.identifier(self.props.itemList[n])
|
||||
deletions[id] = true
|
||||
end
|
||||
end
|
||||
|
||||
if not Cryo.isEmpty(nextProps.itemList) and nextState.lead then
|
||||
for n = nextState.trail.index, nextState.lead.index do
|
||||
local item = nextProps.itemList[n]
|
||||
local id = nextProps.identifier(item)
|
||||
if deletions[id] then
|
||||
-- Element is in both ranges.
|
||||
deletions[id] = nil
|
||||
else
|
||||
additions[id] = item
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- Clear names first, so new items can use them.
|
||||
for id, _ in pairs(deletions) do
|
||||
self:clearMetadata(id)
|
||||
end
|
||||
for id, item in pairs(additions) do
|
||||
self:updateMetadata(id, item, nextProps)
|
||||
end
|
||||
|
||||
-- The focus lock changed, clear the non-state anchor variables.
|
||||
if self.state.lastFocusLock ~= nextState.lastFocusLock then
|
||||
self.anchorFramePosition = 0
|
||||
self.anchorCanvasPosition = self.relativeAnchorLocation
|
||||
end
|
||||
end
|
||||
|
||||
function Scroller:didUpdate(previousProps, previousState)
|
||||
debugPrint("didUpdate")
|
||||
|
||||
if Cryo.isEmpty(self.props.itemList) then
|
||||
return
|
||||
end
|
||||
|
||||
if not self.state.ready then
|
||||
self.onResize(self:getRef().current)
|
||||
return
|
||||
end
|
||||
|
||||
if not self:adjustCanvas(self.scrollingForward, self.scrollingBackward) then
|
||||
self:moveToAnchor()
|
||||
self:loadMore()
|
||||
self.sizeDebounce = false
|
||||
end
|
||||
end
|
||||
|
||||
function Scroller.getDerivedStateFromProps(nextProps, lastState)
|
||||
debugPrint("getDerivedStateFromProps")
|
||||
if not lastState.ready or Cryo.isEmpty(nextProps.itemList) then
|
||||
return
|
||||
end
|
||||
|
||||
local listSize = #nextProps.itemList
|
||||
|
||||
-- Reset the state if the focus lock changes. This is guaranteed to be true the first time.
|
||||
if lastState.lastFocusLock ~= nextProps.focusLock then
|
||||
debugPrint(" Resetting focus")
|
||||
local focusID = nextProps.identifier(nextProps.itemList[nextProps.focusIndex])
|
||||
return {
|
||||
listSize = listSize,
|
||||
trail = {index=nextProps.focusIndex, id=focusID},
|
||||
anchor = {index=nextProps.focusIndex, id=focusID},
|
||||
lead = {index=nextProps.focusIndex, id=focusID},
|
||||
padding = 0,
|
||||
size = 0,
|
||||
lastFocusLock = nextProps.focusLock,
|
||||
}
|
||||
end
|
||||
|
||||
local trailIndex, anchorIndex, leadIndex = findNewIndices(nextProps, lastState)
|
||||
debugPrint(" Trailing index moved from", lastState.trail.index, "to", trailIndex)
|
||||
debugPrint(" Anchor index moved from", lastState.anchor.index, "to", anchorIndex)
|
||||
debugPrint(" Leading index moved from", lastState.lead.index, "to", leadIndex)
|
||||
|
||||
-- There are 8 possibilities here as any combination of these could be deleted. Also, we can't use findIndexAt
|
||||
-- here since that requires access to the children's measurements.
|
||||
if not anchorIndex then
|
||||
if leadIndex and trailIndex then
|
||||
-- Estimate that the new anchor is proportionally the same distance from the lead and trail indices.
|
||||
if leadIndex == trailIndex then
|
||||
-- Guard against divide by zero.
|
||||
anchorIndex = leadIndex
|
||||
else
|
||||
local oldRatio = (lastState.anchor.index - lastState.lead.index)
|
||||
/ (lastState.trail.index - lastState.lead.index)
|
||||
anchorIndex = Round.nearest((trailIndex - leadIndex) * oldRatio + leadIndex)
|
||||
anchorIndex = math.min(math.max(anchorIndex, 1), listSize)
|
||||
end
|
||||
elseif leadIndex then
|
||||
-- Given only the new leading index, estimate that the new anchor is the same distance away as it was.
|
||||
anchorIndex = leadIndex + lastState.anchor.index - lastState.lead.index
|
||||
anchorIndex = math.min(math.max(anchorIndex, 1), listSize)
|
||||
elseif trailIndex then
|
||||
-- Given only the new trailing index, estimate that the new anchor is the same distance away as it was.
|
||||
anchorIndex = trailIndex + lastState.anchor.index - lastState.trail.index
|
||||
anchorIndex = math.min(math.max(anchorIndex, 1), listSize)
|
||||
else
|
||||
-- Everything is gone. Just reuse the same index if that's still within the bounds of the list.
|
||||
anchorIndex = math.min(math.max(lastState.anchor.index, 1), listSize)
|
||||
end
|
||||
debugPrint(" Anchor index moved to", anchorIndex)
|
||||
end
|
||||
|
||||
-- If the leading and trailing indices haven't been worked out yet, estimate that the new ones should be the
|
||||
-- same distance from the anchor as the old ones were.
|
||||
if not trailIndex then
|
||||
trailIndex = anchorIndex + lastState.trail.index - lastState.anchor.index
|
||||
trailIndex = math.min(math.max(trailIndex, 1), listSize)
|
||||
debugPrint(" Trailing index moved to", trailIndex)
|
||||
end
|
||||
if not leadIndex then
|
||||
leadIndex = anchorIndex + lastState.lead.index - lastState.anchor.index
|
||||
leadIndex = math.min(math.max(leadIndex, 1), listSize)
|
||||
debugPrint(" Leading index moved to", leadIndex)
|
||||
end
|
||||
|
||||
local trailID = nextProps.identifier(nextProps.itemList[trailIndex])
|
||||
local anchorID = nextProps.identifier(nextProps.itemList[anchorIndex])
|
||||
local leadID = nextProps.identifier(nextProps.itemList[leadIndex])
|
||||
|
||||
return {
|
||||
listSize = listSize,
|
||||
trail = {index=trailIndex, id=trailID},
|
||||
anchor = {index=anchorIndex, id=anchorID},
|
||||
lead = {index=leadIndex, id=leadID},
|
||||
}
|
||||
end
|
||||
|
||||
function Scroller:init()
|
||||
debugPrint("init")
|
||||
|
||||
-- Only self:getRef() should access this.
|
||||
self._ref = Roact.createRef()
|
||||
|
||||
self.scrollDebounce = false
|
||||
self.sizeDebounce = true
|
||||
|
||||
self.onScroll = function(rbx)
|
||||
debugPrint("onScroll")
|
||||
debugPrint(" CanvasPosition is", rbx.CanvasPosition)
|
||||
if self.scrollDebounce then
|
||||
debugPrint(" Debouncing scroll")
|
||||
return
|
||||
end
|
||||
|
||||
local delta, newState = self:recalculateAnchor()
|
||||
self.scrollingBackward = delta < 0
|
||||
self.scrollingForward = delta > 0
|
||||
debugPrint(" Delta is", delta)
|
||||
|
||||
if not Cryo.isEmpty(newState) then
|
||||
self:setState(newState)
|
||||
end
|
||||
|
||||
-- Handle any passed in scroll callback.
|
||||
if self.props[Roact.Change.CanvasPosition] then
|
||||
self.props[Roact.Change.CanvasPosition](rbx)
|
||||
end
|
||||
end
|
||||
|
||||
self.onResize = function(rbx)
|
||||
debugPrint("onResize")
|
||||
local size = self:measure(rbx.AbsoluteSize)
|
||||
local pos = self:measure(rbx.AbsolutePosition)
|
||||
|
||||
self.itemPadding = self.props.padding.Scale * size + self.props.padding.Offset
|
||||
if isReverse[self.props.orientation] then
|
||||
self.relativeAnchorLocation = self.props.anchorLocation.Scale * size + self.props.anchorLocation.Offset
|
||||
else
|
||||
self.relativeAnchorLocation = (1 - self.props.anchorLocation.Scale) * size
|
||||
- self.props.anchorLocation.Offset
|
||||
end
|
||||
self.absoluteAnchorLocation = self.relativeAnchorLocation + pos
|
||||
self.mountAboveAnchor = self.relativeAnchorLocation + self.props.mountingBuffer
|
||||
self.mountBelowAnchor = size - self.relativeAnchorLocation + self.props.mountingBuffer
|
||||
|
||||
-- Handle any passed in resize callback.
|
||||
if self.props[Roact.Change.AbsoluteSize] then
|
||||
self.props[Roact.Change.AbsoluteSize](rbx)
|
||||
end
|
||||
|
||||
if not self.state.ready then
|
||||
debugPrint(" Setting initial anchor position to", self.relativeAnchorLocation)
|
||||
self.anchorFramePosition = 0
|
||||
self.anchorCanvasPosition = self.relativeAnchorLocation
|
||||
|
||||
coroutine.wrap(function()
|
||||
RunService.Heartbeat:Wait()
|
||||
self:setState({
|
||||
ready = true
|
||||
})
|
||||
|
||||
-- This should only be set by tests.
|
||||
if self.props[NotifyReady] then
|
||||
self.props[NotifyReady]:Fire()
|
||||
end
|
||||
end)()
|
||||
else
|
||||
self:moveToAnchor()
|
||||
self:setState({}) -- Force a rerender.
|
||||
end
|
||||
end
|
||||
|
||||
self.onContentResize = function()
|
||||
debugPrint("onContentResize")
|
||||
|
||||
if self.sizeDebounce or not self.state.ready then
|
||||
debugPrint(" Skipping onContentResize")
|
||||
return
|
||||
end
|
||||
|
||||
self:moveToAnchor()
|
||||
self:setState({}) -- Force a rerender.
|
||||
end
|
||||
|
||||
self.anchorCanvasPosition = 0
|
||||
self.anchorFramePosition = 0
|
||||
|
||||
self.metadata = {}
|
||||
self.pools = {}
|
||||
self.refpool = {}
|
||||
|
||||
self.scrollingBackward = false
|
||||
self.scrollingForward = false
|
||||
|
||||
-- Store the list of props to not pass on to the underlying scrolling frame.
|
||||
self.propsToClear = {}
|
||||
for k, _ in pairs(Scroller.defaultProps) do
|
||||
self.propsToClear[k] = Cryo.None
|
||||
end
|
||||
|
||||
-- This will get updated shortly, but one render will happen before state.ready is set
|
||||
self.state = {
|
||||
ready = false,
|
||||
lastFocusLock = nil,
|
||||
padding = 0,
|
||||
size = 0,
|
||||
}
|
||||
end
|
||||
|
||||
-- Find which element is currently closest to the anchor position.
|
||||
function Scroller:recalculateAnchor()
|
||||
debugPrint("recalculateAnchor")
|
||||
|
||||
-- Find the index of the element at the appropriate position
|
||||
local index = self:findIndexAt(
|
||||
self:absoluteToCanvasPosition(self.absoluteAnchorLocation), self.state.anchor.index, false)
|
||||
|
||||
local delta
|
||||
if index == self.state.anchor.index then
|
||||
debugPrint(" Current anchor still works")
|
||||
return 0, {}
|
||||
elseif index < self.state.anchor.index then
|
||||
delta = -1
|
||||
else
|
||||
delta = 1
|
||||
end
|
||||
|
||||
debugPrint(" New anchor at index", index)
|
||||
|
||||
-- Store the new anchor's details
|
||||
self.anchorCanvasPosition = self:getAnchorCanvasFromIndex(index)
|
||||
self.anchorFramePosition = self:getAnchorFrameFromIndex(index)
|
||||
debugPrint(" New anchor at canvas position", self.anchorCanvasPosition)
|
||||
debugPrint(" New anchor at frame position", self.anchorFramePosition)
|
||||
return delta, {
|
||||
anchor = {index=index, id=self:getID(index)},
|
||||
}
|
||||
end
|
||||
|
||||
-- Move all the rendered elements up or down to put the anchor back where it was.
|
||||
function Scroller:resetAnchorPosition()
|
||||
debugPrint("resetAnchorPosition")
|
||||
debugPrint(" Anchor index is", self.state.anchor.index)
|
||||
local offset = self:getAnchorCanvasPosition()
|
||||
debugPrint(" Anchor is at", offset)
|
||||
debugPrint(" Anchor should be at", self.anchorCanvasPosition)
|
||||
local diff = math.floor(self.anchorCanvasPosition - offset + 0.5)
|
||||
if diff ~= 0 then
|
||||
debugPrint(" Changing padding from", self.state.padding, "to", (self.state.padding + diff))
|
||||
return {padding = self.state.padding + diff}
|
||||
else
|
||||
return {}
|
||||
end
|
||||
end
|
||||
|
||||
-- Get the current padding from the UIPadding child.
|
||||
function Scroller:getCurrentPadding()
|
||||
local pad = self:getCurrent().padding
|
||||
-- Only one of these will be non-zero
|
||||
return pad.PaddingTop.Offset + pad.PaddingLeft.Offset
|
||||
end
|
||||
|
||||
-- Move the top and bottom of the range to be rendered up and down to make sure enough things are being rendered.
|
||||
function Scroller:recalculateBounds(trimTrailing, trimLeading)
|
||||
debugPrint("recalculateBounds")
|
||||
debugPrint(" Leading index was", self.state.lead.index)
|
||||
debugPrint(" Trailing index was", self.state.trail.index)
|
||||
|
||||
local anchorPos = self:getAnchorCanvasPosition()
|
||||
local mountTop = anchorPos - self.mountAboveAnchor
|
||||
local mountBottom = anchorPos + self.mountBelowAnchor
|
||||
debugPrint(" Target for top at", mountTop)
|
||||
debugPrint(" Target for bottom at", mountBottom)
|
||||
|
||||
local topIndex = self:findIndexAt(mountTop, nil, true)
|
||||
debugPrint(" Found new top index at", topIndex)
|
||||
local bottomIndex = self:findIndexAt(mountBottom, nil, true)
|
||||
debugPrint(" Found new bottom index at", bottomIndex)
|
||||
|
||||
local leadIndex = math.max(topIndex, bottomIndex)
|
||||
if leadIndex < self.state.lead.index and not trimLeading then
|
||||
leadIndex = self.state.lead.index
|
||||
end
|
||||
|
||||
local trailIndex = math.min(topIndex, bottomIndex)
|
||||
if trailIndex > self.state.trail.index and not trimTrailing then
|
||||
trailIndex = self.state.trail.index
|
||||
end
|
||||
|
||||
if trailIndex < self.state.trail.index or leadIndex > self.state.lead.index then
|
||||
debugPrint(" Changing leading index to", leadIndex)
|
||||
debugPrint(" Changing trailing index to", trailIndex)
|
||||
return {
|
||||
trail = {index=trailIndex, id=self:getID(trailIndex)},
|
||||
lead = {index=leadIndex, id=self:getID(leadIndex)},
|
||||
}
|
||||
else
|
||||
return {}
|
||||
end
|
||||
end
|
||||
|
||||
-- Find the index of the element that overlaps the given canvas-relative position.
|
||||
function Scroller:findIndexAt(targetPos, hintIndex, extrapolate)
|
||||
debugPrint(" findIndexAt")
|
||||
-- Get the distance from the hinted index or the anchor.
|
||||
local currentIndex = hintIndex or self.state.anchor.index
|
||||
local currentDist = self:distanceToPosition(currentIndex, targetPos)
|
||||
debugPrint(" Searching from index", currentIndex)
|
||||
debugPrint(" Position is", currentDist, "from", targetPos)
|
||||
if currentDist == 0 then
|
||||
return currentIndex
|
||||
end
|
||||
|
||||
-- Get the distance from one end of the list.
|
||||
local nextIndex = (currentDist < 0) and self.state.trail.index or self.state.lead.index
|
||||
debugPrint(" Nearest end at", nextIndex)
|
||||
if currentIndex == nextIndex then
|
||||
debugPrint(" Hint index already at end")
|
||||
-- If the target position lies outside of the loaded elements.
|
||||
if currentIndex + currentDist < self.state.trail.index
|
||||
or currentIndex + currentDist > self.state.lead.index then
|
||||
debugPrint(" Target out of bounds")
|
||||
if not extrapolate then
|
||||
-- Do not extrapolate. Return the closest loaded element.
|
||||
return currentIndex
|
||||
end
|
||||
|
||||
-- Extrapolate using the estimated item size.
|
||||
local delta = Round.awayFromZero(currentDist / self.props.estimatedItemSize)
|
||||
debugPrint(" Estimating target at", delta, "from end")
|
||||
return math.min(math.max(currentIndex + delta, 1), self.state.listSize)
|
||||
end
|
||||
else
|
||||
local nextDist = self:distanceToPosition(nextIndex, targetPos)
|
||||
debugPrint(" End is", nextDist, "from target")
|
||||
if nextDist == 0 then
|
||||
return nextIndex
|
||||
end
|
||||
|
||||
-- If the target position lies outside of the loaded elements.
|
||||
if currentDist * nextDist > 0 then
|
||||
debugPrint(" Target out of bounds")
|
||||
if not extrapolate then
|
||||
-- Do not extrapolate. Return the closest loaded element.
|
||||
return nextIndex
|
||||
end
|
||||
|
||||
-- Extrapolate using the estimated item size.
|
||||
local delta = Round.awayFromZero(nextDist / self.props.estimatedItemSize)
|
||||
debugPrint(" Estimating target at", delta, "from end")
|
||||
return math.min(math.max(nextIndex + delta, 1), self.state.listSize)
|
||||
end
|
||||
|
||||
-- Jump to the approximate location of the target based on the distance from current and next.
|
||||
local totalDist = math.abs(currentDist) + math.abs(nextDist)
|
||||
local indexCount = math.abs(currentIndex - nextIndex)
|
||||
currentIndex = currentIndex + Round.nearest(indexCount * currentDist / totalDist)
|
||||
currentDist = self:distanceToPosition(currentIndex, targetPos)
|
||||
debugPrint(" Interpolated index is", currentIndex)
|
||||
debugPrint(" Distance from interpolated index is", currentDist)
|
||||
end
|
||||
|
||||
-- Linear search from best guess index.
|
||||
while currentDist ~= 0 do
|
||||
if currentDist < 0 then
|
||||
currentIndex = currentIndex - 1
|
||||
else
|
||||
currentIndex = currentIndex + 1
|
||||
end
|
||||
currentDist = self:distanceToPosition(currentIndex, targetPos)
|
||||
debugPrint(" Distance after step is", currentDist)
|
||||
end
|
||||
|
||||
return currentIndex
|
||||
end
|
||||
|
||||
-- Expand the size of the scrolling frame's canvas to make sure everything still fits.
|
||||
function Scroller:expandCanvas(newState)
|
||||
debugPrint("expandCanvas")
|
||||
local reverse = isReverse[self.props.orientation]
|
||||
local bottomIndex = reverse and self.state.trail.index or self.state.lead.index
|
||||
|
||||
local size = self.state.size
|
||||
local newPadding = newState.padding or self.state.padding
|
||||
local oldPadding = self:getCurrentPadding()
|
||||
|
||||
local bottomPos = self:getChildCanvasPosition(bottomIndex)
|
||||
+ self:getChildSize(bottomIndex) - (oldPadding - newPadding)
|
||||
debugPrint(" Bottom of bottom child is", bottomPos)
|
||||
debugPrint(" Canvas size is", self.state.size)
|
||||
debugPrint(" Canvas bottom should be", bottomPos + self.props.dragBuffer)
|
||||
if bottomPos > self.state.size - self.props.dragBuffer then
|
||||
-- Plus footer
|
||||
size = bottomPos + self.props.dragBuffer
|
||||
debugPrint(" Expanding canvas bottom to size", size)
|
||||
end
|
||||
|
||||
debugPrint(" Padding is", newPadding)
|
||||
debugPrint(" Padding should be", self.props.dragBuffer)
|
||||
if newPadding < self.props.dragBuffer then
|
||||
-- Minus header
|
||||
local diff = newPadding - self.props.dragBuffer
|
||||
size = size - diff
|
||||
self.anchorCanvasPosition = self.anchorCanvasPosition - diff
|
||||
newPadding = self.props.dragBuffer
|
||||
debugPrint(" Expanding canvas top to size", size)
|
||||
debugPrint(" Shifting anchor to", self.anchorCanvasPosition)
|
||||
debugPrint(" Padding is now", newPadding)
|
||||
end
|
||||
|
||||
if size ~= self.state.size or newPadding ~= self.state.padding then
|
||||
debugPrint(" Changing size from", self.state.size, "to", size)
|
||||
debugPrint(" Changing padding from", self.state.padding, "to", newPadding)
|
||||
return {
|
||||
size = size,
|
||||
padding = newPadding,
|
||||
}
|
||||
else
|
||||
return {}
|
||||
end
|
||||
end
|
||||
|
||||
-- Try and get the canvas as close to correct as possible this rendering pass.
|
||||
function Scroller:adjustCanvas(trimTrailing, trimLeading)
|
||||
debugPrint("adjustCanvas")
|
||||
|
||||
local newState = Cryo.Dictionary.join(
|
||||
self:resetAnchorPosition(),
|
||||
self:recalculateBounds(trimTrailing, trimLeading)
|
||||
)
|
||||
|
||||
if not newState.trail and not newState.lead then
|
||||
newState = Cryo.Dictionary.join(newState, self:expandCanvas(newState))
|
||||
end
|
||||
|
||||
if Cryo.isEmpty(newState) then
|
||||
return false
|
||||
end
|
||||
|
||||
self:setState(newState)
|
||||
return true
|
||||
end
|
||||
|
||||
-- Move the cavnas position so that the anchor element is in the same place on the screen.
|
||||
function Scroller:moveToAnchor()
|
||||
debugPrint("moveToAnchor")
|
||||
local currentPos = self:getAnchorFramePosition()
|
||||
debugPrint(" Anchor was at frame position", self.anchorFramePosition)
|
||||
debugPrint(" Anchor is currently at frame position", currentPos)
|
||||
self:setScroll(self:measure(self:getCurrent().CanvasPosition) + currentPos - self.anchorFramePosition)
|
||||
end
|
||||
|
||||
-- Call loadNext and loadPrevious if needed.
|
||||
function Scroller:loadMore()
|
||||
debugPrint("loadMore")
|
||||
if self.props.loadPrevious and self.state.trail.index <= self.props.loadingBuffer then
|
||||
debugPrint(" Calling loadPrevious")
|
||||
self.props.loadPrevious()
|
||||
end
|
||||
if self.props.loadNext and self.state.lead.index > self.state.listSize - self.props.loadingBuffer then
|
||||
debugPrint(" Calling loadNext")
|
||||
self.props.loadNext()
|
||||
end
|
||||
end
|
||||
|
||||
-- Set the current canvas position according to Orientation without calling onScroll.
|
||||
function Scroller:setScroll(pos)
|
||||
self.scrollDebounce = true
|
||||
debugPrint(" Scrolling to", pos)
|
||||
self:getCurrent().CanvasPosition = isVertical[self.props.orientation]
|
||||
and Vector2.new(self:getCurrent().CanvasPosition.X, pos)
|
||||
or Vector2.new(pos, self:getCurrent().CanvasPosition.Y)
|
||||
self.scrollDebounce = false
|
||||
end
|
||||
|
||||
-- Returns the signed distance from the element to the given canvas-relative position, or 0 if the element overlaps
|
||||
-- it. The sign of the distance is relative to the list indices. For this distance calculation, the padding between
|
||||
-- elements is considered part of the current element. Returns nil if the element is not currently rendered.
|
||||
function Scroller:distanceToPosition(index, pos)
|
||||
local child = self:getRbx(index)
|
||||
if not child then
|
||||
return nil
|
||||
end
|
||||
|
||||
local childTop = self:absoluteToCanvasPosition(self:measure(child.AbsolutePosition)) - self.itemPadding
|
||||
local childBottom = childTop + self:measure(child.AbsoluteSize) + 2 * self.itemPadding
|
||||
|
||||
if pos < childTop then
|
||||
return (pos - childTop) * direction[self.props.orientation]
|
||||
elseif pos > childBottom then
|
||||
return (pos - childBottom) * direction[self.props.orientation]
|
||||
else
|
||||
return 0
|
||||
end
|
||||
end
|
||||
|
||||
-- Get the canvas-relative position of the current anchor element.
|
||||
function Scroller:getAnchorCanvasPosition()
|
||||
return self:getAnchorCanvasFromIndex(self.state.anchor.index)
|
||||
end
|
||||
|
||||
function Scroller:getAnchorCanvasFromIndex(index)
|
||||
local scale = self.props.anchorLocation.Scale
|
||||
if not isReverse[self.props.orientation] then
|
||||
scale = 1 - scale
|
||||
end
|
||||
|
||||
return Round.nearest(self:getChildCanvasPosition(index) + scale * self:getChildSize(index))
|
||||
end
|
||||
|
||||
-- Get the frame-relative position of the current anchor element.
|
||||
function Scroller:getAnchorFramePosition()
|
||||
return self:getAnchorFrameFromIndex(self.state.anchor.index)
|
||||
end
|
||||
|
||||
function Scroller:getAnchorFrameFromIndex(index)
|
||||
local scale = self.props.anchorLocation.Scale
|
||||
if not isReverse[self.props.orientation] then
|
||||
scale = 1 - scale
|
||||
end
|
||||
|
||||
return Round.nearest(self:getChildFramePosition(index) + scale * self:getChildSize(index))
|
||||
- self.relativeAnchorLocation
|
||||
end
|
||||
|
||||
-- Convert an AbsolutePosition to a position relative to the top-left corner of the canvas.
|
||||
function Scroller:absoluteToCanvasPosition(position)
|
||||
local current = self:getCurrent()
|
||||
local canvas = current.CanvasPosition
|
||||
local absolute = current.AbsolutePosition
|
||||
return position + self:measure(canvas) - self:measure(absolute)
|
||||
end
|
||||
|
||||
-- Convert an AbsolutePosition to a position relative to the top-left corner of the scrolling frame.
|
||||
function Scroller:absoluteToFramePosition(position)
|
||||
local current = self:getCurrent()
|
||||
local absolute = current.AbsolutePosition
|
||||
return position - self:measure(absolute)
|
||||
end
|
||||
|
||||
-- Get the canvas-relative position of the element at the specified index.
|
||||
function Scroller:getChildCanvasPosition(index)
|
||||
local current = self:getRbx(index)
|
||||
return current and self:absoluteToCanvasPosition(self:measure(current.AbsolutePosition)) or 0
|
||||
end
|
||||
|
||||
-- Get the frame-relative position of the element at the specified index.
|
||||
function Scroller:getChildFramePosition(index)
|
||||
local current = self:getRbx(index)
|
||||
return current and self:absoluteToFramePosition(self:measure(current.AbsolutePosition)) or 0
|
||||
end
|
||||
|
||||
-- Get the absolute size of the element at the specified index.
|
||||
function Scroller:getChildSize(index)
|
||||
local current = self:getRbx(index)
|
||||
return current and self:measure(current.AbsoluteSize) or 0
|
||||
end
|
||||
|
||||
-- Get the ID of an element at a specific index.
|
||||
function Scroller:getID(index)
|
||||
return self.props.identifier(self.props.itemList[index])
|
||||
end
|
||||
|
||||
-- Create or update a metadata entry for the given element. This can't use
|
||||
-- self.props in willUpdate as any props it uses could be out of date.
|
||||
function Scroller:updateMetadata(id, item, props)
|
||||
local meta = self.metadata[id]
|
||||
if not meta then
|
||||
meta = {}
|
||||
self.metadata[id] = meta
|
||||
end
|
||||
|
||||
if not meta.name then
|
||||
local elem = props.renderItem(item, false)
|
||||
local class = tostring(elem.component)
|
||||
local pool = self:getKeyPool(class)
|
||||
meta.name = pool:get()
|
||||
end
|
||||
|
||||
if not self.refpool[meta.name] then
|
||||
self.refpool[meta.name] = Roact.createRef()
|
||||
end
|
||||
meta.ref = self.refpool[meta.name]
|
||||
end
|
||||
|
||||
-- Clear the metadata for an element that is being unloaded.
|
||||
function Scroller:clearMetadata(id)
|
||||
local meta = self.metadata[id]
|
||||
if not meta then
|
||||
return
|
||||
end
|
||||
|
||||
meta.name:release()
|
||||
meta.name = nil
|
||||
meta.ref = nil
|
||||
end
|
||||
|
||||
-- Get the key pool for the given class of elements, or create a new one if that doesn't exist yet.
|
||||
function Scroller:getKeyPool(class)
|
||||
if not self.pools[class] then
|
||||
self.pools[class] = KeyPool.new(class)
|
||||
end
|
||||
return self.pools[class]
|
||||
end
|
||||
|
||||
-- Get the metadata info for the element at the specified index.
|
||||
function Scroller:getMetadata(index)
|
||||
return self.metadata[self:getID(index)]
|
||||
end
|
||||
|
||||
-- Get the current Roblox instance from the ref stored in the metadata.
|
||||
function Scroller:getRbx(index)
|
||||
local meta = self:getMetadata(index)
|
||||
return meta and meta.ref and meta.ref.current
|
||||
end
|
||||
|
||||
-- Return X or Y depending on the orientation.
|
||||
function Scroller:measure(vecOrUDim2)
|
||||
return isVertical[self.props.orientation] and vecOrUDim2.Y or vecOrUDim2.X
|
||||
end
|
||||
|
||||
-- Get the current ScrollingFrame instance.
|
||||
function Scroller:getCurrent()
|
||||
return self:getRef().current
|
||||
end
|
||||
|
||||
function Scroller:getRef()
|
||||
-- Make sure to get the ref from props if that exists.
|
||||
return self.props[Roact.Ref] or self._ref
|
||||
end
|
||||
|
||||
return Scroller
|
||||
+61
@@ -0,0 +1,61 @@
|
||||
-- Find the new indicies of the trailing, anchor and leading elements.
|
||||
-- props is expected to contain itemList, the identifier function and the maximum search distance.
|
||||
-- state is expected to contain the old top, anchor and bottom indices and ids.
|
||||
return function(props, state)
|
||||
local topIndex = state.trail.index
|
||||
local topID = state.trail.id
|
||||
local anchorIndex = state.anchor.index
|
||||
local anchorID = state.anchor.id
|
||||
local bottomIndex = state.lead.index
|
||||
local bottomID = state.lead.id
|
||||
|
||||
local listSize = #props.itemList
|
||||
|
||||
-- If too much got deleted and the previous anchor index is off the bottom of the list, start the search from
|
||||
-- the bottom.
|
||||
if anchorIndex > listSize then
|
||||
anchorIndex = listSize
|
||||
end
|
||||
|
||||
-- No access to self:getID
|
||||
local getID = function(index)
|
||||
return props.identifier(props.itemList[index])
|
||||
end
|
||||
local topStill = getID(topIndex) == topID
|
||||
local anchorStill = getID(anchorIndex) == anchorID
|
||||
local bottomStill = getID(bottomIndex) == bottomID
|
||||
if topStill and anchorStill and bottomStill then
|
||||
-- Nothing important moved
|
||||
return topIndex, anchorIndex, bottomIndex
|
||||
end
|
||||
|
||||
local step = 0
|
||||
local foundTop = topStill and topIndex or nil
|
||||
local foundAnchor = nil
|
||||
local foundBottom = bottomStill and bottomIndex or nil
|
||||
|
||||
-- Scan outward from the old anchor index until we find the top and bottom or hit the max distance
|
||||
local deltas = {top=-1, bottom=1}
|
||||
repeat
|
||||
for _, delta in pairs(deltas) do
|
||||
local pos = anchorIndex + delta * step
|
||||
|
||||
if pos >= 1 and pos <= listSize then
|
||||
local id = getID(pos)
|
||||
if id == topID then
|
||||
foundTop = pos
|
||||
end
|
||||
if id == anchorID then
|
||||
foundAnchor = pos
|
||||
end
|
||||
if id == bottomID then
|
||||
foundBottom = pos
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
step = step + 1
|
||||
until (foundTop and foundAnchor and foundBottom) or step > props.maximumSearchDistance
|
||||
|
||||
return foundTop, foundAnchor, foundBottom
|
||||
end
|
||||
+17
@@ -0,0 +1,17 @@
|
||||
local Root = script:FindFirstAncestor("infinite-scroller").Parent
|
||||
local Roact = require(Root.Roact)
|
||||
|
||||
return function(props)
|
||||
return Roact.createFragment({
|
||||
foo = Roact.createElement("Frame", {
|
||||
BackgroundColor3 = props.color,
|
||||
Size = UDim2.new(0, props.width, 0, props.width),
|
||||
LayoutOrder = props.LayoutOrder,
|
||||
}),
|
||||
bar = Roact.createElement("Frame", {
|
||||
BackgroundColor3 = props.color,
|
||||
Size = UDim2.new(0, props.width, 0, props.width),
|
||||
LayoutOrder = props.LayoutOrder + 1,
|
||||
})
|
||||
})
|
||||
end
|
||||
+10
@@ -0,0 +1,10 @@
|
||||
local Root = script:FindFirstAncestor("infinite-scroller").Parent
|
||||
local Roact = require(Root.Roact)
|
||||
|
||||
return function(props)
|
||||
return Roact.createElement("Frame", {
|
||||
BackgroundColor3 = props.color,
|
||||
Size = UDim2.new(0, props.width, 0, props.width),
|
||||
LayoutOrder = props.LayoutOrder,
|
||||
})
|
||||
end
|
||||
+24
@@ -0,0 +1,24 @@
|
||||
local Root = script:FindFirstAncestor("infinite-scroller").Parent
|
||||
local Roact = require(Root.Roact)
|
||||
|
||||
local Thing = Roact.PureComponent:extend("Thing")
|
||||
|
||||
function Thing:render()
|
||||
return Roact.createElement("Frame", {
|
||||
BackgroundColor3 = self.props.color,
|
||||
Size = UDim2.new(0, self.props.width, 0, self.props.width),
|
||||
LayoutOrder = self.props.LayoutOrder,
|
||||
})
|
||||
end
|
||||
|
||||
local ThingRoot = Roact.PureComponent:extend("ThingRoot")
|
||||
|
||||
function ThingRoot:render()
|
||||
return Roact.createElement(Thing, {
|
||||
color = self.props.color,
|
||||
width = self.props.width,
|
||||
LayoutOrder = self.props.LayoutOrder,
|
||||
})
|
||||
end
|
||||
|
||||
return ThingRoot
|
||||
+37
@@ -0,0 +1,37 @@
|
||||
local Root = script:FindFirstAncestor("infinite-scroller").Parent
|
||||
local Roact = require(Root.Roact)
|
||||
|
||||
local Thing = Roact.PureComponent:extend("Thing")
|
||||
|
||||
function Thing:init()
|
||||
self.state = {
|
||||
clicked = false,
|
||||
token = nil,
|
||||
}
|
||||
end
|
||||
|
||||
function Thing.getDerivedStateFromProps(nextProps, lastState)
|
||||
-- Use the token we're already being passed as a surrogate lock, just for this story.
|
||||
-- Normally, this would be a separate prop.
|
||||
if nextProps.token ~= lastState.token then
|
||||
return {
|
||||
clicked = false,
|
||||
token = nextProps.token,
|
||||
}
|
||||
end
|
||||
end
|
||||
|
||||
function Thing:render()
|
||||
return Roact.createElement("Frame", {
|
||||
BackgroundColor3 = self.props.color,
|
||||
Size = UDim2.new(1, -self.props.width, 0, self.state.clicked and 10 or self.props.width),
|
||||
LayoutOrder = self.props.LayoutOrder,
|
||||
[Roact.Event.InputBegan] = function(rbx, input)
|
||||
if input.UserInputType == Enum.UserInputType.MouseButton1 then
|
||||
self:setState({clicked = not self.state.clicked})
|
||||
end
|
||||
end,
|
||||
})
|
||||
end
|
||||
|
||||
return Thing
|
||||
+23
@@ -0,0 +1,23 @@
|
||||
local Root = script:FindFirstAncestor("infinite-scroller").Parent
|
||||
local Roact = require(Root.Roact)
|
||||
|
||||
local SCREEN_SIZE = Vector2.new(800, 480)
|
||||
|
||||
return {
|
||||
name = "Scroller",
|
||||
storyRoot = script,
|
||||
middleware = function(story, target)
|
||||
local tree = Roact.createElement("Frame", {
|
||||
BackgroundColor3 = Color3.fromRGB(30, 31, 28),
|
||||
Size = UDim2.new(0, SCREEN_SIZE.X, 0, SCREEN_SIZE.Y),
|
||||
ClipsDescendants = true,
|
||||
}, {
|
||||
Story = Roact.createElement(story)
|
||||
})
|
||||
|
||||
local handle = Roact.mount(tree, target, "Root")
|
||||
return function()
|
||||
Roact.unmount(handle)
|
||||
end
|
||||
end,
|
||||
}
|
||||
+75
@@ -0,0 +1,75 @@
|
||||
local InfiniteScroller = script:FindFirstAncestor("infinite-scroller")
|
||||
local Root = InfiniteScroller.Parent
|
||||
local Roact = require(Root.Roact)
|
||||
local Cryo = require(Root.Cryo)
|
||||
local Scroller = require(InfiniteScroller).Scroller
|
||||
|
||||
local Story = Roact.PureComponent:extend("Rhodium Story - item deletion test")
|
||||
|
||||
function Story:init()
|
||||
self.state.items = {}
|
||||
for i = -20,20 do
|
||||
table.insert(self.state.items, i)
|
||||
end
|
||||
end
|
||||
|
||||
function Story:render()
|
||||
return Roact.createElement("Frame", {
|
||||
Size=UDim2.new(1, 0, 1, 0),
|
||||
BackgroundTransparency = 1,
|
||||
}, {
|
||||
scroll = Roact.createElement(Scroller, Cryo.Dictionary.join(
|
||||
{
|
||||
BackgroundColor3 = Color3.fromRGB(255, 0, 0),
|
||||
Size = UDim2.new(0, 100, 0, 100),
|
||||
padding = UDim.new(),
|
||||
itemList = self.state.items,
|
||||
focusIndex = 21,
|
||||
anchorLocation = UDim.new(0.5, 0),
|
||||
loadingBuffer = 2,
|
||||
mountingBuffer = 99,
|
||||
estimatedItemSize = 10,
|
||||
renderItem = function(item, _)
|
||||
return Roact.createElement("Frame", {
|
||||
Size = UDim2.new(0, 10, 0, 10),
|
||||
BackgroundColor3 = item == 0
|
||||
and Color3.fromRGB(255, 255, 255)
|
||||
or Color3.fromRGB(0, 128 - 8*item, 128 + 8*item),
|
||||
}, {
|
||||
["INDEX" .. tostring(item)] = Roact.createElement("Frame"),
|
||||
})
|
||||
end,
|
||||
},
|
||||
self.props,
|
||||
{toDelete = Cryo.None}
|
||||
)),
|
||||
deletion = Roact.createElement("TextButton", {
|
||||
Size = UDim2.new(0, 100, 0, 50),
|
||||
Position = UDim2.new(0.5, 0, 0, 0),
|
||||
AnchorPoint = Vector2.new(1, 0),
|
||||
Text = "Delete",
|
||||
[Roact.Event.Activated] = function()
|
||||
local nextItems
|
||||
if self.props.toDelete then
|
||||
nextItems = Cryo.List.filter(self.state.items, function(item)
|
||||
for _, v in pairs(self.props.toDelete) do
|
||||
if item == v then
|
||||
return false
|
||||
end
|
||||
end
|
||||
return true
|
||||
end)
|
||||
else
|
||||
local n = math.random(1, #self.state.items)
|
||||
print("Deleting index " .. tostring(n))
|
||||
nextItems = Cryo.List.removeIndex(self.state.items, n)
|
||||
end
|
||||
self:setState({
|
||||
items = nextItems
|
||||
})
|
||||
end,
|
||||
}),
|
||||
})
|
||||
end
|
||||
|
||||
return Story
|
||||
+30
@@ -0,0 +1,30 @@
|
||||
local InfiniteScroller = script:FindFirstAncestor("infinite-scroller")
|
||||
local Root = InfiniteScroller.Parent
|
||||
local Roact = require(Root.Roact)
|
||||
local Cryo = require(Root.Cryo)
|
||||
local Scroller = require(InfiniteScroller).Scroller
|
||||
|
||||
local Story = Roact.PureComponent:extend("Rhodium Story - a few large items")
|
||||
|
||||
function Story:render()
|
||||
return Roact.createElement(Scroller, Cryo.Dictionary.join({
|
||||
BackgroundColor3 = Color3.fromRGB(255, 0, 0),
|
||||
Size = UDim2.new(0, 50, 0, 50),
|
||||
padding = UDim.new(),
|
||||
itemList = {1, 2, 3, 4, 5, 6, 7},
|
||||
focusIndex = 4,
|
||||
anchorLocation = UDim.new(0.5, 0),
|
||||
loadingBuffer = 2,
|
||||
mountingBuffer = 49,
|
||||
renderItem = function(item, _)
|
||||
return Roact.createElement("Frame", {
|
||||
Size = UDim2.new(0, 50, 0, 50),
|
||||
BackgroundColor3 = Color3.fromRGB(item*30, item*30, item*30),
|
||||
}, {
|
||||
["INDEX" .. tostring(item)] = Roact.createElement("Frame"),
|
||||
})
|
||||
end,
|
||||
}, self.props))
|
||||
end
|
||||
|
||||
return Story
|
||||
+125
@@ -0,0 +1,125 @@
|
||||
local InfiniteScroller = script:FindFirstAncestor("infinite-scroller")
|
||||
local Root = InfiniteScroller.Parent
|
||||
local Roact = require(Root.Roact)
|
||||
local Cryo = require(Root.Cryo)
|
||||
local Scroller = require(InfiniteScroller).Scroller
|
||||
|
||||
local Story = Roact.PureComponent:extend("Rhodium Story - infinite scroll in both directions")
|
||||
|
||||
function Story:init()
|
||||
self.state.items = {
|
||||
{
|
||||
token = 0,
|
||||
color = Color3.fromRGB(255, 255, 255),
|
||||
}
|
||||
}
|
||||
self.state.size = Vector2.new(50, 50)
|
||||
|
||||
self.loadPrevious = function()
|
||||
local newItems = {}
|
||||
local n = self.state.items[1].token
|
||||
for i = n-10, n-1 do
|
||||
table.insert(newItems, {
|
||||
token = i,
|
||||
color = Color3.fromRGB(0, 128 - i, 128 + i),
|
||||
})
|
||||
end
|
||||
self:setState({
|
||||
items = Cryo.List.join(newItems, self.state.items)
|
||||
})
|
||||
end
|
||||
|
||||
self.loadNext = function()
|
||||
local newItems = {}
|
||||
local n = self.state.items[#self.state.items].token
|
||||
for i = n+1, n+10 do
|
||||
table.insert(newItems, {
|
||||
token = i,
|
||||
color = Color3.fromRGB(0, 128 - i, 128 + i),
|
||||
})
|
||||
end
|
||||
self:setState({
|
||||
items = Cryo.List.join(self.state.items, newItems)
|
||||
})
|
||||
end
|
||||
end
|
||||
|
||||
function Story:render()
|
||||
return Roact.createElement("Frame", {
|
||||
Size = UDim2.new(1, 0, 1, 0),
|
||||
BackgroundTransparency = 1,
|
||||
}, {
|
||||
scroll = Roact.createElement(Scroller, Cryo.Dictionary.join({
|
||||
BackgroundColor3 = Color3.fromRGB(255, 0, 0),
|
||||
Size = UDim2.new(0, self.state.size.X, 0, self.state.size.Y),
|
||||
padding = UDim.new(0, 3),
|
||||
itemList = self.state.items,
|
||||
loadNext = self.loadNext,
|
||||
loadPrevious = self.loadPrevious,
|
||||
focusIndex = 1,
|
||||
anchorLocation = UDim.new(0.5, 0),
|
||||
orientation = Scroller.Orientation.Up,
|
||||
estimatedItemSize = 10,
|
||||
mountingBuffer = 50,
|
||||
identifier = function(item)
|
||||
return item.token
|
||||
end,
|
||||
renderItem = function(item, _)
|
||||
assert(item.token, "Item's token is unset")
|
||||
assert(item.color, "Item's color is unset")
|
||||
return Roact.createElement("Frame", {
|
||||
Size = UDim2.new(0, 10, 0, 10),
|
||||
BackgroundColor3 = item.color,
|
||||
}, {
|
||||
["INDEX" .. tostring(item.token)] = Roact.createElement("Frame"),
|
||||
})
|
||||
end,
|
||||
}, self.props)),
|
||||
moveUp = Roact.createElement("TextButton", {
|
||||
Size = UDim2.new(0, 50, 0, 50),
|
||||
Position = UDim2.new(0.5, -50, 0, 0),
|
||||
AnchorPoint = Vector2.new(1, 0),
|
||||
Text = "^",
|
||||
[Roact.Event.Activated] = function()
|
||||
self:setState({
|
||||
size = self.state.size + Vector2.new(0, -20)
|
||||
})
|
||||
end,
|
||||
}),
|
||||
moveDown = Roact.createElement("TextButton", {
|
||||
Size = UDim2.new(0, 50, 0, 50),
|
||||
Position = UDim2.new(0.5, -50, 0, 100),
|
||||
AnchorPoint = Vector2.new(1, 0),
|
||||
Text = "v",
|
||||
[Roact.Event.Activated] = function()
|
||||
self:setState({
|
||||
size = self.state.size + Vector2.new(0, 20)
|
||||
})
|
||||
end,
|
||||
}),
|
||||
moveLeft = Roact.createElement("TextButton", {
|
||||
Size = UDim2.new(0, 50, 0, 50),
|
||||
Position = UDim2.new(0.5, -100, 0, 50),
|
||||
AnchorPoint = Vector2.new(1, 0),
|
||||
Text = "<",
|
||||
[Roact.Event.Activated] = function()
|
||||
self:setState({
|
||||
size = self.state.size + Vector2.new(-20, 0)
|
||||
})
|
||||
end,
|
||||
}),
|
||||
moveRight = Roact.createElement("TextButton", {
|
||||
Size = UDim2.new(0, 50, 0, 50),
|
||||
Position = UDim2.new(0.5, 0, 0, 50),
|
||||
AnchorPoint = Vector2.new(1, 0),
|
||||
Text = ">",
|
||||
[Roact.Event.Activated] = function()
|
||||
self:setState({
|
||||
size = self.state.size + Vector2.new(20, 0)
|
||||
})
|
||||
end,
|
||||
}),
|
||||
})
|
||||
end
|
||||
|
||||
return Story
|
||||
+113
@@ -0,0 +1,113 @@
|
||||
local InfiniteScroller = script:FindFirstAncestor("infinite-scroller")
|
||||
local Root = InfiniteScroller.Parent
|
||||
local Roact = require(Root.Roact)
|
||||
local Cryo = require(Root.Cryo)
|
||||
local Scroller = require(InfiniteScroller).Scroller
|
||||
|
||||
local Bar = Roact.PureComponent:extend("Bar")
|
||||
|
||||
function Bar:init()
|
||||
self.state = {
|
||||
clicked = false,
|
||||
}
|
||||
end
|
||||
|
||||
function Bar:render()
|
||||
return Roact.createElement("TextButton", Cryo.Dictionary.join(
|
||||
{
|
||||
Size = UDim2.new(1, 0, 0, self.state.clicked and 30 or 10),
|
||||
[Roact.Event.Activated] = function()
|
||||
self:setState({
|
||||
clicked = not self.state.clicked,
|
||||
})
|
||||
end,
|
||||
},
|
||||
self.props
|
||||
))
|
||||
end
|
||||
|
||||
local Story = Roact.PureComponent:extend("Rhodium Story - frame resize test")
|
||||
|
||||
function Story:init()
|
||||
self.state = {
|
||||
size = Vector2.new(50, 50),
|
||||
}
|
||||
end
|
||||
|
||||
function Story:render()
|
||||
return Roact.createElement("Frame", {
|
||||
Size=UDim2.new(1, 0, 1, 0),
|
||||
BackgroundTransparency = 1,
|
||||
}, {
|
||||
scroll = Roact.createElement(Scroller, Cryo.Dictionary.join(
|
||||
{
|
||||
BackgroundColor3 = Color3.fromRGB(255, 0, 0),
|
||||
Size = UDim2.new(0, self.state.size.X, 0, self.state.size.Y),
|
||||
padding = UDim.new(),
|
||||
itemList = {1, 2, 3},
|
||||
focusIndex = 2,
|
||||
anchorLocation = UDim.new(0, 0),
|
||||
orientation = Scroller.Orientation.Down,
|
||||
loadingBuffer = 2,
|
||||
mountingBuffer = 99,
|
||||
estimatedItemSize = 10,
|
||||
renderItem = function(item, _)
|
||||
return Roact.createElement(Bar, {
|
||||
BackgroundColor3 = item == 2
|
||||
and Color3.fromRGB(255, 255, 255)
|
||||
or Color3.fromRGB(0, -128 + 128*item, 376 - 128*item),
|
||||
}, {
|
||||
["INDEX" .. tostring(item)] = Roact.createElement("Frame"),
|
||||
})
|
||||
end,
|
||||
},
|
||||
self.props
|
||||
)),
|
||||
moveUp = Roact.createElement("TextButton", {
|
||||
Size = UDim2.new(0, 50, 0, 50),
|
||||
Position = UDim2.new(0.5, -50, 0, 0),
|
||||
AnchorPoint = Vector2.new(1, 0),
|
||||
Text = "^",
|
||||
[Roact.Event.Activated] = function()
|
||||
self:setState({
|
||||
size = self.state.size + Vector2.new(0, -20)
|
||||
})
|
||||
end,
|
||||
}),
|
||||
moveDown = Roact.createElement("TextButton", {
|
||||
Size = UDim2.new(0, 50, 0, 50),
|
||||
Position = UDim2.new(0.5, -50, 0, 100),
|
||||
AnchorPoint = Vector2.new(1, 0),
|
||||
Text = "v",
|
||||
[Roact.Event.Activated] = function()
|
||||
self:setState({
|
||||
size = self.state.size + Vector2.new(0, 20)
|
||||
})
|
||||
end,
|
||||
}),
|
||||
moveLeft = Roact.createElement("TextButton", {
|
||||
Size = UDim2.new(0, 50, 0, 50),
|
||||
Position = UDim2.new(0.5, -100, 0, 50),
|
||||
AnchorPoint = Vector2.new(1, 0),
|
||||
Text = "<",
|
||||
[Roact.Event.Activated] = function()
|
||||
self:setState({
|
||||
size = self.state.size + Vector2.new(-20, 0)
|
||||
})
|
||||
end,
|
||||
}),
|
||||
moveRight = Roact.createElement("TextButton", {
|
||||
Size = UDim2.new(0, 50, 0, 50),
|
||||
Position = UDim2.new(0.5, 0, 0, 50),
|
||||
AnchorPoint = Vector2.new(1, 0),
|
||||
Text = ">",
|
||||
[Roact.Event.Activated] = function()
|
||||
self:setState({
|
||||
size = self.state.size + Vector2.new(20, 0)
|
||||
})
|
||||
end,
|
||||
}),
|
||||
})
|
||||
end
|
||||
|
||||
return Story
|
||||
+33
@@ -0,0 +1,33 @@
|
||||
local InfiniteScroller = script:FindFirstAncestor("infinite-scroller")
|
||||
local Root = InfiniteScroller.Parent
|
||||
local Roact = require(Root.Roact)
|
||||
local Cryo = require(Root.Cryo)
|
||||
local Scroller = require(InfiniteScroller).Scroller
|
||||
|
||||
local Story = Roact.PureComponent:extend("Rhodium Story - enough small items to fill the view")
|
||||
|
||||
function Story:render()
|
||||
return Roact.createElement(Scroller, Cryo.Dictionary.join({
|
||||
BackgroundColor3 = Color3.fromRGB(255, 0, 0),
|
||||
Size = UDim2.new(0, 50, 0, 50),
|
||||
padding = UDim.new(),
|
||||
itemList = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11},
|
||||
focusIndex = 6,
|
||||
anchorLocation = UDim.new(0.5, 0),
|
||||
loadingBuffer = 2,
|
||||
mountingBuffer = 99,
|
||||
estimatedItemSize = 10,
|
||||
renderItem = function(item, _)
|
||||
return Roact.createElement("Frame", {
|
||||
Size = UDim2.new(0, 10, 0, 10),
|
||||
BackgroundColor3 = item == 6
|
||||
and Color3.fromRGB(255, 255, 255)
|
||||
or Color3.fromRGB(0, item*23, item*23),
|
||||
}, {
|
||||
["INDEX" .. tostring(item)] = Roact.createElement("Frame"),
|
||||
})
|
||||
end,
|
||||
}, self.props))
|
||||
end
|
||||
|
||||
return Story
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
local InfiniteScroller = script:FindFirstAncestor("infinite-scroller")
|
||||
local Root = InfiniteScroller.Parent
|
||||
local Roact = require(Root.Roact)
|
||||
local Cryo = require(Root.Cryo)
|
||||
local Scroller = require(InfiniteScroller).Scroller
|
||||
|
||||
local Story = Roact.PureComponent:extend("Rhodium Story - a single small item")
|
||||
|
||||
function Story:render()
|
||||
return Roact.createElement(Scroller, Cryo.Dictionary.join({
|
||||
BackgroundColor3 = Color3.fromRGB(255, 0, 0),
|
||||
Size = UDim2.new(0, 50, 0, 50),
|
||||
itemList = {1},
|
||||
anchorLocation = UDim.new(0.5, 0),
|
||||
dragBuffer = 0,
|
||||
renderItem = function()
|
||||
return Roact.createElement("Frame", {Size=UDim2.new(0, 10, 0, 10)})
|
||||
end,
|
||||
}, self.props))
|
||||
end
|
||||
|
||||
return Story
|
||||
+139
@@ -0,0 +1,139 @@
|
||||
local InfiniteScroller = script:FindFirstAncestor("infinite-scroller")
|
||||
local Root = InfiniteScroller.Parent
|
||||
local Roact = require(Root.Roact)
|
||||
local Cryo = require(Root.Cryo)
|
||||
local Scroller = require(InfiniteScroller).Scroller
|
||||
|
||||
local Thing = require(script.Parent.ResizingThing)
|
||||
|
||||
local Story = Roact.PureComponent:extend("Story")
|
||||
|
||||
function Story:render()
|
||||
return Roact.createFragment({
|
||||
scroller = Roact.createElement(Scroller, {
|
||||
BackgroundColor3 = Color3.fromRGB(56, 19, 18),
|
||||
Size = UDim2.new(0, self.state.size.X, 1, self.state.size.Y),
|
||||
Position = UDim2.new(0, 50, 0, 50),
|
||||
ScrollBarThickness = 8,
|
||||
padding = UDim.new(0, 5),
|
||||
orientation = Scroller.Orientation.Down,
|
||||
itemList = self.state.items,
|
||||
loadNext = self.loadNext,
|
||||
loadPrevious = self.loadPrevious,
|
||||
focusLock = self.state.lock,
|
||||
focusIndex = 101,
|
||||
anchorLocation = UDim.new(0, 0),
|
||||
estimatedItemSize = 40,
|
||||
identifier = function(item) return item.token end,
|
||||
renderItem = function(item, _)
|
||||
return Roact.createElement(Thing, item)
|
||||
end,
|
||||
}),
|
||||
refresh = Roact.createElement("TextButton", {
|
||||
Size = UDim2.new(0, 110, 0, 30),
|
||||
Position = UDim2.new(1, -50, 0, 50),
|
||||
AnchorPoint = Vector2.new(1, 0),
|
||||
Text = "Refresh",
|
||||
BackgroundColor3 = Color3.fromRGB(255, 255, 255),
|
||||
[Roact.Event.Activated] = self.clickRefresh,
|
||||
}),
|
||||
up = Roact.createElement("TextButton", {
|
||||
Size = UDim2.new(0, 30, 0, 30),
|
||||
Position = UDim2.new(1, -90, 0, 90),
|
||||
AnchorPoint = Vector2.new(1, 0),
|
||||
Text = "^",
|
||||
BackgroundColor3 = Color3.fromRGB(255, 255, 255),
|
||||
[Roact.Event.Activated] = self.clickUp,
|
||||
}),
|
||||
down = Roact.createElement("TextButton", {
|
||||
Size = UDim2.new(0, 30, 0, 30),
|
||||
Position = UDim2.new(1, -90, 0, 170),
|
||||
AnchorPoint = Vector2.new(1, 0),
|
||||
Text = "v",
|
||||
BackgroundColor3 = Color3.fromRGB(255, 255, 255),
|
||||
[Roact.Event.Activated] = self.clickDown,
|
||||
}),
|
||||
left = Roact.createElement("TextButton", {
|
||||
Size = UDim2.new(0, 30, 0, 30),
|
||||
Position = UDim2.new(1, -130, 0, 130),
|
||||
AnchorPoint = Vector2.new(1, 0),
|
||||
Text = "<",
|
||||
BackgroundColor3 = Color3.fromRGB(255, 255, 255),
|
||||
[Roact.Event.Activated] = self.clickLeft,
|
||||
}),
|
||||
right = Roact.createElement("TextButton", {
|
||||
Size = UDim2.new(0, 30, 0, 30),
|
||||
Position = UDim2.new(1, -50, 0, 130),
|
||||
AnchorPoint = Vector2.new(1, 0),
|
||||
Text = ">",
|
||||
BackgroundColor3 = Color3.fromRGB(255, 255, 255),
|
||||
[Roact.Event.Activated] = self.clickRight,
|
||||
}),
|
||||
})
|
||||
end
|
||||
|
||||
local function generate(token)
|
||||
if token == 0 then
|
||||
return {color=Color3.fromRGB(255, 255, 255), width=50, token=0}
|
||||
end
|
||||
return {color=Color3.fromRGB(128-token, 255, 128+token), width=50+40*math.sin(token/5), token=token}
|
||||
end
|
||||
|
||||
function Story:init()
|
||||
local items = {}
|
||||
for i = -100,100 do table.insert(items, generate(i)) end
|
||||
self.state = {
|
||||
lock = 1,
|
||||
size = Vector2.new(200, -100),
|
||||
items = items,
|
||||
}
|
||||
|
||||
self.loadNext = function()
|
||||
self:setState({
|
||||
items = Cryo.List.join(self.state.items, {generate(self.state.items[#self.state.items].token + 1)}),
|
||||
})
|
||||
end
|
||||
|
||||
self.loadPrevious = function()
|
||||
self:setState({
|
||||
items = Cryo.List.join({generate(self.state.items[1].token - 1)}, self.state.items),
|
||||
})
|
||||
end
|
||||
|
||||
self.clickRefresh = function()
|
||||
print("Recentering")
|
||||
self:setState({
|
||||
lock = self.state.lock + 1
|
||||
})
|
||||
end
|
||||
|
||||
self.clickUp = function()
|
||||
print("Moving up")
|
||||
self:setState({
|
||||
size = self.state.size + Vector2.new(0, -20),
|
||||
})
|
||||
end
|
||||
|
||||
self.clickDown = function()
|
||||
print("Moving down")
|
||||
self:setState({
|
||||
size = self.state.size + Vector2.new(0, 20),
|
||||
})
|
||||
end
|
||||
|
||||
self.clickLeft = function()
|
||||
print("Moving left")
|
||||
self:setState({
|
||||
size = self.state.size + Vector2.new(-20, 0),
|
||||
})
|
||||
end
|
||||
|
||||
self.clickRight = function()
|
||||
print("Moving right")
|
||||
self:setState({
|
||||
size = self.state.size + Vector2.new(20, 0),
|
||||
})
|
||||
end
|
||||
end
|
||||
|
||||
return Story
|
||||
+5
@@ -0,0 +1,5 @@
|
||||
local Scroller = require(script.Components.Scroller)
|
||||
|
||||
return {
|
||||
Scroller = Scroller,
|
||||
}
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
# Generated by Rotriever. Format subject to change in future releases.
|
||||
name = "roblox/infinite-scroller"
|
||||
version = "0.3.4"
|
||||
commit = "43d4eedf0d7a0a97d2dd55e7f066ac617e7fde9f"
|
||||
source = "url+https://github.com/roblox/infinite-scroller"
|
||||
dependencies = [
|
||||
"Cryo roblox/cryo 1.0.0 url+https://github.com/roblox/cryo",
|
||||
"FitFrame roblox/roact-fit-components 1.2.5 url+https://github.com/roblox/roact-fit-components",
|
||||
"Otter roblox/otter 0.1.2 url+https://github.com/roblox/otter",
|
||||
"Roact roblox/roact 1.3.0 url+https://github.com/roblox/roact",
|
||||
"t roblox/t 1.2.5 url+https://github.com/roblox/t",
|
||||
]
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
--[[
|
||||
Package link auto-generated by Rotriever
|
||||
]]
|
||||
local PackageIndex = script.Parent.Parent
|
||||
|
||||
local package = PackageIndex["roblox_t"]["t"]
|
||||
|
||||
if package.ClassName == "ModuleScript" then
|
||||
return require(package)
|
||||
end
|
||||
|
||||
return package
|
||||
Reference in New Issue
Block a user