This commit is contained in:
lx
2026-07-06 14:01:11 -04:00
commit c4f97d729d
16468 changed files with 935321 additions and 0 deletions
@@ -0,0 +1,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
@@ -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
@@ -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
@@ -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
@@ -0,0 +1,16 @@
return {
-- Returns the signed distance from a point to a range. Returns 0 if the
-- point is within the range, positive if it's below and negative if it's
-- above.
fromPointToRangeSigned = function(point, rangeTop, rangeSize)
local rangeBottom = rangeTop + rangeSize
if point < rangeTop then
return point - rangeTop
elseif point > rangeBottom then
return point - rangeBottom
else
return 0
end
end
}
@@ -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 .. "_" .. string.format("%02d", 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
@@ -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
@@ -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,
}
@@ -0,0 +1,67 @@
-- 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 topIndex > listSize then
topIndex = listSize
end
if anchorIndex > listSize then
anchorIndex = listSize
end
if bottomIndex > listSize then
bottomIndex = 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
@@ -0,0 +1,57 @@
local Round = require(script.Parent.Round)
-- Returns the three new indices with any nils filled in and any misorderings corrected.
-- new and old should be tables containing an anchorIndex, a leadIndex and a trailIndex.
return function(new, old, listSize)
local newAnchor = new.anchorIndex
local newLead = new.leadIndex
local newTrail = new.trailIndex
-- 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 newAnchor then
if newLead and newTrail then
-- Estimate that the new anchor is proportionally the same distance from the lead and trail indices.
if newLead == newTrail then
-- Guard against divide by zero.
newAnchor = newLead
else
local oldRatio = (old.anchorIndex - old.leadIndex) / (old.trailIndex - old.leadIndex)
newAnchor = Round.nearest((newTrail - newLead) * oldRatio + newLead)
newAnchor = math.min(math.max(newAnchor, 1), listSize)
end
elseif newLead then
-- Given only the new leading index, estimate that the new anchor is the same distance away as it was.
newAnchor = newLead + old.anchorIndex - old.leadIndex
newAnchor = math.min(math.max(newAnchor, 1), listSize)
elseif newTrail then
-- Given only the new trailing index, estimate that the new anchor is the same distance away as it was.
newAnchor = newTrail + old.anchorIndex - old.trailIndex
newAnchor = math.min(math.max(newAnchor, 1), listSize)
else
-- Everything is gone. Just reuse the same index if that's still within the bounds of the list.
newAnchor = math.min(math.max(old.anchorIndex, 1), listSize)
end
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 newTrail then
newTrail = newAnchor + old.trailIndex - old.anchorIndex
newTrail = math.min(math.max(newTrail, 1), listSize)
end
if not newLead then
newLead = newAnchor + old.leadIndex - old.anchorIndex
newLead = math.min(math.max(newLead, 1), listSize)
end
-- Make sure the resulting indices are in the right order.
local minIndex = math.min(newAnchor, newLead, newTrail)
local maxIndex = math.max(newAnchor, newLead, newTrail)
return {
trailIndex = minIndex,
anchorIndex = newAnchor,
leadIndex = maxIndex,
}
end
@@ -0,0 +1,56 @@
local Root = script:FindFirstAncestor("infinite-scroller").Parent
local Roact = require(Root.Roact)
ComplexThing = Roact.PureComponent:extend("ComplexThing")
ComplexThing.defaultProps = {
-- things start to lag at 5?
nestedLayer = 3,
Size = UDim2.fromScale(0.5, 0.5),
}
function ComplexThing:render()
--temporary hard limit
if self.props.nestedLayer > 6 then
return Roact.createElement("Frame", {
Size = UDim2.fromOffset(100, 100)
})
end
local children = {}
if self.props.nestedLayer > 1 then
children = {
["TL"..self.props.nestedLayer] = Roact.createElement(ComplexThing, {
Position = UDim2.fromScale(0, 0),
nestedLayer = self.props.nestedLayer - 1,
}),
["TR"..self.props.nestedLayer] = Roact.createElement(ComplexThing, {
Position = UDim2.fromScale(0.5, 0),
nestedLayer = self.props.nestedLayer - 1,
}),
["BL"..self.props.nestedLayer] = Roact.createElement(ComplexThing, {
Position = UDim2.fromScale(0, 0.5),
nestedLayer = self.props.nestedLayer - 1,
}),
["BR"..self.props.nestedLayer] = Roact.createElement(ComplexThing, {
Position = UDim2.fromScale(0.5, 0.5),
nestedLayer = self.props.nestedLayer - 1,
}),
}
end
local r = math.random(255)
local g = math.random(255)
local b = math.random(255)
return Roact.createElement("Frame", {
Size = self.props.Size,
Position = self.props.Position,
BackgroundColor3 = Color3.fromRGB(r, g, b),
BorderSizePixel = self.props.Size.X.Scale == 0.5 and 0 or 4,
BorderColor3 = Color3.fromRGB(255, 255, 255),
}, children)
end
return ComplexThing
@@ -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
@@ -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
@@ -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
@@ -0,0 +1,38 @@
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
return nil
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
@@ -0,0 +1,154 @@
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("Story")
function Story:init()
local startingItems = {}
for i = 1, self.props.startingItems or 50 do
table.insert(startingItems, i)
end
self:setState({
items = startingItems,
lock = 1,
width = 260,
itemSize = 30,
})
self.focus = self.props.startingFocus or 1
end
function Story:render()
return Roact.createFragment({
output = Roact.createElement("Frame", {
Size = UDim2.new(0, self.state.width, 0, self.state.itemSize + 30),
BackgroundTransparency = 1,
},{
scroller = Roact.createElement(Scroller, Cryo.Dictionary.join({
BackgroundColor3 = Color3.fromRGB(255, 0, 0),
Size = UDim2.new(1, 0, 1, 0),
orientation = Scroller.Orientation.Right,
padding = UDim.new(0, 5),
itemList = self.state.items,
focusIndex = self.focus,
focusLock = self.state.lock,
anchorLocation = UDim.new(1, -15),
loadingBuffer = 2,
mountingBuffer = 99,
estimatedItemSize = 10,
dragBuffer = 120,
animateScrolling = true,
identifier = function(item)
assert(item)
return item
end,
renderItem = function(item, _)
return Roact.createElement("TextLabel", {
Size = UDim2.new(0, self.state.itemSize, 0, self.state.itemSize),
Text = tostring(item),
BackgroundColor3 = Color3.fromRGB(255, item*5, (51 - item)*5),
}, {
["INDEX" .. tostring(item)] = Roact.createElement("Frame"),
})
end,
onScrollUpdate = function(data)
self.focus = data.anchorIndex
self.animationActive = data.animationActive
end,
}, self.props, {
startingItems = Cryo.None,
startingFocus = Cryo.None,
})),
}),
scrollLeft = Roact.createElement("TextButton", {
Size = UDim2.new(0, 30, 0, 30),
Position = UDim2.new(0, 0, 0, 100),
Text = "<",
[Roact.Event.Activated] = function()
if self.animationActive then
return
end
self.focus = math.max(self.focus - 4, 1)
self:setState({lock = self.state.lock + 1})
end,
}),
scrollRight = Roact.createElement("TextButton", {
Size = UDim2.new(0, 30, 0, 30),
Position = UDim2.new(0, 270, 0, 100),
Text = ">",
[Roact.Event.Activated] = function()
if self.animationActive then
return
end
self.focus = math.min(self.focus + 4, #self.state.items)
self:setState({lock = self.state.lock + 1})
end,
}),
decreaseWidth = Roact.createElement("TextButton", {
Size = UDim2.new(0, 60, 0, 30),
Position = UDim2.new(0, 90, 0, 100),
Text = "Width -",
[Roact.Event.Activated] = function()
if self.animationActive then
return
end
self.focus = math.max(self.focus - 4, 1)
self:setState({
width = self.state.width - 10,
})
end,
}),
increaseWidth = Roact.createElement("TextButton", {
Size = UDim2.new(0, 60, 0, 30),
Position = UDim2.new(0, 150, 0, 100),
Text = "Width +",
[Roact.Event.Activated] = function()
if self.animationActive then
return
end
self:setState({
width = self.state.width + 10,
})
end,
}),
removeItem = Roact.createElement("TextButton", {
Size = UDim2.new(0, 60, 0, 30),
Position = UDim2.new(0, 30, 0, 100),
Text = "Item -",
[Roact.Event.Activated] = function()
if self.animationActive then
return
end
local items = self.state.items
local lock = nil
if self.focus >= #items then
self.focus = #items - 1
lock = self.state.lock + 1
end
self:setState({
items = Cryo.List.removeIndex(items, #items),
lock = lock,
})
end,
}),
addItem = Roact.createElement("TextButton", {
Size = UDim2.new(0, 60, 0, 30),
Position = UDim2.new(0, 210, 0, 100),
Text = "Item +",
[Roact.Event.Activated] = function()
if self.animationActive then
return
end
local items = self.state.items
self:setState({
items = Cryo.List.join(items, {#items + 1}),
})
end,
}),
})
end
return Story
@@ -0,0 +1,87 @@
local HttpService = game:GetService("HttpService")
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 Box = function(props)
return Roact.createElement("TextLabel", {
Size = UDim2.new(1, 0, 0, 40),
Text = props.text,
})
end
local Story = Roact.PureComponent:extend("Story")
function Story:init()
self.state = {
items = {},
lock = 0,
index = 1,
size = Vector2.new(400, -100),
}
end
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,
focusLock = self.state.lock,
focusIndex = self.state.index,
anchorLocation = UDim.new(0, 0),
estimatedItemSize = 40,
dragBuffer = 0,
extraProps = self.state.items,
identifier = function(item)
return item.guid
end,
renderItem = function(item, _)
return Roact.createElement(Box, item)
end,
onScrollUpdate = function(data)
self.indexData = data
end
}),
textbox = Roact.createElement("TextBox", {
Size = UDim2.new(0, 100, 0, 50),
Position = UDim2.new(1, -100, 0, 0),
[Roact.Event.FocusLost] = function(rbx, entered)
if entered and rbx.Text ~= "" then
local newItem = {
text = rbx.Text,
guid = HttpService:GenerateGUID(false),
}
local newState
if self.state.items then
newState = { items = Cryo.List.join(self.state.items, { newItem })}
if self.indexData and self.indexData.anchorIndex == #self.state.items then
newState.lock = self.state.lock + 1
newState.index = #newState.items
end
else
newState = {
items = { newItem },
index = 1,
lock = 1,
}
end
rbx.Text = ""
self:setState(newState)
end
end
}),
})
end
return Story
@@ -0,0 +1,41 @@
local InfiniteScroller = script:FindFirstAncestor("infinite-scroller")
local Root = InfiniteScroller.Parent
local Roact = require(Root.Roact)
local ComplexThing = require(script.Parent.ComplexThing)
local Story = Roact.PureComponent:extend("Story")
Story.defaultProps = {
numThings = 5,
}
function Story:init()
self.ref = Roact.createRef()
self.manyComplexThings = {
layout = Roact.createElement("UIListLayout", {
})
}
local function makeComplexThing()
return Roact.createElement(ComplexThing, {
Size = UDim2.fromOffset(256, 256),
nestedLayer = 5,
})
end
for _ = 1, self.props.numThings do
table.insert(self.manyComplexThings, makeComplexThing())
end
end
function Story:render()
return Roact.createElement("ScrollingFrame", {
Position = UDim2.new(0, 0, 0, 300),
ClipsDescendants = false,
Size = UDim2.fromOffset(256, 256),
CanvasSize = UDim2.new(1, 0, 0, self.props.numThings * 256),
ScrollingDirection = Enum.ScrollingDirection.Y,
}, self.manyComplexThings)
end
return Story
@@ -0,0 +1,80 @@
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 shuffle test")
local NUM_ITEMS = 10
function Story:init()
self.state.items = {}
for i = 1,NUM_ITEMS do
table.insert(self.state.items, {id = i})
end
self.state.focusLock = 1
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 = 1,
focusLock = self.state.focusLock,
anchorLocation = UDim.new(1, 0),
loadingBuffer = 2,
mountingBuffer = 99,
estimatedItemSize = 10,
identifier = function(item)
return item.id
end,
renderItem = function(item, _)
return Roact.createElement("Frame", {
Size = UDim2.new(0, 20, 0, 20),
BackgroundColor3 = Color3.fromRGB(0, 128 - 8*item.id, 128 + 8*item.id),
}, {
["INDEX" .. tostring(item.id)] = Roact.createElement("TextLabel", {
Size = UDim2.new(1, 0, 1, 0),
Text = item.id,
TextColor3 = Color3.new(1, 1, 1),
BackgroundTransparency = 1,
}),
})
end,
},
self.props,
{toDelete = Cryo.None}
)),
shuffle = Roact.createElement("TextButton", {
Size = UDim2.new(0, 100, 0, 50),
Position = UDim2.new(0.5, 0, 0, 0),
AnchorPoint = Vector2.new(1, 0),
Text = "Shuffle",
[Roact.Event.Activated] = function()
local nextItems = self.state.items
for _, v in pairs(nextItems) do
v.id = v.id+1
if v.id > NUM_ITEMS then
v.id = 1
end
end
self:setState({
items = nextItems,
focusLock = self.state.focusLock + 1,
})
end,
}),
})
end
return Story
@@ -0,0 +1,27 @@
local Root = script:FindFirstAncestor("infinite-scroller").Parent
local Roact = require(Root.Roact)
Roact.setGlobalConfig({
propValidation = true,
})
local SCREEN_SIZE = Vector2.new(800, 800)
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,
}
@@ -0,0 +1,88 @@
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 addition test")
function Story:init()
local items = {}
local numberOfItems = self.props.numberOfItems or 30
for i = 1, numberOfItems do
table.insert(items, {id = i})
end
self.state = {
itemList = items,
}
end
function Story:render()
local focusIndex = self.props.addToFront and 1 or #self.state.itemList
return Roact.createElement("Frame", {
Size = UDim2.new(1, 0, 1, 0),
BackgroundTransparency = 1,
}, {
scroll = Roact.createElement(Scroller, Cryo.Dictionary.join(
{
Size = UDim2.new(0, 100, 0, 200),
focusIndex = focusIndex,
focusLock = self.state.itemList[focusIndex].id,
anchorLocation = UDim.new(1, 0),
dragBuffer = 200,
itemList = self.state.itemList,
orientation = Scroller.Orientation.Up,
identifier = function(item)
return item.id
end,
renderItem = function(item, _)
local r = 88+88*math.sin(math.rad(8*item.id+90))
local g = 88+44*math.sin(math.rad(8*item.id+0))
local b = 88+88*math.sin(math.rad(8*item.id+180))
return Roact.createElement("Frame", {
Size = UDim2.new(0, 20, 0, 20),
BackgroundColor3 = Color3.fromRGB(r, g, b),
}, {
["INDEX" .. tostring(item.id)] = Roact.createElement("TextLabel", {
Size = UDim2.new(1, 0, 1, 0),
Text = item.id,
TextColor3 = Color3.new(1, 1, 1),
BackgroundTransparency = 1,
}),
})
end,
},
self.props,
{
addToFront = Cryo.None,
numberOfItems = Cryo.None,
}
)),
addition = Roact.createElement("TextButton", {
Size = UDim2.new(0, 100, 0, 50),
Position = UDim2.new(0.5, 0, 0, 0),
AnchorPoint = Vector2.new(1, 0),
Text = "Add Item",
[Roact.Event.Activated] = function()
local nextId = #self.state.itemList + 1
local nextItems
if self.props.addToFront then
nextItems = Cryo.List.join({{id = nextId}}, self.state.itemList)
else
nextItems = Cryo.List.join(self.state.itemList, {{id = nextId}})
end
self:setState({
itemList = nextItems,
})
end,
}),
})
end
return Story
@@ -0,0 +1,78 @@
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, {id = 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,
identifier = function(item)
return item.id
end,
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.id, 128 + 8*item.id),
}, {
["INDEX" .. tostring(item.id)] = 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.id == 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
@@ -0,0 +1,78 @@
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 - extra props test")
local smallScroll = Roact.PureComponent:extend("smallScroll")
function smallScroll:render()
return Roact.createElement(Scroller, Cryo.Dictionary.join({
BackgroundColor3 = Color3.fromRGB(255, 0, 0),
Size = UDim2.new(0, 50, 0, 50),
padding = UDim.new(),
focusIndex = 6,
anchorLocation = UDim.new(0.5, 0),
loadingBuffer = 2,
mountingBuffer = 99,
estimatedItemSize = 10,
}, self.props))
end
function Story:init()
self.state = {
items = {},
store = {},
}
for i = 1, 11 do
self.state.items[i] = i
self.state.store[i] = (i == 6
and Color3.fromRGB(255, 255, 255)
or Color3.fromRGB(0, i*23, i*23))
end
self.renderItem = function(item)
return Roact.createElement("Frame", {
Size = UDim2.new(0, 10, 0, 10),
BackgroundColor3 = self.state.store[item],
}, {
["INDEX" .. tostring(item)] = Roact.createElement("Frame"),
})
end
end
function Story:render()
return Roact.createElement("Frame", {}, {
scroller = Roact.createElement(smallScroll, Cryo.Dictionary.join({
itemList = self.state.items,
renderItem = self.renderItem,
extraProps = {self.state.store},
}, self.props, {newColor = Cryo.None})),
change = Roact.createElement("TextButton", {
Size = UDim2.new(0, 30, 0, 30),
Position = UDim2.new(0, 100, 0, 10),
BackgroundColor3 = Color3.new(1, 1, 1),
Text = "X",
[Roact.Event.Activated] = function()
local newColor = self.props.newColor
if not newColor then
newColor = Color3.fromRGB(
math.random(0, 255),
math.random(0, 255),
math.random(0, 255)
)
print("Changing color to ", newColor)
end
local store = {}
for i = 1, 11 do
store[i] = newColor
end
self:setState({store = store})
end,
}),
})
end
return Story
@@ -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
@@ -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
@@ -0,0 +1,69 @@
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")
Story.defaultProps = {
startItems = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11},
newItems = {11, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1},
}
function Story:init()
self.state.items = self.props.startItems
end
function Story:render()
return Roact.createElement("Frame", {
Size = UDim2.fromScale(1, 1),
BackgroundTransparency = 1,
}, {
Scroller = Roact.createElement(Scroller, Cryo.Dictionary.join(
{
BackgroundColor3 = Color3.fromRGB(255, 0, 0),
Size = UDim2.fromOffset(50, 50),
padding = UDim.new(),
itemList = self.state.items,
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,
{
startItems = Cryo.None,
newItems = Cryo.None,
clicked = Cryo.None,
}
)),
Button = Roact.createElement("TextButton", {
Size = UDim2.fromOffset(100, 50),
Position = UDim2.fromScale(1, 0),
AnchorPoint = Vector2.new(1, 0),
Text = "Rearrange",
[Roact.Event.Activated] = function()
self:setState({
items = self.props.newItems,
})
if self.props.clicked then
self.props.clicked()
end
end,
})
})
end
return Story
@@ -0,0 +1,96 @@
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 - delete top item test")
local END_OF_LIST_INDEX = 100
function Story:init()
self.state.items = {}
if self.props.loadAll then
for i = 1, END_OF_LIST_INDEX do
table.insert(self.state.items, {id = i})
end
else
for i = 1, 20 do
table.insert(self.state.items, {id = i})
end
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(38, 161, 38),
Size = UDim2.new(0, 100, 0, 100),
padding = UDim.new(),
itemList = self.state.items,
focusIndex = 1,
anchorLocation = UDim.new(1, 0),
estimatedItemSize = 20,
orientation = Scroller.Orientation.Up,
BackgroundTransparency = 0,
dragBuffer = 0,
ElasticBehavior = Enum.ElasticBehavior.Always,
ScrollBarThickness = 10,
VerticalScrollBarInset = Enum.ScrollBarInset.Always,
identifier = function(item)
return item.id
end,
renderItem = function(item, _)
return Roact.createElement("TextButton", {
Size = UDim2.new(0, 20, 0, 20),
BackgroundColor3 = Color3.fromRGB(200,200, 200),
Text = tostring(item.id)
}, {
["INDEX" .. tostring(item.id)] = Roact.createElement("Frame"),
})
end,
loadNext = function()
if not self.props.loadAll then
local newItems = {}
local n = self.state.items[#self.state.items].id
local endIndex = math.min(n + 10, END_OF_LIST_INDEX)
for i = n+1, endIndex do
table.insert(newItems, {
id = i,
})
end
if not Cryo.isEmpty(newItems) then
self:setState({
items = Cryo.List.join(self.state.items, newItems)
})
end
end
end,
},
self.props,
{deleteLastItem = Cryo.None},
{loadAll = 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 indexToDelete = self.props.deleteLastItem and #self.state.items or 1
local nextItems = Cryo.List.removeIndex(self.state.items, indexToDelete)
self:setState({
items = nextItems
})
end,
}),
})
end
return Story
@@ -0,0 +1,121 @@
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")
Story.defaultProps = {
resizeAmount = 20,
initialHeight = 50,
}
function Story:init()
self.state = {
size = Vector2.new(50, self.props.initialHeight),
}
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,
{
resizeAmount = Cryo.None,
initialHeight = Cryo.None,
}
)),
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, -self.props.resizeAmount)
})
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, self.props.resizeAmount)
})
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(-self.props.resizeAmount, 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(self.props.resizeAmount, 0)
})
end,
}),
})
end
return Story
@@ -0,0 +1,46 @@
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("Frame", {
BackgroundTransparency = 1,
Size = self.props.frameSize or UDim2.new(0, 50, 0, 50),
}, {
layout = Roact.createElement("UIListLayout", {
VerticalAlignment = Enum.VerticalAlignment.Center,
HorizontalAlignment = Enum.HorizontalAlignment.Center,
}),
scroll = 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,
{ frameSize = Cryo.None }
))
})
end
return Story
@@ -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
@@ -0,0 +1,87 @@
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 - Swap a large list for a smaller one")
Story.defaultProps = {
anchorLocation = UDim.new(1, -10),
focusIndex = 1,
orientation = Scroller.Orientation.Up,
size = UDim2.new(0, 400, 0, 300),
shortToLong = false, -- Swap from a short list to a long list when true, swap from a long list to a short list when false
longItemList = {"a","b","c","d","e","f","g","h","i","j","k","l","m","n","o","p","q","r","s","t","u","v","w","x","y","z"},
shortItemList = {"Z", "X", "Y"},
}
function Story:init()
self.state = {
items = self.props.shortToLong and self.props.shortItemList or self.props.longItemList,
}
end
function Story:render()
return Roact.createElement("Frame", {
Size = UDim2.new(1, 0, 1, 0),
BackgroundTransparency = 1,
}, {
scroller = Roact.createElement(Scroller, Cryo.Dictionary.join(
{
BackgroundColor3 = Color3.fromRGB(56, 19, 18),
Size = self.props.size,
orientation = self.props.orientation,
itemList = self.state.items,
focusLock = self.state.items[1],
focusIndex = self.props.focusIndex,
anchorLocation = self.props.anchorLocation,
estimatedItemSize = 40,
dragBuffer = 0,
identifier = function(item)
return item
end,
renderItem = function(item, _)
return Roact.createElement("Frame", {
Size = UDim2.new(0, 300, 0, 40),
BackgroundColor3 = Color3.fromRGB(0, 255, 21),
}, {
["INDEX" .. tostring(item)] = Roact.createElement("TextLabel", {
Size = UDim2.new(1, 0, 1, 0),
Text = item,
TextColor3 = Color3.new(0, 0, 0),
BackgroundTransparency = 1,
}),
})
end,
},
self.props,
{
shortToLong = Cryo.None,
longItemList = Cryo.None,
shortItemList = Cryo.None,
size = Cryo.None,
}
)),
swapButton = Roact.createElement("TextButton", {
Size = UDim2.new(0, 100, 0, 50),
Position = UDim2.new(1, -100, 0, 0),
Text = "Swap Lists",
[Roact.Event.Activated] = function()
-- Swap lists
if #self.state.items == #self.props.shortItemList then
self:setState({
items = self.props.longItemList,
})
else
self:setState({
items = self.props.shortItemList,
})
end
end,
}),
})
end
return Story
@@ -0,0 +1,204 @@
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
-- Note: if you set this to ComplexThing, you may crash
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 = self.state.index,
anchorLocation = UDim.new(0, 0),
estimatedItemSize = 40,
identifier = function(item) return item.token end,
renderItem = function(item, _)
return Roact.createElement(Thing, item)
end,
onScrollUpdate = function(data)
self.indexData = data
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,
}),
skipUp = Roact.createElement("TextButton", {
Size = UDim2.new(0, 60, 0, 30),
Position = UDim2.new(1, -90, 0, 220),
AnchorPoint = Vector2.new(1, 0),
Text = "SkipUp",
BackgroundColor3 = Color3.fromRGB(255, 255, 255),
[Roact.Event.Activated] = self.skipUp,
}),
skipDown = Roact.createElement("TextButton", {
Size = UDim2.new(0, 60, 0, 30),
Position = UDim2.new(1, -90, 0, 270),
AnchorPoint = Vector2.new(1, 0),
Text = "SkipDown",
BackgroundColor3 = Color3.fromRGB(255, 255, 255),
[Roact.Event.Activated] = self.skipDown,
}),
SkipAmount = Roact.createElement("TextBox", {
Size = UDim2.new(0, 60, 0, 30),
Position = UDim2.new(1, 0, 0, 270),
AnchorPoint = Vector2.new(1, 0),
BackgroundColor3 = Color3.fromRGB(255, 255, 255),
Text = "SkipAmt",
PlaceholderText = "Skip",
ClearTextOnFocus = true,
[Roact.Change.Text] = self.onChangeText,
}),
})
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,
index = 101,
skipAmount = 1,
size = Vector2.new(200, -100),
items = items,
}
self.indexData = {
anchorIndex = 101,
}
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
self.skipUp = function()
local newIndex = self.indexData.anchorIndex + self.state.skipAmount
print("Skipping up to index:", newIndex)
self:setState({
lock = self.state.lock + 1,
index = newIndex,
})
end
self.skipDown = function()
local newIndex = self.indexData.anchorIndex - self.state.skipAmount
print("Skipping down to index:", newIndex)
self:setState({
lock = self.state.lock + 1,
index = newIndex,
})
end
self.onChangeText = function(rbx)
local skipAmount = tonumber(rbx.Text)
print("skipAmount:", skipAmount)
if skipAmount then
self:setState({
skipAmount = skipAmount,
})
else
rbx.Text = self.state.skipAmount
end
end
end
return Story
@@ -0,0 +1,513 @@
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 ComplexThing = require(script.Parent.ComplexThing)
local TextService = game:GetService("TextService")
local debugScroller = Roact.PureComponent:extend("debugScroller")
local COLORS = {
DEFAULT = Color3.fromRGB(188, 188, 188),
WHITE = Color3.fromRGB(244, 244, 244),
BLACK = Color3.fromRGB(0, 0, 0),
TRUE = Color3.fromRGB(188, 222, 188),
FALSE = Color3.fromRGB(222, 188, 188),
FOCUS_INDEX = Color3.fromRGB(0, 255, 0),
ANCHOR_LOCATION = Color3.fromRGB(0, 127, 255),
DRAG_BUFFER = Color3.fromRGB(222, 0, 222),
CANVAS = Color3.fromRGB(244, 188, 66),
LEAD_INDEX = Color3.fromRGB(0, 255, 0),
TRAIL_INDEX = Color3.fromRGB(255, 0, 0),
ANCHOR_INDEX = Color3.fromRGB(255, 255, 0),
PADDING = Color3.fromRGB(145, 88, 222),
}
local COMPLEX_THING_SIZE = 128
local LAYOUT_ORDER_INDEX = 0
function getNextLayout()
LAYOUT_ORDER_INDEX = LAYOUT_ORDER_INDEX + 1
return LAYOUT_ORDER_INDEX
end
function makeLabel(text, value, color)
return Roact.createElement("TextLabel", {
Size = UDim2.new(1, 0, 0, 20),
Text = string.format("%s: %s", text, tostring(value)),
TextColor3 = color or COLORS.WHITE,
TextXAlignment = Enum.TextXAlignment.Left,
BorderSizePixel = 0,
BackgroundTransparency = 1,
LayoutOrder = getNextLayout(),
})
end
function makeButton(text, func, color)
color = color or COLORS.DEFAULT
return Roact.createElement("TextButton", {
Size = UDim2.fromOffset(110, 30),
BackgroundColor3 = color,
TextWrapped = true,
Text = text,
[Roact.Event.Activated] = func,
LayoutOrder = getNextLayout(),
})
end
function debugScroller:makeToggleButton(value)
local function doToggle()
self:setState({
[value] = not self.state[value]
})
end
return makeButton(value, doToggle, self.state[value] and COLORS.TRUE or COLORS.FALSE)
end
function makeLabelLine(text, position, color, pointsRight)
color = color or COLORS.BLACK
local LINE_WIDTH = 20
local textSize = TextService:GetTextSize(text, 8, Enum.Font.Legacy, Vector2.new())
return Roact.createElement("Frame", {
Size = UDim2.fromOffset(textSize.X + LINE_WIDTH, textSize.Y),
Position = position,
AnchorPoint = pointsRight and Vector2.new(1, 0.5) or Vector2.new(0, 0.5),
BackgroundTransparency = 1,
}, {
Roact.createFragment({
layout = Roact.createElement("UIListLayout", {
SortOrder = Enum.SortOrder.LayoutOrder,
FillDirection = Enum.FillDirection.Horizontal,
VerticalAlignment = Enum.VerticalAlignment.Center,
}),
labelLine = Roact.createElement("Frame", {
Position = position,
Size = UDim2.fromOffset(LINE_WIDTH, 1),
BorderSizePixel = 0,
BackgroundColor3 = color,
LayoutOrder = 2,
}),
label = Roact.createElement("TextLabel", {
AnchorPoint = Vector2.new(0, 0.5),
Size = UDim2.new(1, -LINE_WIDTH, 0, 8),
Text = text,
TextColor3 = color,
BackgroundTransparency = 1,
LayoutOrder = pointsRight and 1 or 3
})
})
})
end
debugScroller.defaultProps = {
anchorLocation = UDim.new(1, 0),
mountingBuffer = 150,
dragBuffer = 0,
focusIndex = 1,
Size = UDim2.new(1, -20, 0, 100),
numItems = 20,
}
function debugScroller:init()
self.initialState = {
items = {},
-- Scroller props. These can be changed by debugger buttons
focusLock = 1,
orientation = Scroller.Orientation.Up,
clipsDescendants = false,
nestedLayer = 1,
-- Scroller internals. Don't change these manually, meant for DISPLAY ONLY
canvasPosition = 0,
canvasSize = 0,
paddingSize = -1,
paddingPosition = 0,
anchorLinePositionY = 0,
leadIndex = -1,
trailIndex = -1,
anchorIndex = -1,
}
self.mutableState = {
loadPreviousEnabled = false,
loadNextEnabled = false,
}
for i = 1,self.props.numItems do
table.insert(self.initialState.items, {id = i})
end
self.state = Cryo.Dictionary.join(self.initialState, self.mutableState)
-- horsecat storybooks have a Y-offset that we need to address before using absolutePosition
self.initialY, self.updateInitialY = Roact.createBinding(0)
self.ref = Roact.createRef()
end
function debugScroller:render()
return Roact.createElement("Frame", {
Size=UDim2.new(1, 0, 1, 0),
BackgroundTransparency = 1,
[Roact.Change.AbsolutePosition] = function(rbx)
self.updateInitialY(rbx.AbsolutePosition.Y)
end
}, {
layout = Roact.createElement("UIListLayout", {
SortOrder = Enum.SortOrder.LayoutOrder,
FillDirection = Enum.FillDirection.Horizontal,
}),
visibilityFrame = Roact.createElement("Frame", {
LayoutOrder = 1,
Size = UDim2.new(0, 200, 1, 0),
BackgroundTransparency = 1,
}, {
layout = Roact.createElement("UIListLayout", {
SortOrder = Enum.SortOrder.LayoutOrder,
}),
anchorLocationLabel = makeLabel("anchorLocation", self.props.anchorLocation, COLORS.ANCHOR_LOCATION),
anchorIndexLabel = makeLabel("anchorIndex", self.state.anchorIndex, COLORS.ANCHOR_INDEX),
leadIndexLabel = makeLabel("leadIndex", self.state.leadIndex, COLORS.LEAD_INDEX),
trailIndexLabel = makeLabel("trailIndex", self.state.trailIndex, COLORS.TRAIL_INDEX),
canvasPositionLabel = makeLabel("canvasPosition", self.state.canvasPosition, COLORS.CANVAS),
canvasSizeLabel = makeLabel("canvasSize", self.state.canvasSize, COLORS.CANVAS),
paddingSizeLabel = makeLabel("size of padding Frame", self.state.paddingSize, COLORS.PADDING),
whiteSpace = Roact.createElement("Frame", { Size = UDim2.fromOffset(0, 20), BackgroundTransparency = 1, LayoutOrder = getNextLayout()}),
disableLoadPrevious = self:makeToggleButton("loadPreviousEnabled"),
disableLoadNext = self:makeToggleButton("loadNextEnabled"),
clipsDescendants = self:makeToggleButton("clipsDescendants"),
}),
scrollerFrame = Roact.createElement("Frame", {
LayoutOrder = 2,
Size = UDim2.new(0, 160, 0, 300),
BackgroundTransparency = 1,
}, {
scroller = Roact.createElement(Scroller, {
ElasticBehavior = Enum.ElasticBehavior.Always,
ClipsDescendants = self.state.clipsDescendants,
Position = UDim2.fromOffset(0, 200),
BackgroundColor3 = Color3.fromRGB(111, 111, 111),
Size = self.props.Size,
orientation = self.state.orientation,
padding = UDim.new(),
itemList = self.state.items,
focusIndex = self.props.focusIndex,
focusLock = self.state.focusLock,
anchorLocation = self.props.anchorLocation,
dragBuffer = self.props.dragBuffer,
mountingBuffer = self.props.mountingBuffer,
estimatedItemSize = self.state.nestedLayer ~= 1 and COMPLEX_THING_SIZE or 20,
[Roact.Ref] = self.ref,
identifier = function(item)
return item.id
end,
--recyclingDisabledFor={"ComplexThing"},
renderItem = function(item, _)
if self.state.nestedLayer ~= 1 then
return Roact.createElement(ComplexThing, {
Size = UDim2.fromOffset(COMPLEX_THING_SIZE, COMPLEX_THING_SIZE),
nestedLayer = self.state.nestedLayer,
})
else
local r = 88+88*math.sin(math.rad(8*item.id+90))
local g = 88+44*math.sin(math.rad(8*item.id+0))
local b = 88+88*math.sin(math.rad(8*item.id+180))
local leadIndexId = self.state.items[self.state.leadIndex] and self.state.items[self.state.leadIndex].id
local trailIndexId = self.state.items[self.state.trailIndex] and self.state.items[self.state.trailIndex].id
local anchorIndexId = self.state.items[self.state.anchorIndex] and self.state.items[self.state.anchorIndex].id
return Roact.createElement("Frame", {
Size = UDim2.new(0, 20, 0, 20),
BackgroundColor3 = Color3.fromRGB(r, g, b),
}, {
["INDEX" .. tostring(item.id)] = Roact.createElement("TextLabel", {
Size = UDim2.new(1, 0, 1, 0),
Text = item.id,
TextColor3 = Color3.new(1, 1, 1),
BackgroundTransparency = 1,
}, {
item.id == leadIndexId and makeLabelLine("leadIndex ",
UDim2.fromScale(1, 0),
COLORS.LEAD_INDEX,
false),
item.id == trailIndexId and makeLabelLine("trailIndex ",
UDim2.fromScale(1, 0),
COLORS.TRAIL_INDEX,
false),
item.id == anchorIndexId and makeLabelLine("anchorIndex ",
UDim2.new(),
COLORS.ANCHOR_INDEX,
true)
}),
})
end
end,
loadPrevious = function()
if not self.state.loadPreviousEnabled then
return
end
local newItems = {}
local n = self.state.items[1].id
for i = n-10, n-1 do
table.insert(newItems, {
id = i,
})
end
self:setState({
items = Cryo.List.join(newItems, self.state.items)
})
end,
loadNext = function()
if not self.state.loadNextEnabled then
return
end
local newItems = {}
local n = self.state.items[#self.state.items].id
for i = n+1, n+10 do
table.insert(newItems, {
id = i,
})
end
self:setState({
items = Cryo.List.join(self.state.items, newItems)
})
end,
[Roact.Change.CanvasPosition] = function()
if not self.ref.current then
return
end
local rbx = self.ref.current
-- this feels dirty, but allows us to visualize the padding frame
local padding_rbx = rbx:FindFirstChild("padding")
local anchorLinePositionY = rbx.AbsolutePosition.Y - self.props.anchorLocation.Offset
if self.state.orientation == Scroller.Orientation.Down then
anchorLinePositionY = anchorLinePositionY + (1 - self.props.anchorLocation.Scale) * rbx.AbsoluteSize.Y
elseif self.state.orientation == Scroller.Orientation.Up then
anchorLinePositionY = anchorLinePositionY + self.props.anchorLocation.Scale * rbx.AbsoluteSize.Y
else
anchorLinePositionY = 0
end
self:setState({
canvasPosition = rbx.CanvasPosition.Y,
canvasSize = rbx.CanvasSize.Y.Offset,
paddingSize = padding_rbx.Size.Y.Offset,
anchorLinePositionY = anchorLinePositionY,
paddingPosition = padding_rbx.AbsolutePosition.Y,
})
end,
onScrollUpdate = function(indices)
self.state.leadIndex = indices.leadIndex
self.state.anchorIndex = indices.anchorIndex
self.state.trailIndex = indices.trailIndex
end
}),
anchorLine = self.ref.current and makeLabelLine("anchorLocation ",
UDim2.fromOffset(20, self.state.anchorLinePositionY - self.initialY:getValue()),
COLORS.ANCHOR_LOCATION,
true),
dragBuffer1 = self.ref.current and self.props.dragBuffer ~= 0 and makeLabelLine("dragBuffer ",
UDim2.fromOffset(20, self.props.dragBuffer + self.ref.current.AbsolutePosition.Y - self.initialY:getValue()),
COLORS.DRAG_BUFFER,
true),
dragBuffer2 = self.ref.current and self.props.dragBuffer ~= 0 and makeLabelLine("dragBuffer ",
UDim2.fromOffset(20, self.ref.current.AbsoluteSize.Y - self.props.dragBuffer + self.ref.current.AbsolutePosition.Y - self.initialY:getValue()),
COLORS.DRAG_BUFFER,
true),
mountingBuffer1 = self.ref.current and makeLabelLine("mountingBuffer ",
UDim2.fromOffset(20, -self.props.mountingBuffer + self.ref.current.AbsolutePosition.Y - self.initialY:getValue()),
COLORS.WHITE,
true),
mountingBuffer2 = self.ref.current and makeLabelLine("mountingBuffer ",
UDim2.fromOffset(20, self.ref.current.AbsoluteSize.Y + self.props.mountingBuffer + self.ref.current.AbsolutePosition.Y - self.initialY:getValue()),
COLORS.WHITE,
true),
canvasEstimate = self.ref.current and Roact.createElement("Frame", {
Size = UDim2.fromOffset(40, self.state.canvasSize),
Position = UDim2.fromOffset(20, self.state.paddingPosition - self.initialY:getValue()),
BackgroundColor3 = COLORS.CANVAS,
BorderSizePixel = 0,
ZIndex = -10,
}),
paddingEstimate = self.ref.current and Roact.createElement("Frame", {
Size = UDim2.fromOffset(30, self.state.paddingSize),
Position = UDim2.fromOffset(0, self.state.paddingPosition - self.initialY:getValue()),
BackgroundColor3 = COLORS.PADDING,
BorderSizePixel = 0,
ZIndex = -9,
}),
}),
operationsFrame = Roact.createElement("Frame", {
LayoutOrder = 3,
Size = UDim2.new(0, 100, 1, 0),
BackgroundTransparency = 1,
}, {
layout = Roact.createElement("UIListLayout", {
SortOrder = Enum.SortOrder.LayoutOrder,
}),
reset = makeButton("Reset ItemList", function()
local newState = Cryo.Dictionary.join(self.initialState, {
focusLock = self.state.focusLock + 1,
loadNext = self.state.loadNext,
loadPrev = self.state.loadPrev,
clipsDescendants = self.state.clipsDescendants,
})
self:setState(newState)
end),
-- Reversing the Orientation currently does not call resize, does not update the anchorLocation
-- reverse = makeButton("Reverse Orientation", function()
-- local newOrientation
-- if self.state.orientation == Scroller.Orientation.Down then
-- newOrientation = Scroller.Orientation.Up
-- else
-- newOrientation = Scroller.Orientation.Down
-- end
-- self:setState({
-- orientation = newOrientation,
-- focusLock = self.state.focusLock + 1,
-- })
-- end),
rotateForward = makeButton("Rotate Forward", function()
local nextItems = {}
local numItems = #self.state.items
local a = self.state.items[numItems]
table.insert(nextItems, a)
for i = 1, numItems-1 do
table.insert(nextItems, self.state.items[i])
end
self:setState({
focusLock = self.state.focusLock + 1,
items = nextItems,
})
end),
rotateBack = makeButton("Rotate Backward", function()
local nextItems = {}
local a = self.state.items[1]
for i = 2, #self.state.items do
table.insert(nextItems, self.state.items[i])
end
table.insert(nextItems, a)
self:setState({
focusLock = self.state.focusLock + 1,
items = nextItems,
})
end),
insertFront = makeButton("Insert Front", function()
local nextItems = {}
table.insert(nextItems, {id = self.state.items[1].id-1 })
for k, v in pairs(self.state.items) do
table.insert(nextItems, v)
end
self:setState({
focusLock = self.state.focusLock + 1,
items = nextItems,
})
end),
insertBack = makeButton("Insert Back", function()
local nextItems = {}
for k, v in pairs(self.state.items) do
table.insert(nextItems, v)
end
table.insert(nextItems, {id = self.state.items[#self.state.items].id+1 })
self:setState({
focusLock = self.state.focusLock + 1,
items = nextItems,
})
end),
removeFront = makeButton("Remove Front", function()
local nextItems = {}
local numItems = #self.state.items
for i = 2, numItems do
table.insert(nextItems, self.state.items[i])
end
self:setState({
focusLock = self.state.focusLock + 1,
items = nextItems,
})
end),
removeBack = makeButton("Remove Back", function()
local nextItems = {}
local numItems = #self.state.items
for i = 1, numItems-1 do
table.insert(nextItems, self.state.items[i])
end
self:setState({
focusLock = self.state.focusLock + 1,
items = nextItems,
})
end),
reverseList = makeButton("Reverse List", function()
local nextItems = {}
local numItems = #self.state.items
for i = 1, numItems do
nextItems[i] = self.state.items[numItems - i + 1]
end
self:setState({
focusLock = self.state.focusLock + 1,
items = nextItems,
})
end),
scrollUpOnce = makeButton("Scroll Up 1px", function()
if self.ref.current then
self.ref.current.CanvasPosition = self.ref.current.CanvasPosition - Vector2.new(0, 1)
end
end),
scrollDownOnce = makeButton("Scroll Down 1px", function()
if self.ref.current then
self.ref.current.CanvasPosition = self.ref.current.CanvasPosition + Vector2.new(0, 1)
end
end),
toggleComplexity = makeButton("Toggle Complexity", function()
self:setState({
nestedLayer = self.state.nestedLayer%5 + 1
})
end),
})
})
end
return debugScroller
@@ -0,0 +1,5 @@
local Scroller = require(script.Components.Scroller)
return {
Scroller = Scroller,
}
@@ -0,0 +1,12 @@
# Generated by Rotriever. Format subject to change in future releases.
name = "roblox/infinite-scroller"
version = "0.5.6"
commit = "d622d74bec4a599c5f8ef642194fa9eaf973b5c0"
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",
]
@@ -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