mirror of
https://github.com/rojo-rbx/rojo.git
synced 2026-10-24 06:34:09 +00:00
- Replaced `Flipper.Spring` with `Flipper.Instant` for setting animation goals across multiple components, including `Page`, `Checkbox`, `Dropdown`, `IconButton`, `TextButton`, `TextInput`, `TouchRipple`, and `Notification`. - This change simplifies the animation logic, providing immediate transitions without spring dynamics, enhancing performance and responsiveness. These updates improve the user experience by ensuring quicker visual feedback during state changes.
767 lines
23 KiB
Lua
767 lines
23 KiB
Lua
local SelectionService = game:GetService("Selection")
|
|
|
|
local Rojo = script:FindFirstAncestor("Rojo")
|
|
local Plugin = Rojo.Plugin
|
|
local Packages = Rojo.Packages
|
|
|
|
local Roact = require(Packages.Roact)
|
|
local Flipper = require(Packages.Flipper)
|
|
|
|
local Assets = require(Plugin.Assets)
|
|
local Theme = require(Plugin.App.Theme)
|
|
local bindingUtil = require(Plugin.App.bindingUtil)
|
|
|
|
local e = Roact.createElement
|
|
|
|
local ChangeList = require(script.Parent.ChangeList)
|
|
local ClassIcon = require(Plugin.App.Components.ClassIcon)
|
|
|
|
local ChangeTag = Roact.Component:extend("ChangeTag")
|
|
|
|
function ChangeTag:init()
|
|
self:setState({
|
|
isHovered = false,
|
|
})
|
|
end
|
|
|
|
function ChangeTag:render()
|
|
local props = self.props
|
|
local isHovered = self.state.isHovered
|
|
|
|
return Theme.with(function(theme)
|
|
local tagColor = props.color or theme.SubTextColor
|
|
return e("Frame", {
|
|
Size = UDim2.new(0, 0, 0, 16),
|
|
AutomaticSize = Enum.AutomaticSize.X,
|
|
BackgroundColor3 = tagColor,
|
|
BackgroundTransparency = props.transparency:map(function(t)
|
|
return 0.85 + (0.15 * t)
|
|
end),
|
|
LayoutOrder = props.layoutOrder or 1,
|
|
Active = true,
|
|
[Roact.Event.MouseEnter] = function()
|
|
self:setState({ isHovered = true })
|
|
end,
|
|
[Roact.Event.MouseLeave] = function()
|
|
self:setState({ isHovered = false })
|
|
end,
|
|
}, {
|
|
Corner = e("UICorner", {
|
|
CornerRadius = UDim.new(0, 3),
|
|
}),
|
|
Padding = e("UIPadding", {
|
|
PaddingLeft = UDim.new(0, 5),
|
|
PaddingRight = UDim.new(0, 5),
|
|
}),
|
|
Label = e("TextLabel", {
|
|
Text = props.text,
|
|
BackgroundTransparency = 1,
|
|
FontFace = theme.Font.Thin,
|
|
TextSize = theme.TextSize.Small,
|
|
TextColor3 = tagColor,
|
|
TextXAlignment = Enum.TextXAlignment.Center,
|
|
TextTransparency = props.transparency,
|
|
Size = UDim2.new(0, 0, 1, 0),
|
|
AutomaticSize = Enum.AutomaticSize.X,
|
|
}),
|
|
Tooltip = if props.tooltipText and isHovered
|
|
then e("TextLabel", {
|
|
Text = props.tooltipText,
|
|
BackgroundColor3 = theme.BorderedContainer.BackgroundColor,
|
|
BackgroundTransparency = 0.05,
|
|
TextColor3 = theme.TextColor,
|
|
FontFace = theme.Font.Main,
|
|
TextSize = theme.TextSize.Small,
|
|
Size = UDim2.new(0, 0, 0, 20),
|
|
AutomaticSize = Enum.AutomaticSize.X,
|
|
Position = UDim2.new(0.5, 0, 0, -22),
|
|
AnchorPoint = Vector2.new(0.5, 0),
|
|
ZIndex = 100,
|
|
}, {
|
|
Corner = e("UICorner", {
|
|
CornerRadius = UDim.new(0, 3),
|
|
}),
|
|
Padding = e("UIPadding", {
|
|
PaddingLeft = UDim.new(0, 6),
|
|
PaddingRight = UDim.new(0, 6),
|
|
}),
|
|
})
|
|
else nil,
|
|
})
|
|
end)
|
|
end
|
|
|
|
-- Proportional diff bar showing the ratio of removals/additions/property changes
|
|
local function DiffBar(props)
|
|
local removed = props.removed or 0
|
|
local added = props.added or 0
|
|
local propChanges = props.propChanges or 0
|
|
local total = removed + added + propChanges
|
|
if total == 0 then
|
|
return nil
|
|
end
|
|
|
|
return Theme.with(function(theme)
|
|
local children = {
|
|
Layout = e("UIListLayout", {
|
|
FillDirection = Enum.FillDirection.Horizontal,
|
|
SortOrder = Enum.SortOrder.LayoutOrder,
|
|
}),
|
|
Corner = e("UICorner", {
|
|
CornerRadius = UDim.new(0, 2),
|
|
}),
|
|
}
|
|
|
|
if propChanges > 0 then
|
|
children.SegProp = e("Frame", {
|
|
Size = UDim2.fromScale(propChanges / total, 1),
|
|
BackgroundColor3 = theme.Diff.Property,
|
|
BackgroundTransparency = props.transparency,
|
|
BorderSizePixel = 0,
|
|
LayoutOrder = 1,
|
|
})
|
|
end
|
|
|
|
if added > 0 then
|
|
local color = if props.isWhitespaceOnly then theme.Diff.WhitespaceOnly else theme.Diff.Add
|
|
children.SegAdd = e("Frame", {
|
|
Size = UDim2.fromScale(added / total, 1),
|
|
BackgroundColor3 = color,
|
|
BackgroundTransparency = props.transparency,
|
|
BorderSizePixel = 0,
|
|
LayoutOrder = 2,
|
|
})
|
|
end
|
|
|
|
if removed > 0 then
|
|
local color = if props.isWhitespaceOnly then theme.Diff.WhitespaceOnly else theme.Diff.Remove
|
|
children.SegRemove = e("Frame", {
|
|
Size = UDim2.fromScale(removed / total, 1),
|
|
BackgroundColor3 = color,
|
|
BackgroundTransparency = props.transparency,
|
|
BorderSizePixel = 0,
|
|
LayoutOrder = 3,
|
|
})
|
|
end
|
|
|
|
return e("Frame", {
|
|
Size = UDim2.new(0, 40, 0, 8),
|
|
BackgroundTransparency = 1,
|
|
ClipsDescendants = true,
|
|
LayoutOrder = props.layoutOrder or 10,
|
|
}, children)
|
|
end)
|
|
end
|
|
|
|
-- Radio button option for selection (as a component to track hover state)
|
|
local SelectionOption = Roact.Component:extend("SelectionOption")
|
|
|
|
function SelectionOption:init()
|
|
self:setState({
|
|
isHovered = false,
|
|
})
|
|
end
|
|
|
|
function SelectionOption:render()
|
|
local props = self.props
|
|
local isHovered = self.state.isHovered
|
|
|
|
return Theme.with(function(theme)
|
|
local isSelected = props.isSelected
|
|
local isParentOnly = props.isParentOnly
|
|
local hasChildren = props.hasChildren
|
|
|
|
local bgColor = if isSelected
|
|
then (if props.optionType == "push"
|
|
then Color3.fromHex("335FFF")
|
|
elseif props.optionType == "pull" then Color3.fromHex("E74C3C")
|
|
else Color3.fromHex("7F8C8D"))
|
|
else theme.BorderedContainer.BackgroundColor
|
|
local textColor = if isSelected then Color3.new(1, 1, 1) else theme.TextColor
|
|
|
|
local displayText = props.text
|
|
-- Use wider button for "Studio" text (6 chars vs 4 for others)
|
|
local buttonWidth = if props.text == "Studio" then 44 else 36
|
|
|
|
-- Transparency logic with hover effect
|
|
local bgTransparency, txtTransparency
|
|
if isParentOnly then
|
|
-- Parent-only: partially transparent, less so when hovering the button
|
|
bgTransparency = props.transparency:map(function(t)
|
|
return if isHovered then 0.2 + (0.8 * t) else 0.5 + (0.5 * t)
|
|
end)
|
|
txtTransparency = props.transparency:map(function(t)
|
|
return if isHovered then 0 + t else 0.3 + (0.7 * t)
|
|
end)
|
|
else
|
|
-- Normal buttons: show hover effect by reducing transparency
|
|
bgTransparency = props.transparency:map(function(t)
|
|
if isSelected then
|
|
return if isHovered then 0 + (1 * t) else 0.1 + (0.9 * t)
|
|
else
|
|
return if isHovered then 0.5 + (0.5 * t) else 0.7 + (0.3 * t)
|
|
end
|
|
end)
|
|
txtTransparency = props.transparency
|
|
end
|
|
|
|
return e("TextButton", {
|
|
Size = UDim2.new(0, buttonWidth, 0, 18),
|
|
BackgroundColor3 = bgColor,
|
|
BackgroundTransparency = bgTransparency,
|
|
BorderSizePixel = 0,
|
|
Text = displayText,
|
|
FontFace = if isSelected then theme.Font.Bold else theme.Font.Main,
|
|
TextSize = theme.TextSize.Small,
|
|
TextColor3 = textColor,
|
|
TextTransparency = txtTransparency,
|
|
LayoutOrder = props.layoutOrder,
|
|
ZIndex = 10,
|
|
[Roact.Event.MouseEnter] = function()
|
|
self:setState({ isHovered = true })
|
|
-- Trigger subtree highlight when this button would apply to children
|
|
if (isParentOnly or (isSelected and hasChildren)) and props.onSubtreeHoverStart then
|
|
props.onSubtreeHoverStart()
|
|
end
|
|
end,
|
|
[Roact.Event.MouseLeave] = function()
|
|
self:setState({ isHovered = false })
|
|
if props.onSubtreeHoverEnd then
|
|
props.onSubtreeHoverEnd()
|
|
end
|
|
end,
|
|
[Roact.Event.Activated] = function()
|
|
if isParentOnly or (isSelected and hasChildren) then
|
|
-- Parent-only or already selected with children: click applies to subtree
|
|
if props.onSubtreeClick then
|
|
props.onSubtreeClick()
|
|
end
|
|
else
|
|
-- Normal click: select this node only
|
|
if props.onClick then
|
|
props.onClick()
|
|
end
|
|
end
|
|
end,
|
|
}, {
|
|
Corner = e("UICorner", {
|
|
CornerRadius = UDim.new(0, 3),
|
|
}),
|
|
})
|
|
end)
|
|
end
|
|
|
|
-- Selection radio group component
|
|
local function SelectionRadio(props)
|
|
if not props.visible then
|
|
return nil
|
|
end
|
|
|
|
local hasChildren = props.hasChildren
|
|
local isParentOnly = props.isParentOnly
|
|
|
|
return e("Frame", {
|
|
Size = UDim2.new(0, 0, 0, 18),
|
|
AutomaticSize = Enum.AutomaticSize.X,
|
|
BackgroundTransparency = 1,
|
|
Position = UDim2.new(1, -5, 0, 3),
|
|
AnchorPoint = Vector2.new(1, 0),
|
|
ZIndex = 10,
|
|
}, {
|
|
Layout = e("UIListLayout", {
|
|
FillDirection = Enum.FillDirection.Horizontal,
|
|
HorizontalAlignment = Enum.HorizontalAlignment.Right,
|
|
VerticalAlignment = Enum.VerticalAlignment.Center,
|
|
SortOrder = Enum.SortOrder.LayoutOrder,
|
|
Padding = UDim.new(0, 4),
|
|
}),
|
|
Studio = e(SelectionOption, {
|
|
text = "Studio",
|
|
optionType = "pull",
|
|
isSelected = props.selection == "pull",
|
|
transparency = props.transparency,
|
|
layoutOrder = 1,
|
|
hasChildren = hasChildren,
|
|
isParentOnly = isParentOnly,
|
|
onSubtreeHoverStart = props.onSubtreeHoverStart,
|
|
onSubtreeHoverEnd = props.onSubtreeHoverEnd,
|
|
onClick = function()
|
|
if props.onSelectionChange then
|
|
props.onSelectionChange(props.nodeId, "pull")
|
|
end
|
|
end,
|
|
onSubtreeClick = function()
|
|
if props.onSubtreeSelectionChange then
|
|
props.onSubtreeSelectionChange(props.nodeId, "pull")
|
|
end
|
|
end,
|
|
}),
|
|
Skip = e(SelectionOption, {
|
|
text = "Skip",
|
|
optionType = "ignore",
|
|
isSelected = props.selection == "ignore",
|
|
transparency = props.transparency,
|
|
layoutOrder = 2,
|
|
hasChildren = hasChildren,
|
|
isParentOnly = isParentOnly,
|
|
onSubtreeHoverStart = props.onSubtreeHoverStart,
|
|
onSubtreeHoverEnd = props.onSubtreeHoverEnd,
|
|
onClick = function()
|
|
if props.onSelectionChange then
|
|
props.onSelectionChange(props.nodeId, "ignore")
|
|
end
|
|
end,
|
|
onSubtreeClick = function()
|
|
if props.onSubtreeSelectionChange then
|
|
props.onSubtreeSelectionChange(props.nodeId, "ignore")
|
|
end
|
|
end,
|
|
}),
|
|
Atlas = e(SelectionOption, {
|
|
text = "Atlas",
|
|
optionType = "push",
|
|
isSelected = props.selection == "push",
|
|
transparency = props.transparency,
|
|
layoutOrder = 3,
|
|
hasChildren = hasChildren,
|
|
isParentOnly = isParentOnly,
|
|
onSubtreeHoverStart = props.onSubtreeHoverStart,
|
|
onSubtreeHoverEnd = props.onSubtreeHoverEnd,
|
|
onClick = function()
|
|
if props.onSelectionChange then
|
|
props.onSelectionChange(props.nodeId, "push")
|
|
end
|
|
end,
|
|
onSubtreeClick = function()
|
|
if props.onSubtreeSelectionChange then
|
|
props.onSubtreeSelectionChange(props.nodeId, "push")
|
|
end
|
|
end,
|
|
}),
|
|
})
|
|
end
|
|
|
|
local Expansion = Roact.Component:extend("Expansion")
|
|
|
|
function Expansion:render()
|
|
local props = self.props
|
|
|
|
if not props.rendered then
|
|
return nil
|
|
end
|
|
|
|
return e("Frame", {
|
|
BackgroundTransparency = 1,
|
|
Size = UDim2.new(1, -props.indent, 1, -24),
|
|
Position = UDim2.new(0, props.indent, 0, 24),
|
|
}, {
|
|
ChangeList = e(ChangeList, {
|
|
changes = props.changeList,
|
|
transparency = props.transparency,
|
|
showStringDiff = if props.showStringDiff
|
|
then function(current: string, incoming: string)
|
|
props.showStringDiff(current, incoming, props.instancePath)
|
|
end
|
|
else nil,
|
|
showTableDiff = props.showTableDiff,
|
|
}),
|
|
})
|
|
end
|
|
|
|
local DomLabel = Roact.Component:extend("DomLabel")
|
|
|
|
function DomLabel:init()
|
|
local initHeight = self.props.elementHeight:getValue()
|
|
self.expanded = initHeight > 24
|
|
self.isMounted = true
|
|
|
|
self.motor = Flipper.SingleMotor.new(initHeight)
|
|
self.binding = bindingUtil.fromMotor(self.motor)
|
|
|
|
self:setState({
|
|
renderExpansion = self.expanded,
|
|
isHovered = false,
|
|
})
|
|
self.motorStepConnection = self.motor:onStep(function(value)
|
|
if not self.isMounted then
|
|
return
|
|
end
|
|
|
|
local renderExpansion = value > 24
|
|
|
|
self.props.setElementHeight(value)
|
|
if self.props.updateEvent then
|
|
self.props.updateEvent:Fire()
|
|
end
|
|
|
|
self:setState(function(state)
|
|
if state.renderExpansion == renderExpansion then
|
|
return nil
|
|
end
|
|
|
|
return {
|
|
renderExpansion = renderExpansion,
|
|
}
|
|
end)
|
|
end)
|
|
end
|
|
|
|
function DomLabel:willUnmount()
|
|
self.isMounted = false
|
|
-- Stop the motor to prevent onStep callbacks after unmount
|
|
self.motor:stop()
|
|
if self.motorStepConnection then
|
|
self.motorStepConnection:disconnect()
|
|
self.motorStepConnection = nil
|
|
end
|
|
end
|
|
|
|
function DomLabel:didUpdate(prevProps)
|
|
-- When parent re-renders it creates new bindings starting at 24, but our motor
|
|
-- may be at expanded height. Sync immediately to prevent VirtualScroller from
|
|
-- positioning elements incorrectly.
|
|
if prevProps.setElementHeight ~= self.props.setElementHeight then
|
|
local currentHeight = self.binding:getValue()
|
|
self.props.setElementHeight(currentHeight)
|
|
end
|
|
|
|
if
|
|
prevProps.instance ~= self.props.instance
|
|
or prevProps.patchType ~= self.props.patchType
|
|
or prevProps.name ~= self.props.name
|
|
or prevProps.changeList ~= self.props.changeList
|
|
then
|
|
-- Close the expansion when the domlabel is changed to a different thing
|
|
self.expanded = false
|
|
self.motor:setGoal(Flipper.Instant.new(24))
|
|
end
|
|
end
|
|
|
|
function DomLabel:render()
|
|
local props = self.props
|
|
local depth = props.depth or 1
|
|
|
|
-- Derive subtree highlight state from binding (no re-renders needed)
|
|
local ancestorIds = props.ancestorIds
|
|
local isSubtreeHighlighted = if props.subtreeHighlightNodeId
|
|
then props.subtreeHighlightNodeId:map(function(highlightId)
|
|
if highlightId == nil then
|
|
return false
|
|
end
|
|
return ancestorIds[highlightId] == true
|
|
end)
|
|
else nil
|
|
|
|
return Theme.with(function(theme)
|
|
local color = if props.isWarning
|
|
then theme.Diff.Warning
|
|
elseif props.patchType then theme.Diff.Background[props.patchType]
|
|
else theme.TextColor
|
|
|
|
local indent = (depth - 1) * 12 + 15
|
|
|
|
-- Line guides help indent depth remain readable
|
|
local lineGuides = {}
|
|
for i = 2, depth do
|
|
if props.depthsComplete[i] then
|
|
continue
|
|
end
|
|
if props.isFinalChild and i == depth then
|
|
-- This line stops halfway down to merge with our connector for the right angle
|
|
lineGuides["Line_" .. i] = e("Frame", {
|
|
Size = UDim2.new(0, 2, 0, 15),
|
|
Position = UDim2.new(0, (12 * (i - 1)) + 6, 0, -1),
|
|
BorderSizePixel = 0,
|
|
BackgroundTransparency = props.transparency,
|
|
BackgroundColor3 = theme.BorderedContainer.BorderColor,
|
|
})
|
|
else
|
|
-- All other lines go all the way
|
|
-- with the exception of the final element, which stops halfway down
|
|
lineGuides["Line_" .. i] = e("Frame", {
|
|
Size = UDim2.new(0, 2, 1, if props.isFinalElement then -9 else 2),
|
|
Position = UDim2.new(0, (12 * (i - 1)) + 6, 0, -1),
|
|
BorderSizePixel = 0,
|
|
BackgroundTransparency = props.transparency,
|
|
BackgroundColor3 = theme.BorderedContainer.BorderColor,
|
|
})
|
|
end
|
|
end
|
|
|
|
if depth > 1 then
|
|
lineGuides["Connector"] = e("Frame", {
|
|
Size = UDim2.new(0, 8, 0, 2),
|
|
Position = UDim2.new(0, 2 + (12 * props.depth), 0, 12),
|
|
AnchorPoint = Vector2.xAxis,
|
|
BorderSizePixel = 0,
|
|
BackgroundTransparency = props.transparency,
|
|
BackgroundColor3 = theme.BorderedContainer.BorderColor,
|
|
})
|
|
end
|
|
|
|
return e("Frame", {
|
|
ClipsDescendants = true,
|
|
BackgroundTransparency = if props.elementIndex % 2 == 0 then 0.985 else 1,
|
|
BackgroundColor3 = theme.Diff.Row,
|
|
Size = self.binding:map(function(expand)
|
|
return UDim2.new(1, 0, 0, expand)
|
|
end),
|
|
}, {
|
|
Padding = e("UIPadding", {
|
|
PaddingLeft = UDim.new(0, 10),
|
|
PaddingRight = UDim.new(0, 10),
|
|
}),
|
|
Button = e("Frame", {
|
|
Active = true, -- Required for InputBegan to work on Frames
|
|
BackgroundTransparency = 1,
|
|
Size = UDim2.new(1, 0, 1, 0),
|
|
[Roact.Event.MouseEnter] = function()
|
|
self:setState({ isHovered = true })
|
|
end,
|
|
[Roact.Event.MouseLeave] = function()
|
|
self:setState({ isHovered = false })
|
|
end,
|
|
[Roact.Event.InputBegan] = function(_rbx: Instance, input: InputObject)
|
|
if input.UserInputType ~= Enum.UserInputType.MouseButton1 then
|
|
return
|
|
end
|
|
|
|
-- Check for double click
|
|
local now = os.clock()
|
|
local lastClickTime = self.lastClickTime or 0
|
|
self.lastClickTime = now
|
|
|
|
if now - lastClickTime < 0.3 then
|
|
-- Double click opens the instance in explorer
|
|
self.lastDoubleClickTime = now
|
|
if props.instance then
|
|
SelectionService:Set({ props.instance })
|
|
end
|
|
else
|
|
-- Removed script: show diff of old source vs empty
|
|
if
|
|
props.patchType == "Remove"
|
|
and props.instance
|
|
and props.changeInfo
|
|
and props.changeInfo.linesRemoved
|
|
and props.showStringDiff
|
|
then
|
|
local ok, source = pcall(function()
|
|
return (props.instance :: any).Source
|
|
end)
|
|
if ok and type(source) == "string" then
|
|
props.showStringDiff(source, "", props.instancePath)
|
|
return
|
|
end
|
|
end
|
|
|
|
-- Added script: find Source in changeList and show diff of empty vs new source
|
|
if
|
|
props.patchType == "Add"
|
|
and props.changeList
|
|
and props.changeInfo
|
|
and props.changeInfo.linesAdded
|
|
and props.showStringDiff
|
|
then
|
|
for _, entry in props.changeList do
|
|
if entry[1] == "Source" and type(entry[3]) == "string" then
|
|
props.showStringDiff("", tostring(entry[3]), props.instancePath)
|
|
return
|
|
end
|
|
end
|
|
end
|
|
|
|
if props.changeList then
|
|
local cl = props.changeList
|
|
if #cl == 2 and cl[2][1] == "Source" and props.showStringDiff then
|
|
props.showStringDiff(tostring(cl[2][2]), tostring(cl[2][3]), props.instancePath)
|
|
else
|
|
self.expanded = not self.expanded
|
|
local goalHeight = 24
|
|
+ (if self.expanded then math.clamp(#props.changeList * 24, 24, 24 * 6) else 0)
|
|
self.motor:setGoal(Flipper.Instant.new(goalHeight))
|
|
end
|
|
end
|
|
end
|
|
end,
|
|
}),
|
|
Expansion = if props.changeList
|
|
then e(Expansion, {
|
|
rendered = self.state.renderExpansion,
|
|
indent = indent,
|
|
transparency = props.transparency,
|
|
changeList = props.changeList,
|
|
showStringDiff = props.showStringDiff,
|
|
showTableDiff = props.showTableDiff,
|
|
})
|
|
else nil,
|
|
DiffIcon = if props.patchType
|
|
then e("ImageLabel", {
|
|
Image = Assets.Images.Diff[props.patchType],
|
|
ImageColor3 = color,
|
|
ImageTransparency = props.transparency,
|
|
BackgroundTransparency = 1,
|
|
Size = UDim2.new(0, 14, 0, 14),
|
|
Position = UDim2.new(0, 0, 0, 12),
|
|
AnchorPoint = Vector2.new(0, 0.5),
|
|
})
|
|
else nil,
|
|
ClassIcon = e(ClassIcon, {
|
|
className = props.className,
|
|
color = color,
|
|
transparency = props.transparency,
|
|
size = UDim2.new(0, 16, 0, 16),
|
|
position = UDim2.new(0, indent + 2, 0, 12),
|
|
anchorPoint = Vector2.new(0, 0.5),
|
|
}),
|
|
InstanceName = e("TextLabel", {
|
|
Text = (if props.isWarning then "⚠ " else "") .. props.name,
|
|
RichText = true,
|
|
BackgroundTransparency = 1,
|
|
FontFace = if props.patchType then theme.Font.Bold else theme.Font.Main,
|
|
TextSize = theme.TextSize.Body,
|
|
TextColor3 = color,
|
|
TextXAlignment = Enum.TextXAlignment.Left,
|
|
TextTransparency = props.transparency,
|
|
TextTruncate = Enum.TextTruncate.AtEnd,
|
|
Size = UDim2.new(1, -indent - 50, 0, 24),
|
|
Position = UDim2.new(0, indent + 22, 0, 0),
|
|
}),
|
|
ChangeInfo = e("Frame", {
|
|
BackgroundTransparency = 1,
|
|
Size = UDim2.new(1, -indent - (if props.patchType and props.onSelectionChange then 210 else 80), 0, 24),
|
|
Position = UDim2.new(1, if props.patchType and props.onSelectionChange then -130 else -2, 0, 0),
|
|
AnchorPoint = Vector2.new(1, 0),
|
|
ClipsDescendants = true,
|
|
}, {
|
|
Layout = e("UIListLayout", {
|
|
FillDirection = Enum.FillDirection.Horizontal,
|
|
HorizontalAlignment = Enum.HorizontalAlignment.Right,
|
|
VerticalAlignment = Enum.VerticalAlignment.Center,
|
|
SortOrder = Enum.SortOrder.LayoutOrder,
|
|
Padding = UDim.new(0, 4),
|
|
}),
|
|
|
|
-- Git-style line change tags: -M (red) then +N (green)
|
|
-- Always show both when any line change data exists (even -0 or +0).
|
|
-- Grayed when whitespace-only. Always exact counts.
|
|
RemovedTag = if props.changeInfo
|
|
and (props.changeInfo.linesRemoved ~= nil or props.changeInfo.linesAdded ~= nil)
|
|
then e(ChangeTag, {
|
|
text = "-" .. (props.changeInfo.linesRemoved or 0),
|
|
color = if props.changeInfo.isWhitespaceOnly
|
|
or (props.changeInfo.linesRemoved or 0) == 0
|
|
then theme.Diff.WhitespaceOnly
|
|
else theme.Diff.Remove,
|
|
transparency = props.transparency,
|
|
layoutOrder = 3,
|
|
tooltipText = (props.changeInfo.linesRemoved or 0)
|
|
.. (if (props.changeInfo.linesRemoved or 0) == 1 then " line removed" else " lines removed")
|
|
.. (if props.changeInfo.isWhitespaceOnly then " (whitespace only)" else ""),
|
|
})
|
|
else nil,
|
|
|
|
AddedTag = if props.changeInfo
|
|
and (props.changeInfo.linesRemoved ~= nil or props.changeInfo.linesAdded ~= nil)
|
|
then e(ChangeTag, {
|
|
text = "+" .. (props.changeInfo.linesAdded or 0),
|
|
color = if props.changeInfo.isWhitespaceOnly
|
|
or (props.changeInfo.linesAdded or 0) == 0
|
|
then theme.Diff.WhitespaceOnly
|
|
else theme.Diff.Add,
|
|
transparency = props.transparency,
|
|
layoutOrder = 2,
|
|
tooltipText = (props.changeInfo.linesAdded or 0)
|
|
.. (if (props.changeInfo.linesAdded or 0) == 1 then " line added" else " lines added")
|
|
.. (if props.changeInfo.isWhitespaceOnly then " (whitespace only)" else ""),
|
|
})
|
|
else nil,
|
|
|
|
-- Property changes: compact "NP" format in yellow
|
|
PropsTag = if props.changeInfo and props.changeInfo.propChanges
|
|
then e(ChangeTag, {
|
|
text = props.changeInfo.propChanges .. "P",
|
|
color = theme.Diff.Property,
|
|
transparency = props.transparency,
|
|
layoutOrder = 1,
|
|
tooltipText = props.changeInfo.propChanges .. (if props.changeInfo.propChanges == 1
|
|
then " property change"
|
|
else " property changes") .. " (excluding Source)",
|
|
})
|
|
else nil,
|
|
|
|
-- Proportional diff bar
|
|
Bar = if props.changeInfo
|
|
and (
|
|
(props.changeInfo.linesRemoved and props.changeInfo.linesRemoved > 0)
|
|
or (props.changeInfo.linesAdded and props.changeInfo.linesAdded > 0)
|
|
or (props.changeInfo.propChanges and props.changeInfo.propChanges > 0)
|
|
)
|
|
then e(DiffBar, {
|
|
removed = props.changeInfo.linesRemoved or 0,
|
|
added = props.changeInfo.linesAdded or 0,
|
|
propChanges = props.changeInfo.propChanges or 0,
|
|
isWhitespaceOnly = props.changeInfo.isWhitespaceOnly,
|
|
transparency = props.transparency,
|
|
layoutOrder = 4,
|
|
})
|
|
else nil,
|
|
|
|
-- Failed changes
|
|
Failed = if props.changeInfo and props.changeInfo.failed
|
|
then e(ChangeTag, {
|
|
text = "Failed " .. props.changeInfo.failed,
|
|
color = theme.Diff.Warning,
|
|
transparency = props.transparency,
|
|
layoutOrder = 5,
|
|
tooltipText = props.changeInfo.failed .. (if props.changeInfo.failed == 1
|
|
then " change failed to apply"
|
|
else " changes failed to apply"),
|
|
})
|
|
else nil,
|
|
}),
|
|
SubtreeHighlight = if isSubtreeHighlighted
|
|
then e("Frame", {
|
|
Size = UDim2.fromScale(1, 1),
|
|
BackgroundColor3 = theme.Diff.SubtreeHighlight,
|
|
BackgroundTransparency = isSubtreeHighlighted:map(function(highlighted)
|
|
return if highlighted then 0.92 else 1
|
|
end),
|
|
BorderSizePixel = 0,
|
|
ZIndex = 0,
|
|
})
|
|
else nil,
|
|
SelectionRadio = e(SelectionRadio, {
|
|
-- Visible for nodes with patchType, or for parent-only nodes on hover
|
|
visible = (props.patchType ~= nil and props.onSelectionChange ~= nil)
|
|
or (
|
|
props.patchType == nil
|
|
and props.hasChildren
|
|
and self.state.isHovered
|
|
and props.onSubtreeSelectionChange ~= nil
|
|
),
|
|
nodeId = props.nodeId,
|
|
selection = props.selection,
|
|
transparency = props.transparency,
|
|
hasChildren = props.hasChildren,
|
|
-- For parent-only nodes (no patchType), only subtree selection makes sense
|
|
isParentOnly = props.patchType == nil,
|
|
onSelectionChange = props.onSelectionChange,
|
|
onSubtreeSelectionChange = props.onSubtreeSelectionChange,
|
|
onSubtreeHoverStart = function()
|
|
if props.setSubtreeHighlightNodeId then
|
|
props.setSubtreeHighlightNodeId(props.nodeId)
|
|
end
|
|
end,
|
|
onSubtreeHoverEnd = function()
|
|
if props.setSubtreeHighlightNodeId then
|
|
props.setSubtreeHighlightNodeId(nil)
|
|
end
|
|
end,
|
|
}),
|
|
LineGuides = e("Folder", nil, lineGuides),
|
|
})
|
|
end)
|
|
end
|
|
|
|
return DomLabel
|