Skip to content

Instantly share code, notes, and snippets.

@cxmeel
Last active May 20, 2024 11:17
Show Gist options
  • Save cxmeel/02e109df44130b4fc8ac595b618e1970 to your computer and use it in GitHub Desktop.
Save cxmeel/02e109df44130b4fc8ac595b618e1970 to your computer and use it in GitHub Desktop.
StoryCanvas is a component for rendering React components in Hoarcekat.

StoryCanvas

StoryCanvas is a component for rendering React components in Hoarcekat.

Screenshot of StoryCanvas being rendered in Hoarcekat

A StoryCanvas component handles mounting and unmounting of stories automatically. It features:

  • A checkered background (synced with Studio theme)
  • A UIListLayout to display components centrally and evenly spaced
  • A ScrollingFrame to handle overflows

Additionally, clicking the "UI" button in Hoarcekat will automatically move focus to the StoryCanvas' internal ScrollingFrame so you can inspect the contents quicker. Clicking the "UI" button a second time will return the selection to the Hoarcekat render target if you need it.

local React = require(script.Parent.Parent.Parent.Packages.React)

local StoryCanvas = require(script.Parent.StoryCanvas)
local Button = require(script.Parent.Button)

local e = React.createElement

return StoryCanvas.render({
	e(Button, {
		text = "Primary Button",
		variant = "primary",
	}),

	e(Button, {
		text = "Default Button",
		variant = "default",
	}),
})
-- StoryCanvas props
type Props = {
	useLayout: boolean?,
	direction: Enum.FillDirection?,
	spacing: number?,
	children: React.ReactNode,
}

-- Default props
local DEFAULT_PROPS = {
	useLayout = true,
	direction = Enum.FillDirection.Vertical,
	spacing = 8,
}
local Selection = game:GetService("Selection")
local React = require(script.Parent.Parent.Parent.Packages.React)
local ReactRoblox = require(script.Parent.Parent.Parent.Packages.ReactRoblox)
local useStudioTheme = require(script.Parent.Parent.Parent.Hooks.useStudioTheme)
local e = React.createElement
type Props = {
useLayout: boolean?,
direction: Enum.FillDirection?,
spacing: number?,
children: React.ReactNode,
}
local DEFAULT_PROPS = {
useLayout = true,
direction = Enum.FillDirection.Vertical,
spacing = 8,
}
local CanvasComponent = React.forwardRef(function(props: Props, ref)
local theme = useStudioTheme()
local isLight = theme.Name == "Light"
local canvasSize, setCanvasSize = React.useState(Vector2.one)
local contentSize, setContentSize = React.useState(Vector2.one)
local doesOverflow = contentSize.Y > canvasSize.Y
local useLayout = if props.useLayout == nil
then DEFAULT_PROPS.useLayout
else props.useLayout
local innerCanvasRef = React.useRef(nil :: ScrollingFrame?)
React.useImperativeHandle(ref, function()
return innerCanvasRef.current
end)
return e("ImageLabel", {
key = "StoryCanvas",
BackgroundColor3 = isLight and Color3.new(1, 1, 1) or Color3.new(),
BorderSizePixel = 0,
Size = UDim2.fromScale(1, 1),
Image = "rbxasset://textures/meshPartFallback.png",
ImageTransparency = 0.8,
ScaleType = Enum.ScaleType.Tile,
TileSize = UDim2.fromOffset(16, 16),
}, {
Padding = doesOverflow and e("UIPadding", {
PaddingRight = UDim.new(0, 8),
PaddingTop = UDim.new(0, 8),
-- PaddingBottom = UDim.new(0, 8),
PaddingLeft = UDim.new(0, 8),
}),
Canvas = e("ScrollingFrame", {
ref = innerCanvasRef,
BackgroundTransparency = 1,
BorderSizePixel = 0,
Size = UDim2.new(1, 0, 1, 0),
AutomaticCanvasSize = Enum.AutomaticSize.Y,
CanvasSize = UDim2.new(),
VerticalScrollBarInset = Enum.ScrollBarInset.ScrollBar,
ScrollingDirection = Enum.ScrollingDirection.Y,
ScrollBarThickness = 8,
ScrollBarImageColor3 = theme:GetColor(
Enum.StudioStyleGuideColor.DimmedText
),
BottomImage = "rbxasset://textures/AnimationEditor/image_scrollbar_vertical_bottom.png",
MidImage = "rbxasset://textures/AnimationEditor/image_scrollbar_vertical_mid.png",
TopImage = "rbxasset://textures/AnimationEditor/image_scrollbar_vertical_top.png",
[React.Change.AbsoluteSize] = function(rbx: ScrollingFrame)
setCanvasSize(rbx.AbsoluteSize)
end,
}, {
layout = useLayout and e("UIListLayout", {
Padding = UDim.new(0, props.spacing or DEFAULT_PROPS.spacing),
FillDirection = props.direction or DEFAULT_PROPS.direction,
HorizontalAlignment = Enum.HorizontalAlignment.Center,
SortOrder = Enum.SortOrder.LayoutOrder,
VerticalAlignment = doesOverflow and Enum.VerticalAlignment.Top
or Enum.VerticalAlignment.Center,
[React.Change.AbsoluteContentSize] = function(rbx: UIListLayout)
setContentSize(rbx.AbsoluteContentSize)
end,
}),
padding = e("UIPadding", {
PaddingBottom = UDim.new(0, 8),
PaddingLeft = UDim.new(0, 8),
PaddingRight = UDim.new(0, 8),
PaddingTop = UDim.new(0, 8),
}),
}, props.children),
})
end)
local function renderCanvas(children: React.ReactNode, props: Props?)
local withProps = props or DEFAULT_PROPS
return function(target: Instance)
local appRef = React.createRef()
withProps.ref = appRef
local root = ReactRoblox.createRoot(target)
local app = React.createElement(CanvasComponent, withProps, children)
root:render(app)
local hoarcekatToggle = true
local signal = Selection.SelectionChanged:Connect(function()
local hoarcekatSelected = table.find(Selection:Get(), target)
if appRef.current and hoarcekatToggle and hoarcekatSelected then
Selection:Add({ appRef.current })
Selection:Remove({ target })
end
if hoarcekatSelected then
hoarcekatToggle = not hoarcekatToggle
end
end)
return function()
signal:Disconnect()
root:unmount()
end
end
end
return {
render = renderCanvas,
}
local Studio = settings().Studio
local React = require(script.Parent.Parent.Packages.React)
local function useStudioTheme()
local theme, setTheme = React.useState(Studio.Theme)
React.useEffect(function()
local connection = Studio.ThemeChanged:Connect(function()
setTheme(Studio.Theme)
end)
return function()
connection:Disconnect()
end
end, {})
return theme
end
return useStudioTheme
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment