mirror of
https://github.com/Ukendio/jecs.git
synced 2026-03-18 00:44:32 +00:00
Add Jabby module
This commit is contained in:
parent
22dd91b111
commit
aeedea2fcb
123 changed files with 18769 additions and 0 deletions
106
modules/Jabby/client/apps/entity/add_component.luau
Normal file
106
modules/Jabby/client/apps/entity/add_component.luau
Normal file
|
|
@ -0,0 +1,106 @@
|
|||
local ui = require(script.Parent.Parent.Parent.Parent.ui)
|
||||
local vide = require(script.Parent.Parent.Parent.Parent.vide)
|
||||
local query_parser = require(script.Parent.Parent.Parent.Parent.server.query_parser)
|
||||
|
||||
local create = vide.create
|
||||
local source = vide.source
|
||||
local show = vide.show
|
||||
|
||||
type props = {
|
||||
changes: vide.Source<{[string]: string}>,
|
||||
editing: vide.Source<false | string>,
|
||||
adding: vide.Source<false | string>,
|
||||
text: vide.Source<string>
|
||||
}
|
||||
|
||||
return function(props: props)
|
||||
|
||||
local component_edit_text = source("")
|
||||
local adding = props.adding
|
||||
local editing = props.editing
|
||||
local text = props.text
|
||||
|
||||
return create "Folder" {
|
||||
Name = "Add Component",
|
||||
|
||||
show(adding, function()
|
||||
return create "Frame" {
|
||||
ZIndex = 1000,
|
||||
Size = UDim2.new(1,16, 1, 16),
|
||||
Position = UDim2.fromScale(0.5, 0.5),
|
||||
AnchorPoint = Vector2.new(0.5, 0.5),
|
||||
|
||||
BackgroundColor3 = Color3.new(0, 0, 0),
|
||||
BackgroundTransparency = 0.5,
|
||||
|
||||
Active = true,
|
||||
|
||||
create "UIListLayout" {
|
||||
HorizontalAlignment = Enum.HorizontalAlignment.Center,
|
||||
VerticalAlignment = Enum.VerticalAlignment.Center,
|
||||
Padding = UDim.new(0, 8)
|
||||
},
|
||||
|
||||
ui.padding {
|
||||
padding = UDim.new(0, 32)
|
||||
},
|
||||
|
||||
ui.textfield {
|
||||
size = UDim2.fromOffset(200, 30),
|
||||
|
||||
placeholder = "Entity",
|
||||
|
||||
oninput = component_edit_text,
|
||||
},
|
||||
|
||||
create "Frame" {
|
||||
Size = UDim2.new(1, 0, 0, 30),
|
||||
BackgroundTransparency = 1,
|
||||
AutomaticSize = Enum.AutomaticSize.Y,
|
||||
|
||||
create "UIListLayout" {
|
||||
HorizontalFlex = Enum.UIFlexAlignment.Fill,
|
||||
FillDirection = Enum.FillDirection.Horizontal,
|
||||
Padding = UDim.new(0, 8)
|
||||
},
|
||||
|
||||
ui.button {
|
||||
size = UDim2.fromOffset(150, 30),
|
||||
text = "Edit",
|
||||
activated = function()
|
||||
adding(false)
|
||||
editing(component_edit_text())
|
||||
end,
|
||||
disabled = function()
|
||||
local ok, node = pcall(query_parser, component_edit_text())
|
||||
|
||||
if not ok then return true end
|
||||
if not node[1] then return true end
|
||||
if node[2] then return true end
|
||||
local n = node[1]
|
||||
if n.type == "Relationship" then
|
||||
if n.left.type == "Wildcard" then return true end
|
||||
if n.right.type == "Wildcard" then return true end
|
||||
end
|
||||
|
||||
return false
|
||||
end,
|
||||
accent = true
|
||||
},
|
||||
|
||||
ui.button {
|
||||
size = UDim2.fromOffset(150, 30),
|
||||
text = "Cancel",
|
||||
activated = function()
|
||||
adding(false)
|
||||
end
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
end)
|
||||
|
||||
}
|
||||
|
||||
end
|
||||
118
modules/Jabby/client/apps/entity/editor.luau
Normal file
118
modules/Jabby/client/apps/entity/editor.luau
Normal file
|
|
@ -0,0 +1,118 @@
|
|||
local ui = require(script.Parent.Parent.Parent.Parent.ui)
|
||||
local vide = require(script.Parent.Parent.Parent.Parent.vide)
|
||||
|
||||
local create = vide.create
|
||||
local show = vide.show
|
||||
|
||||
type props = {
|
||||
components: () -> {[string]: string},
|
||||
changes: vide.Source<{[string]: string}>,
|
||||
editing: vide.Source<false | string>,
|
||||
text: vide.Source<string>
|
||||
}
|
||||
|
||||
return function(props: props)
|
||||
|
||||
local editing = props.editing
|
||||
local text = props.text
|
||||
local changes = props.changes
|
||||
|
||||
return create "Folder" {
|
||||
Name = "Text Editor",
|
||||
|
||||
show(function()
|
||||
return editing()
|
||||
end, function()
|
||||
return create "Frame" {
|
||||
ZIndex = 1000,
|
||||
Size = UDim2.new(1,16, 1, 16),
|
||||
Position = UDim2.fromScale(0.5, 0.5),
|
||||
AnchorPoint = Vector2.new(0.5, 0.5),
|
||||
|
||||
BackgroundColor3 = Color3.new(0, 0, 0),
|
||||
BackgroundTransparency = 0.5,
|
||||
|
||||
Active = true,
|
||||
|
||||
create "UIListLayout" {
|
||||
HorizontalAlignment = Enum.HorizontalAlignment.Center,
|
||||
VerticalAlignment = Enum.VerticalAlignment.Center,
|
||||
VerticalFlex = Enum.UIFlexAlignment.SpaceEvenly,
|
||||
Padding = UDim.new(0, 8)
|
||||
},
|
||||
|
||||
ui.padding {
|
||||
padding = UDim.new(0, 32)
|
||||
},
|
||||
|
||||
ui.typography {
|
||||
text = function()
|
||||
return `Editing {editing()}`
|
||||
end
|
||||
},
|
||||
|
||||
create "Frame" {
|
||||
Size = UDim2.fromScale(1, 0),
|
||||
|
||||
create "UIFlexItem" {
|
||||
FlexMode = Enum.UIFlexMode.Fill
|
||||
},
|
||||
|
||||
BackgroundTransparency = 1,
|
||||
|
||||
ui.textfield {
|
||||
size = UDim2.fromScale(1, 1),
|
||||
position = UDim2.fromScale(0.5, 0.5),
|
||||
anchorpoint = Vector2.new(0.5, 0.5),
|
||||
|
||||
multiline = true,
|
||||
code = true,
|
||||
|
||||
text = text,
|
||||
oninput = text,
|
||||
},
|
||||
},
|
||||
|
||||
create "Frame" {
|
||||
Size = UDim2.new(1, 0, 0, 30),
|
||||
BackgroundTransparency = 1,
|
||||
AutomaticSize = Enum.AutomaticSize.Y,
|
||||
|
||||
create "UIListLayout" {
|
||||
HorizontalFlex = Enum.UIFlexAlignment.Fill,
|
||||
FillDirection = Enum.FillDirection.Horizontal,
|
||||
Padding = UDim.new(0, 8)
|
||||
},
|
||||
|
||||
ui.button {
|
||||
size = UDim2.fromOffset(150, 30),
|
||||
text = "Save Changes",
|
||||
activated = function()
|
||||
local key = editing()
|
||||
changes()[key] = text()
|
||||
|
||||
editing(false)
|
||||
changes(changes())
|
||||
|
||||
if props.components()[key] ~= nil then return end
|
||||
props.components()[key] = text()
|
||||
end,
|
||||
accent = true
|
||||
},
|
||||
|
||||
ui.button {
|
||||
size = UDim2.fromOffset(150, 30),
|
||||
text = "Cancel Changes",
|
||||
activated = function()
|
||||
editing(false)
|
||||
end
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
end)
|
||||
|
||||
}
|
||||
|
||||
end
|
||||
81
modules/Jabby/client/apps/entity/init.luau
Normal file
81
modules/Jabby/client/apps/entity/init.luau
Normal file
|
|
@ -0,0 +1,81 @@
|
|||
local RunService = game:GetService("RunService")
|
||||
|
||||
local vide = require(script.Parent.Parent.Parent.vide)
|
||||
local loop = require(script.Parent.Parent.Parent.modules.loop)
|
||||
local widget = require(script.widget)
|
||||
|
||||
local source = vide.source
|
||||
local cleanup = vide.cleanup
|
||||
|
||||
local overview_entity = {
|
||||
class_name = "app" :: "app",
|
||||
name = "Entity"
|
||||
}
|
||||
|
||||
type props = {
|
||||
host: Player | "server",
|
||||
vm: number,
|
||||
id: number,
|
||||
entity: number,
|
||||
}
|
||||
|
||||
local function generate_random_query_id()
|
||||
return math.random(2 ^ 31 - 1)
|
||||
end
|
||||
|
||||
function overview_entity.mount(props: props, destroy: () -> ())
|
||||
|
||||
local keys = source({})
|
||||
local changes = source({})
|
||||
local enable_live_updates = source(true)
|
||||
local apply_changes = source(false)
|
||||
local deleting = source(false)
|
||||
local inspect_id = generate_random_query_id()
|
||||
|
||||
-- check if the query and columns are properly
|
||||
local app_loop = loop (
|
||||
"app-client-entity",
|
||||
{
|
||||
host = props.host,
|
||||
vm = props.vm,
|
||||
id = props.id,
|
||||
inspect_id = inspect_id,
|
||||
entity = tonumber(props.entity),
|
||||
|
||||
keys = keys,
|
||||
live_updates = enable_live_updates,
|
||||
changes = changes,
|
||||
apply_changes = apply_changes,
|
||||
deleting = deleting
|
||||
},
|
||||
|
||||
{i = 1},
|
||||
script.systems.obtain_entity_data
|
||||
)
|
||||
|
||||
cleanup(
|
||||
RunService.Heartbeat:Connect(app_loop)
|
||||
)
|
||||
|
||||
return widget {
|
||||
host = props.host,
|
||||
vm = props.vm,
|
||||
id = props.id,
|
||||
inspect_id = inspect_id,
|
||||
entity = props.entity,
|
||||
|
||||
components = keys,
|
||||
live_updates = enable_live_updates,
|
||||
changes = changes,
|
||||
apply_changes = apply_changes,
|
||||
|
||||
delete = function()
|
||||
deleting(true)
|
||||
end,
|
||||
|
||||
destroy = destroy
|
||||
}
|
||||
|
||||
end
|
||||
|
||||
return overview_entity
|
||||
|
|
@ -0,0 +1,85 @@
|
|||
local vide = require(script.Parent.Parent.Parent.Parent.Parent.vide)
|
||||
local queue = require(script.Parent.Parent.Parent.Parent.Parent.modules.queue)
|
||||
local remotes = require(script.Parent.Parent.Parent.Parent.Parent.modules.remotes)
|
||||
|
||||
local effect = vide.effect
|
||||
local cleanup = vide.cleanup
|
||||
local batch = vide.batch
|
||||
|
||||
type Context = {
|
||||
host: Player | "server",
|
||||
vm: number,
|
||||
id: number,
|
||||
inspect_id: number,
|
||||
entity: number,
|
||||
|
||||
keys: vide.Source<{[string]: string}>,
|
||||
changes: vide.Source<{[string]: string}>,
|
||||
apply_changes: vide.Source<boolean>,
|
||||
live_updates: () -> boolean,
|
||||
deleting: () -> boolean,
|
||||
|
||||
}
|
||||
|
||||
return function(context: Context)
|
||||
|
||||
local inspect_entity_update = queue(remotes.inspect_entity_update)
|
||||
|
||||
local current_inspectid = context.inspect_id
|
||||
local outgoing = {
|
||||
host = context.host,
|
||||
to_vm = context.vm,
|
||||
}
|
||||
|
||||
remotes.inspect_entity:fire(
|
||||
outgoing,
|
||||
context.id,
|
||||
context.entity,
|
||||
current_inspectid
|
||||
)
|
||||
|
||||
local settings_changed = false
|
||||
|
||||
effect(function()
|
||||
context.live_updates()
|
||||
settings_changed = true
|
||||
end)
|
||||
|
||||
cleanup(function()
|
||||
remotes.stop_inspect_entity:fire(
|
||||
outgoing,
|
||||
current_inspectid
|
||||
)
|
||||
end)
|
||||
|
||||
return function()
|
||||
|
||||
if context.apply_changes() then
|
||||
remotes.update_entity:fire(outgoing, current_inspectid, context.changes())
|
||||
context.apply_changes(false)
|
||||
context.changes({})
|
||||
end
|
||||
|
||||
if context.deleting() then
|
||||
remotes.delete_entity:fire(outgoing, current_inspectid)
|
||||
end
|
||||
|
||||
if settings_changed then
|
||||
remotes.update_inspect_settings:fire(
|
||||
outgoing,
|
||||
current_inspectid,
|
||||
{paused = not context.live_updates()}
|
||||
)
|
||||
settings_changed = false
|
||||
end
|
||||
|
||||
batch(function()
|
||||
for incoming, inspectid, key, value in inspect_entity_update:iter() do
|
||||
if inspectid ~= current_inspectid then continue end
|
||||
context.keys()[key] = value
|
||||
context.keys(context.keys())
|
||||
end
|
||||
end)
|
||||
|
||||
end
|
||||
end
|
||||
414
modules/Jabby/client/apps/entity/widget.luau
Normal file
414
modules/Jabby/client/apps/entity/widget.luau
Normal file
|
|
@ -0,0 +1,414 @@
|
|||
local RunService = game:GetService("RunService")
|
||||
local UserInputService = game:GetService("UserInputService")
|
||||
|
||||
local ui = require(script.Parent.Parent.Parent.Parent.ui)
|
||||
local vide = require(script.Parent.Parent.Parent.Parent.vide)
|
||||
local remotes = require(script.Parent.Parent.Parent.Parent.modules.remotes)
|
||||
local add_component = require(script.Parent.add_component)
|
||||
local editor = require(script.Parent.editor)
|
||||
|
||||
local create = vide.create
|
||||
local indexes = vide.indexes
|
||||
local source = vide.source
|
||||
local show = vide.show
|
||||
|
||||
type SystemId = number
|
||||
|
||||
type props = {
|
||||
host: Player | "server",
|
||||
vm: number,
|
||||
id: number,
|
||||
entity: number,
|
||||
inspect_id: number,
|
||||
|
||||
components: vide.Source<{[string]: string}>,
|
||||
changes: vide.Source<{[string]: string}>,
|
||||
live_updates: vide.Source<boolean>,
|
||||
apply_changes: vide.Source<boolean>,
|
||||
|
||||
destroy: () -> (),
|
||||
delete: () -> ()
|
||||
}
|
||||
|
||||
local mouse_location = source(Vector2.zero)
|
||||
|
||||
RunService.PreRender:Connect(function()
|
||||
mouse_location(UserInputService:GetMouseLocation())
|
||||
end)
|
||||
|
||||
return function(props: props)
|
||||
local outgoing = {
|
||||
host = props.host,
|
||||
to_vm = props.vm,
|
||||
}
|
||||
|
||||
local live_updates = props.live_updates
|
||||
local changes = props.changes
|
||||
|
||||
local text = source("")
|
||||
local adding = source(false)
|
||||
local editing = source(false :: false | string)
|
||||
|
||||
local function components()
|
||||
local components = {}
|
||||
|
||||
for key, value in props.components() do
|
||||
if value == "tag" then continue end
|
||||
components[key] = value
|
||||
end
|
||||
|
||||
return components
|
||||
end
|
||||
|
||||
local function tags()
|
||||
local tags = {}
|
||||
|
||||
for key, value in props.components() do
|
||||
if value ~= "tag" then continue end
|
||||
tags[key] = value
|
||||
end
|
||||
|
||||
return tags
|
||||
end
|
||||
|
||||
local function is_removed(value: string)
|
||||
return value == "nil"
|
||||
end
|
||||
|
||||
local function edit_component(component: string)
|
||||
remotes.get_component:fire(outgoing, props.inspect_id, component)
|
||||
editing(component)
|
||||
|
||||
text("waiting...")
|
||||
end
|
||||
|
||||
vide.cleanup(remotes.return_component:connect(function(from, inspect, component, value)
|
||||
if props.host ~= from.host then return end
|
||||
if props.inspect_id ~= inspect then return end
|
||||
|
||||
local current_value = props.changes()[component]
|
||||
|
||||
if current_value then
|
||||
text(current_value)
|
||||
else
|
||||
text(value)
|
||||
end
|
||||
|
||||
end))
|
||||
|
||||
return ui.widget {
|
||||
title = `Entity #{props.entity}`,
|
||||
subtitle = `host: {props.host} vm: {props.vm} id: {props.id}`,
|
||||
|
||||
min_size = Vector2.new(300, 300),
|
||||
|
||||
bind_to_close = props.destroy,
|
||||
|
||||
create "Frame" {
|
||||
Size = UDim2.fromScale(1, 1),
|
||||
BackgroundTransparency = 1,
|
||||
|
||||
create "UIListLayout" {
|
||||
VerticalFlex = Enum.UIFlexAlignment.SpaceEvenly,
|
||||
Padding = UDim.new(0, 8)
|
||||
},
|
||||
|
||||
editor {
|
||||
components = props.components,
|
||||
editing = editing,
|
||||
text = text,
|
||||
changes = changes
|
||||
},
|
||||
|
||||
add_component {
|
||||
editing = edit_component,
|
||||
text = text,
|
||||
changes = changes,
|
||||
adding = adding
|
||||
},
|
||||
|
||||
ui.row {
|
||||
|
||||
justifycontent = Enum.UIFlexAlignment.Fill,
|
||||
|
||||
ui.button {
|
||||
text = "Live Updates",
|
||||
activated = function()
|
||||
live_updates(not live_updates())
|
||||
end,
|
||||
|
||||
create "UIListLayout" {
|
||||
FillDirection = Enum.FillDirection.Horizontal,
|
||||
VerticalAlignment = Enum.VerticalAlignment.Center,
|
||||
HorizontalFlex = Enum.UIFlexAlignment.SpaceBetween,
|
||||
Padding = UDim.new(0, 4)
|
||||
},
|
||||
|
||||
ui.checkbox {
|
||||
size = UDim2.fromOffset(16, 16),
|
||||
layoutorder = -1,
|
||||
checked = live_updates,
|
||||
|
||||
create "UIFlexItem" {
|
||||
FlexMode = Enum.UIFlexMode.None,
|
||||
}
|
||||
}
|
||||
|
||||
},
|
||||
|
||||
ui.button {
|
||||
size = UDim2.fromOffset(130, 30),
|
||||
text = function()
|
||||
local total = 0
|
||||
|
||||
for _, change in changes() do
|
||||
total += 1
|
||||
end
|
||||
|
||||
return `Apply {total} Edits`
|
||||
end,
|
||||
disabled = function()
|
||||
return next(changes()) == nil
|
||||
end,
|
||||
activated = function()
|
||||
props.apply_changes(true)
|
||||
end
|
||||
},
|
||||
|
||||
ui.button {
|
||||
create "UIFlexItem" {
|
||||
ItemLineAlignment = Enum.ItemLineAlignment.End,
|
||||
},
|
||||
size = UDim2.fromOffset(130, 30),
|
||||
|
||||
text = "Cancel changes",
|
||||
disabled = function()
|
||||
return next(changes()) == nil
|
||||
end,
|
||||
activated = function()
|
||||
changes({})
|
||||
end
|
||||
}
|
||||
|
||||
},
|
||||
|
||||
create "ScrollingFrame" {
|
||||
Size = UDim2.fromScale(1, 0),
|
||||
CanvasSize = UDim2.new(),
|
||||
AutomaticCanvasSize = Enum.AutomaticSize.Y,
|
||||
|
||||
BackgroundColor3 = ui.theme.bg[-1],
|
||||
|
||||
ScrollBarThickness = 6,
|
||||
VerticalScrollBarInset = Enum.ScrollBarInset.Always,
|
||||
|
||||
create "UIFlexItem" {
|
||||
FlexMode = Enum.UIFlexMode.Fill
|
||||
},
|
||||
|
||||
create "UIListLayout" {
|
||||
Padding = UDim.new(0, 4)
|
||||
},
|
||||
|
||||
ui.typography {text = "Components"},
|
||||
|
||||
ui.container {
|
||||
Size = UDim2.fromScale(1, 0),
|
||||
AutomaticSize = Enum.AutomaticSize.Y,
|
||||
|
||||
create "UIListLayout" {
|
||||
SortOrder = Enum.SortOrder.Name,
|
||||
},
|
||||
|
||||
indexes(components, function(value, key)
|
||||
return ui.button {
|
||||
{ Name = if not string.match(key, "^%a") then "zzzz" .. key else key },
|
||||
size = UDim2.new(1, 0, 0, 32),
|
||||
automaticsize = Enum.AutomaticSize.Y,
|
||||
|
||||
text = "",
|
||||
|
||||
corner = false,
|
||||
|
||||
activated = function()
|
||||
edit_component(key)
|
||||
end,
|
||||
|
||||
create "UIListLayout" {
|
||||
FillDirection = Enum.FillDirection.Horizontal,
|
||||
HorizontalFlex = Enum.UIFlexAlignment.SpaceEvenly,
|
||||
VerticalAlignment = Enum.VerticalAlignment.Center,
|
||||
Padding = UDim.new(0, 8)
|
||||
},
|
||||
|
||||
ui.padding {
|
||||
y = UDim.new(0, 4)
|
||||
},
|
||||
|
||||
ui.typography {
|
||||
size = UDim2.new(0, 100, 0, 18),
|
||||
automaticsize = Enum.AutomaticSize.Y,
|
||||
text = key,
|
||||
code = true,
|
||||
wrapped = true,
|
||||
truncate = Enum.TextTruncate.SplitWord,
|
||||
xalignment = Enum.TextXAlignment.Left,
|
||||
},
|
||||
|
||||
ui.typography {
|
||||
size = UDim2.fromOffset(0, 18),
|
||||
automaticsize = Enum.AutomaticSize.Y,
|
||||
|
||||
yalignment = Enum.TextYAlignment.Top,
|
||||
xalignment = Enum.TextXAlignment.Left,
|
||||
text = value,
|
||||
wrapped = true,
|
||||
truncate = Enum.TextTruncate.AtEnd,
|
||||
code = true,
|
||||
|
||||
create "UIFlexItem" {
|
||||
FlexMode = Enum.UIFlexMode.Fill
|
||||
},
|
||||
|
||||
},
|
||||
|
||||
show(function()
|
||||
return props.changes()[key] ~= nil
|
||||
end, function()
|
||||
return ui.typography {
|
||||
text = function()
|
||||
local old = value()
|
||||
local change = props.changes()[key]
|
||||
|
||||
return if old == nil then "(added)"
|
||||
elseif is_removed(change) then "(removed)"
|
||||
else "(changed)"
|
||||
end,
|
||||
disabled = true,
|
||||
textsize = 14,
|
||||
}
|
||||
end),
|
||||
|
||||
create "UISizeConstraint" {
|
||||
MaxSize = Vector2.new(math.huge, 300)
|
||||
}
|
||||
}
|
||||
end),
|
||||
},
|
||||
|
||||
ui.typography {text = "Tags"},
|
||||
|
||||
ui.container {
|
||||
Size = UDim2.fromScale(1, 0),
|
||||
AutomaticSize = Enum.AutomaticSize.Y,
|
||||
|
||||
create "UIListLayout" {
|
||||
SortOrder = Enum.SortOrder.Name,
|
||||
},
|
||||
|
||||
indexes(tags, function(value, key)
|
||||
local function did_change()
|
||||
return changes()[key] ~= nil
|
||||
end
|
||||
|
||||
return ui.button {
|
||||
{ Name = if not string.match(key, "^%a") then "zzzz" .. key else key },
|
||||
size = UDim2.new(1, 0, 0, 24),
|
||||
|
||||
text = "",
|
||||
|
||||
corner = false,
|
||||
|
||||
activated = function()
|
||||
if changes()[key] == "tag" then
|
||||
changes()[key] = nil
|
||||
props.components()[key] = nil
|
||||
|
||||
-- notify about the change
|
||||
changes(changes())
|
||||
props.components(props.components())
|
||||
elseif changes()[key] then
|
||||
changes()[key] = nil
|
||||
|
||||
-- notify about the change
|
||||
changes(changes())
|
||||
else
|
||||
changes()[key] = "nil"
|
||||
|
||||
-- notify about the change
|
||||
changes(changes())
|
||||
end
|
||||
end,
|
||||
|
||||
create "UIListLayout" {
|
||||
FillDirection = Enum.FillDirection.Horizontal,
|
||||
HorizontalFlex = Enum.UIFlexAlignment.SpaceEvenly,
|
||||
VerticalAlignment = Enum.VerticalAlignment.Center,
|
||||
Padding = UDim.new(0, 8)
|
||||
},
|
||||
|
||||
ui.padding {
|
||||
y = UDim.new(0, 4)
|
||||
},
|
||||
|
||||
ui.typography {
|
||||
size = UDim2.fromScale(0, 1),
|
||||
text = key,
|
||||
code = true,
|
||||
wrapped = true,
|
||||
truncate = Enum.TextTruncate.SplitWord,
|
||||
xalignment = Enum.TextXAlignment.Left,
|
||||
|
||||
create "UIFlexItem" {
|
||||
FlexMode = Enum.UIFlexMode.Fill
|
||||
}
|
||||
},
|
||||
|
||||
show(did_change, function()
|
||||
return ui.typography {
|
||||
text = function()
|
||||
local old = value()
|
||||
local change = changes()[key]
|
||||
|
||||
return if old == nil then "(added)"
|
||||
elseif is_removed(change) then "(removed)"
|
||||
else "(changed)"
|
||||
end,
|
||||
disabled = true,
|
||||
textsize = 14,
|
||||
}
|
||||
end)
|
||||
}
|
||||
end),
|
||||
}
|
||||
|
||||
},
|
||||
|
||||
ui.row {
|
||||
|
||||
justifycontent = Enum.UIFlexAlignment.Fill,
|
||||
|
||||
ui.button {
|
||||
size = UDim2.fromOffset(100, 30),
|
||||
text = "Delete Id",
|
||||
|
||||
activated = props.delete
|
||||
},
|
||||
|
||||
ui.button {
|
||||
size = UDim2.fromOffset(200, 30),
|
||||
text = "Add Component",
|
||||
|
||||
activated = function()
|
||||
adding(true)
|
||||
end
|
||||
},
|
||||
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
end
|
||||
36
modules/Jabby/client/apps/home/init.luau
Normal file
36
modules/Jabby/client/apps/home/init.luau
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
local RunService = game:GetService("RunService")
|
||||
|
||||
local vide = require(script.Parent.Parent.Parent.vide)
|
||||
local loop = require(script.Parent.Parent.Parent.modules.loop)
|
||||
local widget = require(script.widget)
|
||||
|
||||
local cleanup = vide.cleanup
|
||||
|
||||
local home = {
|
||||
class_name = "app" :: "app",
|
||||
name = "Home"
|
||||
}
|
||||
|
||||
function home.mount(_: nil, destroy: () -> ())
|
||||
|
||||
local servers = vide.source {} :: any
|
||||
local app_loop = loop (
|
||||
"app-client-home",
|
||||
servers,
|
||||
|
||||
{i = 1},
|
||||
script.systems.get_core_data
|
||||
)
|
||||
|
||||
cleanup(
|
||||
RunService.Heartbeat:Connect(app_loop)
|
||||
)
|
||||
|
||||
return widget {
|
||||
servers = servers,
|
||||
destroy = destroy
|
||||
}
|
||||
|
||||
end
|
||||
|
||||
return home
|
||||
93
modules/Jabby/client/apps/home/systems/get_core_data.luau
Normal file
93
modules/Jabby/client/apps/home/systems/get_core_data.luau
Normal file
|
|
@ -0,0 +1,93 @@
|
|||
local Players = game:GetService("Players")
|
||||
|
||||
local vide = require(script.Parent.Parent.Parent.Parent.Parent.vide)
|
||||
local queue = require(script.Parent.Parent.Parent.Parent.Parent.modules.queue)
|
||||
local remotes = require(script.Parent.Parent.Parent.Parent.Parent.modules.remotes)
|
||||
local reverse_connector = require(script.Parent.Parent.Parent.Parent.Parent.modules.reverse_connector)
|
||||
|
||||
return function(data)
|
||||
|
||||
for _, player in Players:GetPlayers() do
|
||||
remotes.ping:fire({
|
||||
host = player,
|
||||
})
|
||||
end
|
||||
remotes.ping:fire({
|
||||
host = "server"
|
||||
})
|
||||
|
||||
local servers_responding = queue(remotes.new_server_registered)
|
||||
local server_update = queue(remotes.update_server_data)
|
||||
local player_removing = queue(Players.PlayerRemoving)
|
||||
|
||||
local n = 0
|
||||
local servers = data :: any
|
||||
local map_to_idx = {
|
||||
server = {}
|
||||
}
|
||||
|
||||
return function()
|
||||
|
||||
for connector in servers_responding:iter() do
|
||||
local outgoing = reverse_connector(connector)
|
||||
remotes.bind_to_server_core:fire(outgoing)
|
||||
end
|
||||
|
||||
for player in player_removing:iter() do
|
||||
local indexes = map_to_idx[player]
|
||||
|
||||
if not indexes then continue end
|
||||
|
||||
for _, idx in indexes do
|
||||
servers()[idx] = nil
|
||||
end
|
||||
servers(servers())
|
||||
end
|
||||
|
||||
for connector, packet in server_update:iter() do
|
||||
local outgoing = reverse_connector(connector)
|
||||
|
||||
map_to_idx[outgoing.host] = map_to_idx[outgoing.host] or {}
|
||||
local idx = map_to_idx[outgoing.host][outgoing.to_vm]
|
||||
|
||||
if not idx then
|
||||
-- print("new server")
|
||||
idx = n + 1; n += 1
|
||||
map_to_idx[outgoing.host][outgoing.to_vm] = idx
|
||||
servers()[idx] = {
|
||||
host = outgoing.host,
|
||||
vm = outgoing.to_vm,
|
||||
|
||||
schedulers = vide.source {},
|
||||
worlds = vide.source {}
|
||||
}
|
||||
servers(servers())
|
||||
-- print("set worlds")
|
||||
-- print(servers())
|
||||
end
|
||||
|
||||
local server = servers()[idx]
|
||||
local schedulers = server.schedulers()
|
||||
local worlds = server.worlds()
|
||||
|
||||
table.clear(schedulers)
|
||||
table.clear(worlds)
|
||||
|
||||
for index, data in packet.schedulers do
|
||||
local at = schedulers[index]
|
||||
if at and at.name == data.name and at.id == data.id then continue end
|
||||
schedulers[index] = data
|
||||
end
|
||||
|
||||
for index, data in packet.worlds do
|
||||
local at = worlds[index]
|
||||
if at and at.name == data.name and at.id == data.id then continue end
|
||||
worlds[index] = data
|
||||
end
|
||||
|
||||
server.schedulers(schedulers)
|
||||
server.worlds(worlds)
|
||||
|
||||
end
|
||||
end
|
||||
end
|
||||
183
modules/Jabby/client/apps/home/widget.luau
Normal file
183
modules/Jabby/client/apps/home/widget.luau
Normal file
|
|
@ -0,0 +1,183 @@
|
|||
local Players = game:GetService("Players")
|
||||
|
||||
local ui = require(script.Parent.Parent.Parent.Parent.ui)
|
||||
local vide = require(script.Parent.Parent.Parent.Parent.vide)
|
||||
local spawn_app = require(script.Parent.Parent.Parent.spawn_app)
|
||||
local overview_scheduler = require(script.Parent.Parent.overview_scheduler)
|
||||
local registry = require(script.Parent.Parent.registry)
|
||||
|
||||
local create = vide.create
|
||||
local derive = vide.derive
|
||||
local source = vide.source
|
||||
local values = vide.values
|
||||
local show = vide.show
|
||||
|
||||
type props = {
|
||||
|
||||
servers: () -> {
|
||||
{
|
||||
host: "server" | Player,
|
||||
vm: number,
|
||||
|
||||
schedulers: () -> {
|
||||
{id: number, name: string}
|
||||
},
|
||||
worlds: () -> {
|
||||
{id: number, name: string}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
destroy: () -> ()
|
||||
|
||||
}
|
||||
|
||||
return function(props: props)
|
||||
|
||||
local selected = source(Players.LocalPlayer)
|
||||
|
||||
local hosts = derive(function()
|
||||
local hosts = {}
|
||||
|
||||
for _, server in props.servers() do
|
||||
local host = server.host
|
||||
hosts[host] = hosts[host] or {}
|
||||
table.insert(hosts[host], server)
|
||||
end
|
||||
|
||||
hosts["all"] = props.servers()
|
||||
|
||||
return hosts
|
||||
end)
|
||||
|
||||
local options = derive(function()
|
||||
local options = {}
|
||||
|
||||
for host, servers in hosts() do
|
||||
options[host] = if type(host) == "string" then host else `@{host.Name}`
|
||||
end
|
||||
|
||||
options[Players.LocalPlayer] = "localplayer"
|
||||
options["all"] = "all"
|
||||
|
||||
return options
|
||||
end)
|
||||
|
||||
local function objects()
|
||||
return hosts()[selected()] or {} :: never
|
||||
end
|
||||
|
||||
local function is_empty()
|
||||
local no_objects = next(objects()) == nil
|
||||
if no_objects then return "No objects found. You may not have permissions to use this." end
|
||||
-- for _, object in objects() do
|
||||
-- if #object.worlds() == 0 then return "No worlds found. Did you forget to set updated?" end
|
||||
-- if #object.schedulers() == 0 then return "No schedulers found. Did you forget to set updated?" end
|
||||
-- end
|
||||
return false
|
||||
end
|
||||
|
||||
return ui.widget {
|
||||
title = "Home",
|
||||
min_size = Vector2.new(230, 200),
|
||||
bind_to_close = props.destroy,
|
||||
|
||||
ui.container {
|
||||
|
||||
create "UIListLayout" {
|
||||
Padding = UDim.new(0, 2),
|
||||
VerticalFlex = Enum.UIFlexAlignment.SpaceEvenly,
|
||||
HorizontalFlex = Enum.UIFlexAlignment.Fill
|
||||
},
|
||||
|
||||
ui.select {
|
||||
size = UDim2.new(1, 0, 0, 32),
|
||||
options = options :: any,
|
||||
selected = selected :: any,
|
||||
update_selected = selected
|
||||
},
|
||||
|
||||
create "ScrollingFrame" {
|
||||
-- Size = UDim2.fromScale(1, 1),
|
||||
CanvasSize = UDim2.new(),
|
||||
AutomaticCanvasSize = Enum.AutomaticSize.Y,
|
||||
BackgroundTransparency = 1,
|
||||
ScrollBarThickness = 6,
|
||||
HorizontalScrollBarInset = Enum.ScrollBarInset.Always,
|
||||
|
||||
create "UIFlexItem" {
|
||||
FlexMode = Enum.UIFlexMode.Fill
|
||||
},
|
||||
|
||||
ui.padding {
|
||||
x = UDim.new(0, 1),
|
||||
right = UDim.new(0, 8)
|
||||
},
|
||||
|
||||
create "UIListLayout" {
|
||||
FillDirection = Enum.FillDirection.Horizontal,
|
||||
HorizontalFlex = Enum.UIFlexAlignment.Fill,
|
||||
Padding = UDim.new(0, 8),
|
||||
Wraps = true
|
||||
},
|
||||
|
||||
show(is_empty, function()
|
||||
return ui.typography {
|
||||
text = is_empty(),
|
||||
xalignment = Enum.TextXAlignment.Left,
|
||||
wrapped = true
|
||||
}
|
||||
end),
|
||||
|
||||
values(objects, function(value, key)
|
||||
|
||||
return ui.pane{
|
||||
name = "",
|
||||
size = UDim2.fromOffset(200, 0),
|
||||
automaticsize = Enum.AutomaticSize.Y,
|
||||
|
||||
create "UIListLayout" {
|
||||
Padding = UDim.new(0, 8)
|
||||
},
|
||||
|
||||
ui.typography {
|
||||
text = `host: {value.host}\tvm id: {value.vm}`,
|
||||
wrapped = true
|
||||
},
|
||||
|
||||
values(value.worlds, function(world)
|
||||
return ui.button {
|
||||
size = UDim2.new(1, 0, 0, 30),
|
||||
text = `World: {world.name}`,
|
||||
|
||||
activated = function()
|
||||
spawn_app.spawn_app(registry, {
|
||||
host = value.host,
|
||||
vm = value.vm,
|
||||
id = world.id
|
||||
})
|
||||
end
|
||||
}
|
||||
end),
|
||||
|
||||
values(value.schedulers, function(scheduler)
|
||||
return ui.button {
|
||||
size = UDim2.new(1, 0, 0, 30),
|
||||
text = `Scheduler: {scheduler.name}`,
|
||||
|
||||
activated = function()
|
||||
spawn_app.spawn_app(overview_scheduler, {
|
||||
host = value.host,
|
||||
vm = value.vm,
|
||||
id = scheduler.id
|
||||
})
|
||||
end
|
||||
}
|
||||
end)
|
||||
}
|
||||
end)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
end
|
||||
68
modules/Jabby/client/apps/overview_scheduler/init.luau
Normal file
68
modules/Jabby/client/apps/overview_scheduler/init.luau
Normal file
|
|
@ -0,0 +1,68 @@
|
|||
local RunService = game:GetService("RunService")
|
||||
|
||||
local vide = require(script.Parent.Parent.Parent.vide)
|
||||
local loop = require(script.Parent.Parent.Parent.modules.loop)
|
||||
local remotes = require(script.Parent.Parent.Parent.modules.remotes)
|
||||
local widget = require(script.widget)
|
||||
|
||||
local source = vide.source
|
||||
local cleanup = vide.cleanup
|
||||
|
||||
local overview_scheduler = {
|
||||
class_name = "app" :: "app",
|
||||
name = "Scheduler"
|
||||
}
|
||||
|
||||
type props = {
|
||||
host: Player | "server",
|
||||
vm: number,
|
||||
id: number
|
||||
}
|
||||
|
||||
function overview_scheduler.mount(props: props, destroy: () -> ())
|
||||
|
||||
local system_data = source {}
|
||||
local system_frames = source {}
|
||||
local system_ids = source {}
|
||||
|
||||
local app_loop = loop (
|
||||
"app-client-scheduler",
|
||||
{
|
||||
host = props.host,
|
||||
vm = props.vm,
|
||||
id = props.id,
|
||||
|
||||
system_ids = system_ids,
|
||||
system_data = system_data,
|
||||
system_frames = system_frames,
|
||||
},
|
||||
|
||||
{i = 1},
|
||||
script.systems.get_scheduler_data
|
||||
)
|
||||
|
||||
cleanup(
|
||||
RunService.Heartbeat:Connect(app_loop)
|
||||
)
|
||||
|
||||
return widget {
|
||||
host = props.host,
|
||||
vm = props.vm,
|
||||
id = props.id,
|
||||
|
||||
system_ids = system_ids,
|
||||
system_data = system_data,
|
||||
system_frames = system_frames,
|
||||
pause_system = function(system: number)
|
||||
remotes.scheduler_system_pause:fire({
|
||||
host = props.host,
|
||||
to_vm = props.vm
|
||||
}, props.id, system, not system_data()[system].paused)
|
||||
end,
|
||||
|
||||
destroy = destroy
|
||||
}
|
||||
|
||||
end
|
||||
|
||||
return overview_scheduler
|
||||
46
modules/Jabby/client/apps/overview_scheduler/stack_bar.luau
Normal file
46
modules/Jabby/client/apps/overview_scheduler/stack_bar.luau
Normal file
|
|
@ -0,0 +1,46 @@
|
|||
local vide = require(script.Parent.Parent.Parent.Parent.vide)
|
||||
|
||||
local create = vide.create
|
||||
local indexes = vide.indexes
|
||||
local derive = vide.derive
|
||||
|
||||
type stack_bar = {
|
||||
values: () -> {{value: number, color: Color3}},
|
||||
selected: (number) -> ()
|
||||
}
|
||||
|
||||
return function(props: stack_bar)
|
||||
|
||||
local total = derive(function()
|
||||
local total = 0
|
||||
for _, value in props.values() do
|
||||
total += value.value
|
||||
end
|
||||
return total
|
||||
end)
|
||||
|
||||
return create "Frame" {
|
||||
Name = "Graph",
|
||||
Size = UDim2.new(1, 0, 0, 32),
|
||||
|
||||
indexes(props.values, function(value, index)
|
||||
return create "Frame" {
|
||||
|
||||
Size = function()
|
||||
return UDim2.fromScale(value().value / total(), 1)
|
||||
end,
|
||||
|
||||
BackgroundColor3 = function() return value().color end,
|
||||
|
||||
MouseEnter = function()
|
||||
props.selected(index)
|
||||
end,
|
||||
}
|
||||
end),
|
||||
|
||||
create "UIListLayout" {
|
||||
FillDirection = Enum.FillDirection.Horizontal,
|
||||
},
|
||||
}
|
||||
|
||||
end
|
||||
|
|
@ -0,0 +1,98 @@
|
|||
local vide = require(script.Parent.Parent.Parent.Parent.Parent.vide)
|
||||
local queue = require(script.Parent.Parent.Parent.Parent.Parent.modules.queue)
|
||||
local remotes = require(script.Parent.Parent.Parent.Parent.Parent.modules.remotes)
|
||||
local types = require(script.Parent.Parent.Parent.Parent.Parent.modules.types)
|
||||
|
||||
local batch = vide.batch
|
||||
local cleanup = vide.cleanup
|
||||
|
||||
type SystemId = types.SystemId
|
||||
type SystemData = types.SystemData
|
||||
type SystemFrame = types.SystemFrame
|
||||
|
||||
type context = {
|
||||
host: Player | "server",
|
||||
vm: number,
|
||||
id: number,
|
||||
|
||||
system_ids: vide.Source<{[SystemId]: true}>,
|
||||
system_data: vide.Source<{[SystemId]: types.SystemData}>,
|
||||
system_frames: vide.Source<{[SystemId]: {types.SystemFrame}}>,
|
||||
}
|
||||
|
||||
local MAX_BUFFER_SIZE = 50
|
||||
|
||||
return function(context: context)
|
||||
local outgoing = {
|
||||
host = context.host,
|
||||
to_vm = context.vm
|
||||
}
|
||||
|
||||
remotes.request_scheduler:fire(outgoing, context.id)
|
||||
|
||||
local scheduler_static_data_updated = queue(remotes.scheduler_system_static_update)
|
||||
local scheduler_frame_data_updated = queue(remotes.scheduler_system_update)
|
||||
|
||||
cleanup(function()
|
||||
remotes.disconnect_scheduler:fire(outgoing, context.id)
|
||||
end)
|
||||
|
||||
return function()
|
||||
|
||||
batch(function()
|
||||
for incoming, scheduler, id, new_data in scheduler_static_data_updated:iter() do
|
||||
if incoming.host ~= context.host then continue end
|
||||
if incoming.from_vm ~= context.vm then continue end
|
||||
if scheduler ~= context.id then continue end
|
||||
|
||||
if new_data == nil then
|
||||
context.system_ids()[id] = nil
|
||||
context.system_data()[id] = nil
|
||||
context.system_frames()[id] = nil
|
||||
|
||||
context.system_ids(context.system_ids())
|
||||
context.system_data(context.system_data())
|
||||
context.system_frames(context.system_frames())
|
||||
else
|
||||
context.system_ids()[id] = true
|
||||
context.system_data()[id] = new_data
|
||||
context.system_frames()[id] = context.system_frames()[id] or {}
|
||||
|
||||
context.system_ids(context.system_ids())
|
||||
context.system_data(context.system_data())
|
||||
context.system_frames(context.system_frames())
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
for incoming, scheduler, id, f, s in scheduler_frame_data_updated:iter() do
|
||||
if incoming.host ~= context.host then continue end
|
||||
if incoming.from_vm ~= context.vm then continue end
|
||||
if scheduler ~= context.id then continue end
|
||||
if context.system_frames()[id] == nil then continue end
|
||||
|
||||
-- look where to append the frame to
|
||||
local frames = context.system_frames()[id]
|
||||
local f_data = {i = f, s = s}
|
||||
local added = false
|
||||
|
||||
--- since it's unreliable we have to constantly check if we arent out of order
|
||||
for i, frame in frames do
|
||||
if frame.i == f then frames[i] = f_data; continue end
|
||||
if frame.i > f then continue end
|
||||
table.insert(frames, i, f_data)
|
||||
table.remove(frames, MAX_BUFFER_SIZE + 1)
|
||||
added = true
|
||||
break
|
||||
end
|
||||
|
||||
if #frames <= MAX_BUFFER_SIZE and added == false then
|
||||
table.insert(frames, f_data)
|
||||
end
|
||||
|
||||
context.system_frames(context.system_frames())
|
||||
end
|
||||
end)
|
||||
|
||||
end
|
||||
end
|
||||
412
modules/Jabby/client/apps/overview_scheduler/widget.luau
Normal file
412
modules/Jabby/client/apps/overview_scheduler/widget.luau
Normal file
|
|
@ -0,0 +1,412 @@
|
|||
local ui = require(script.Parent.Parent.Parent.Parent.ui)
|
||||
local vide = require(script.Parent.Parent.Parent.Parent.vide)
|
||||
local convert_units = require(script.Parent.Parent.Parent.Parent.modules.convert_units)
|
||||
local types = require(script.Parent.Parent.Parent.Parent.modules.types)
|
||||
local spawn_app = require(script.Parent.Parent.Parent.spawn_app)
|
||||
local system_widget = require(script.Parent.Parent.system)
|
||||
local stack_bar = require(script.Parent.stack_bar)
|
||||
|
||||
local create = vide.create
|
||||
local indexes = vide.indexes
|
||||
local values = vide.values
|
||||
local changed = vide.changed
|
||||
local source = vide.source
|
||||
local derive = vide.derive
|
||||
|
||||
type SystemId = number
|
||||
|
||||
type props = {
|
||||
host: Player | "server",
|
||||
vm: number,
|
||||
id: number,
|
||||
|
||||
system_ids: () -> {[SystemId]: true},
|
||||
system_data: () -> {[SystemId]: types.SystemData},
|
||||
system_frames: () -> {[SystemId]: {types.SystemFrame}},
|
||||
|
||||
pause_system: (SystemId) -> (),
|
||||
destroy: () -> ()
|
||||
|
||||
}
|
||||
|
||||
local function color(n: number)
|
||||
return Color3.fromHSV((0.15 * (n-1)) % 1, 1, 1)
|
||||
end
|
||||
|
||||
local sort_by_options = {
|
||||
"Name",
|
||||
"Id",
|
||||
"Frame Time"
|
||||
}
|
||||
|
||||
return function(props: props)
|
||||
|
||||
local selected = source(0)
|
||||
local systems_query = source("")
|
||||
|
||||
local sort_by = source(2)
|
||||
|
||||
local max_frametime = derive(function()
|
||||
local max = 0
|
||||
for _, frames in props.system_frames() do
|
||||
local sum = 0
|
||||
|
||||
for _, frame in frames do
|
||||
sum += frame.s
|
||||
end
|
||||
|
||||
max = math.max(max, sum / #frames)
|
||||
end
|
||||
return max
|
||||
end)
|
||||
|
||||
local map_phases_to_systems = derive(function()
|
||||
local phases: {[false | string]: {number}} = {[false] = {}}
|
||||
for id, data in props.system_data() do
|
||||
if phases[data.phase or false] == nil then phases[data.phase or false] = {} end
|
||||
table.insert(phases[data.phase or false], id)
|
||||
end
|
||||
return phases
|
||||
end)
|
||||
|
||||
local function system(id: number)
|
||||
local gui_state = source(Enum.GuiState.Idle)
|
||||
|
||||
local function frame_time()
|
||||
local sum = 0
|
||||
local frames = props.system_frames()[id]
|
||||
|
||||
for _, frame in frames do
|
||||
sum += frame.s
|
||||
end
|
||||
|
||||
return sum / #frames
|
||||
end
|
||||
|
||||
local b = create "ImageButton" {
|
||||
Name = function()
|
||||
return props.system_data()[id].name
|
||||
end,
|
||||
Size = UDim2.new(1, 0, 0, 32),
|
||||
|
||||
LayoutOrder = function()
|
||||
return if sort_by() == 3 then 1e9 - frame_time() * 1e8 else id
|
||||
end,
|
||||
|
||||
BackgroundColor3 = function()
|
||||
return if gui_state() == Enum.GuiState.Press then
|
||||
ui.theme.bg[-1]()
|
||||
elseif gui_state() == Enum.GuiState.Hover then
|
||||
ui.theme.bg[6]()
|
||||
else
|
||||
ui.theme.bg[3]()
|
||||
end,
|
||||
|
||||
Visible = function()
|
||||
return not not string.match(props.system_data()[id].name, systems_query())
|
||||
end,
|
||||
|
||||
changed("GuiState", gui_state),
|
||||
|
||||
Activated = function()
|
||||
spawn_app.spawn_app(system_widget, {
|
||||
host = props.host,
|
||||
vm = props.vm,
|
||||
|
||||
scheduler = props.id,
|
||||
system = id,
|
||||
name = props.system_data()[id].name
|
||||
})
|
||||
end,
|
||||
|
||||
MouseButton2Click = function()
|
||||
props.pause_system(id)
|
||||
end,
|
||||
|
||||
-- create a frame that ignores all rules!
|
||||
create "Folder" {
|
||||
create "Frame" {
|
||||
Position = UDim2.new(0, 0, 1, 4),
|
||||
AnchorPoint = Vector2.new(0, 1),
|
||||
Size = function()
|
||||
return UDim2.new(frame_time() / max_frametime(), 0, 0, 1)
|
||||
end,
|
||||
|
||||
BackgroundColor3 = ui.theme.fg_on_bg_high[0]
|
||||
|
||||
}
|
||||
},
|
||||
|
||||
create "UIStroke" {
|
||||
Color = ui.theme.bg[-3]
|
||||
},
|
||||
|
||||
create "UICorner" {
|
||||
CornerRadius = UDim.new(0, 4)
|
||||
},
|
||||
|
||||
create "UIListLayout" {
|
||||
FillDirection = Enum.FillDirection.Horizontal,
|
||||
VerticalAlignment = Enum.VerticalAlignment.Center,
|
||||
HorizontalFlex = Enum.UIFlexAlignment.SpaceEvenly,
|
||||
Padding = UDim.new(0, 8)
|
||||
},
|
||||
|
||||
ui.padding {
|
||||
x = UDim.new(0, 8)
|
||||
},
|
||||
|
||||
create "Frame" {
|
||||
Size = UDim2.fromOffset(16, 16),
|
||||
AnchorPoint = Vector2.new(0.5, 0.5),
|
||||
|
||||
BackgroundColor3 = color(id),
|
||||
|
||||
create "UICorner" {
|
||||
CornerRadius = UDim.new(1, 0)
|
||||
},
|
||||
},
|
||||
|
||||
ui.typography {
|
||||
automaticsize = Enum.AutomaticSize.None,
|
||||
text = function()
|
||||
return props.system_data()[id].name
|
||||
end,
|
||||
|
||||
truncate = Enum.TextTruncate.SplitWord,
|
||||
xalignment = Enum.TextXAlignment.Left,
|
||||
disabled = function()
|
||||
return props.system_data()[id].paused
|
||||
end,
|
||||
|
||||
create "UIFlexItem" {
|
||||
FlexMode = Enum.UIFlexMode.Fill,
|
||||
GrowRatio = 1,
|
||||
ShrinkRatio = 1
|
||||
}
|
||||
},
|
||||
|
||||
ui.typography {
|
||||
automaticsize = Enum.AutomaticSize.XY,
|
||||
text = function()
|
||||
local sum = 0
|
||||
local frames = props.system_frames()[id]
|
||||
|
||||
for _, frame in frames do
|
||||
sum += frame.s
|
||||
end
|
||||
|
||||
return `{convert_units("s", sum / #frames)}`
|
||||
end,
|
||||
|
||||
xalignment = Enum.TextXAlignment.Right,
|
||||
|
||||
disabled = true,
|
||||
|
||||
},
|
||||
|
||||
}
|
||||
return b
|
||||
end
|
||||
|
||||
return ui.widget {
|
||||
title = "Scheduler",
|
||||
subtitle = `host: {props.host} vm: {props.vm} id: {props.id}`,
|
||||
|
||||
min_size = Vector2.new(200, 300),
|
||||
|
||||
bind_to_close = props.destroy,
|
||||
|
||||
create "Frame" {
|
||||
Name = "Elements",
|
||||
Size = UDim2.fromScale(1, 1),
|
||||
AutomaticSize = Enum.AutomaticSize.Y,
|
||||
|
||||
BackgroundTransparency = 1,
|
||||
|
||||
create "UIListLayout" {
|
||||
VerticalAlignment = Enum.VerticalAlignment.Bottom,
|
||||
FillDirection = Enum.FillDirection.Vertical,
|
||||
VerticalFlex = Enum.UIFlexAlignment.SpaceBetween,
|
||||
Padding = UDim.new(0, 8)
|
||||
},
|
||||
|
||||
create "Frame" {
|
||||
Size = UDim2.fromScale(1, 0),
|
||||
AutomaticSize = Enum.AutomaticSize.Y,
|
||||
|
||||
BackgroundTransparency = 1,
|
||||
|
||||
-- create "UIFlexItem" {
|
||||
-- FlexMode = Enum.UIFlexMode.Custom,
|
||||
-- GrowRatio = 0,
|
||||
-- ShrinkRatio = 0
|
||||
-- },
|
||||
|
||||
ui.pane {
|
||||
name = "Overview",
|
||||
size = UDim2.fromScale(1, 0),
|
||||
|
||||
create "UIListLayout" {
|
||||
FillDirection = Enum.FillDirection.Vertical
|
||||
},
|
||||
|
||||
ui.typography {
|
||||
text = function()
|
||||
local run_time = 0
|
||||
for id, frames in props.system_frames() do
|
||||
if props.system_data()[id].paused then continue end
|
||||
local sum = 0
|
||||
|
||||
for _, frame in frames do
|
||||
sum += frame.s
|
||||
end
|
||||
|
||||
run_time += sum / #frames
|
||||
end
|
||||
|
||||
return `Run time: {convert_units("s", run_time)}`
|
||||
end
|
||||
},
|
||||
|
||||
stack_bar {
|
||||
values = function()
|
||||
local v = {}
|
||||
|
||||
local system_ids = props.system_ids()
|
||||
local system_frames = props.system_frames()
|
||||
|
||||
for i = 1, table.maxn(system_ids) do
|
||||
if system_ids[i] == nil then continue end
|
||||
if props.system_data()[i].paused then continue end
|
||||
|
||||
local sum = 0
|
||||
local frames = system_frames[i]
|
||||
|
||||
for _, frame in frames do
|
||||
sum += frame.s
|
||||
end
|
||||
|
||||
table.insert(v, {value = sum / #frames, color = color(i)})
|
||||
end
|
||||
|
||||
return v
|
||||
end,
|
||||
selected = selected
|
||||
},
|
||||
|
||||
ui.row {
|
||||
justifycontent = Enum.UIFlexAlignment.Fill,
|
||||
ui.button {
|
||||
text = "Pause all",
|
||||
|
||||
activated = function()
|
||||
for system, data in props.system_data() do
|
||||
if data.paused then continue end
|
||||
props.pause_system(system)
|
||||
end
|
||||
end
|
||||
},
|
||||
|
||||
ui.button {
|
||||
text = "Resume all",
|
||||
|
||||
activated = function()
|
||||
for system, data in props.system_data() do
|
||||
if not data.paused then continue end
|
||||
props.pause_system(system)
|
||||
end
|
||||
end
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
},
|
||||
|
||||
ui.select {
|
||||
size = UDim2.new(1, 0, 0, 30),
|
||||
|
||||
options = sort_by_options,
|
||||
selected = sort_by,
|
||||
update_selected = function(new)
|
||||
-- print(new)
|
||||
sort_by(new)
|
||||
end
|
||||
},
|
||||
|
||||
ui.textfield {
|
||||
size = UDim2.new(1, 0, 0, 36),
|
||||
|
||||
placeholder = "System Match",
|
||||
|
||||
oninput = systems_query,
|
||||
},
|
||||
|
||||
create "ScrollingFrame" {
|
||||
Name = "Systems",
|
||||
|
||||
Size = UDim2.fromScale(1, 0),
|
||||
CanvasSize = UDim2.new(),
|
||||
AutomaticCanvasSize = Enum.AutomaticSize.Y,
|
||||
|
||||
BackgroundTransparency = 1,
|
||||
|
||||
ScrollBarThickness = 6,
|
||||
VerticalScrollBarInset = Enum.ScrollBarInset.Always,
|
||||
ScrollBarImageColor3 = ui.theme.fg_on_bg_low[3],
|
||||
|
||||
create "UIFlexItem" {
|
||||
FlexMode = Enum.UIFlexMode.Fill
|
||||
},
|
||||
|
||||
create "UIListLayout" {
|
||||
FillDirection = Enum.FillDirection.Vertical,
|
||||
Padding = UDim.new(0, 8),
|
||||
|
||||
SortOrder = function()
|
||||
return if sort_by() == 1 then Enum.SortOrder.Name else Enum.SortOrder.LayoutOrder
|
||||
end
|
||||
},
|
||||
|
||||
ui.padding {
|
||||
y = UDim.new(0, 1),
|
||||
x = UDim.new(0, 1)
|
||||
},
|
||||
|
||||
values(function()
|
||||
return map_phases_to_systems()[false]
|
||||
end, system),
|
||||
|
||||
indexes(map_phases_to_systems, function(systems, phase)
|
||||
if phase == false then return {} end
|
||||
local expanded = source(true)
|
||||
-- print(systems())
|
||||
return ui.accordion {
|
||||
expanded = expanded,
|
||||
set_expanded = expanded,
|
||||
text = phase,
|
||||
|
||||
ui.container {
|
||||
Size = UDim2.fromScale(1, 0),
|
||||
|
||||
create "UIListLayout" {
|
||||
FillDirection = Enum.FillDirection.Vertical,
|
||||
Padding = UDim.new(0, 8),
|
||||
|
||||
SortOrder = function()
|
||||
return if sort_by() == 1 then Enum.SortOrder.Name else Enum.SortOrder.LayoutOrder
|
||||
end
|
||||
},
|
||||
|
||||
values(systems, system)
|
||||
}
|
||||
}
|
||||
end),
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
end
|
||||
154
modules/Jabby/client/apps/registry/init.luau
Normal file
154
modules/Jabby/client/apps/registry/init.luau
Normal file
|
|
@ -0,0 +1,154 @@
|
|||
local ContextActionService = game:GetService("ContextActionService")
|
||||
local RunService = game:GetService("RunService")
|
||||
|
||||
local vide = require(script.Parent.Parent.Parent.vide)
|
||||
local loop = require(script.Parent.Parent.Parent.modules.loop)
|
||||
local spawn_app = require(script.Parent.Parent.spawn_app)
|
||||
local entity_widget = require(script.Parent.entity)
|
||||
local widget = require(script.widget)
|
||||
|
||||
local source = vide.source
|
||||
local effect = vide.effect
|
||||
local cleanup = vide.cleanup
|
||||
|
||||
local overview_query = {
|
||||
class_name = "app" :: "app",
|
||||
name = "Query"
|
||||
}
|
||||
|
||||
type props = {
|
||||
host: Player | "server",
|
||||
vm: number,
|
||||
id: number
|
||||
}
|
||||
|
||||
function overview_query.mount(props: props, destroy: () -> ())
|
||||
|
||||
-- the entity id
|
||||
local current_entity = source(nil)
|
||||
-- enables picking
|
||||
local enable_pick = source(false)
|
||||
-- the entity data as a string
|
||||
local entity_hovering_over = source()
|
||||
-- the part the player is hovering over
|
||||
local hovering_over = source()
|
||||
|
||||
local validate_query = source("")
|
||||
local ok = source(false)
|
||||
local msg = source("")
|
||||
|
||||
local query = source("")
|
||||
local primary_entity = source()
|
||||
local columns = source({})
|
||||
local from = source(1)
|
||||
local upto = source(25)
|
||||
local total_entities = source(0)
|
||||
|
||||
local paused = source(false)
|
||||
local refresh = source(false)
|
||||
|
||||
-- check if the query and columns are properly
|
||||
local app_loop = loop (
|
||||
"app-client-registry",
|
||||
{
|
||||
host = props.host,
|
||||
vm = props.vm,
|
||||
id = props.id,
|
||||
|
||||
enable_pick = enable_pick,
|
||||
entity_hovering_over = entity_hovering_over,
|
||||
hovering_over = hovering_over,
|
||||
set_entity = current_entity,
|
||||
|
||||
columns = columns,
|
||||
query = query,
|
||||
primary_entity = primary_entity,
|
||||
paused = paused,
|
||||
refresh = refresh,
|
||||
|
||||
total_entities = total_entities,
|
||||
|
||||
from = from,
|
||||
upto = upto,
|
||||
|
||||
validate_query = validate_query,
|
||||
ok = ok,
|
||||
msg = msg
|
||||
},
|
||||
|
||||
{i = 1},
|
||||
script.systems.validate_query,
|
||||
script.systems.obtain_query_data,
|
||||
script.systems.send_workspace_entity,
|
||||
script.systems.highlight_workspace_entity
|
||||
)
|
||||
|
||||
cleanup(RunService.Heartbeat:Connect(app_loop))
|
||||
|
||||
local function open_entity_widget(_, state: Enum.UserInputState)
|
||||
local entity = current_entity()
|
||||
|
||||
if state ~= Enum.UserInputState.Begin then return end
|
||||
if entity == nil then return end
|
||||
|
||||
enable_pick(false)
|
||||
entity_hovering_over(nil)
|
||||
hovering_over(nil)
|
||||
|
||||
spawn_app.spawn_app(entity_widget, {
|
||||
host = props.host,
|
||||
vm = props.vm,
|
||||
id = props.id,
|
||||
entity = entity
|
||||
})
|
||||
end
|
||||
|
||||
effect(function()
|
||||
local picking = enable_pick()
|
||||
local key = `select entity:{props.host} {props.vm} {props.id}`
|
||||
|
||||
if picking then
|
||||
ContextActionService:BindAction(key, open_entity_widget, false, Enum.UserInputType.MouseButton1)
|
||||
end
|
||||
|
||||
cleanup(function()
|
||||
ContextActionService:UnbindAction(key)
|
||||
end)
|
||||
|
||||
end)
|
||||
|
||||
return widget {
|
||||
host = props.host,
|
||||
vm = props.vm,
|
||||
id = props.id,
|
||||
|
||||
validate_query = validate_query,
|
||||
|
||||
update_system_query = query,
|
||||
current_query = query,
|
||||
total_rows_per_page = source(25),
|
||||
set_rows_per_page = source(25),
|
||||
|
||||
primary_entity = primary_entity,
|
||||
|
||||
from = from,
|
||||
upto = upto,
|
||||
total_entities = total_entities,
|
||||
paused = paused,
|
||||
refresh = refresh,
|
||||
|
||||
enable_pick = enable_pick,
|
||||
entity_hovering_over = entity_hovering_over,
|
||||
hovering_over = hovering_over,
|
||||
|
||||
ok = ok,
|
||||
msg = msg,
|
||||
|
||||
columns = columns,
|
||||
|
||||
destroy = destroy
|
||||
}
|
||||
|
||||
end
|
||||
|
||||
return overview_query
|
||||
|
|
@ -0,0 +1,34 @@
|
|||
local queue = require(script.Parent.Parent.Parent.Parent.Parent.modules.queue)
|
||||
local remotes = require(script.Parent.Parent.Parent.Parent.Parent.modules.remotes)
|
||||
|
||||
type Context = {
|
||||
host: Player | "server",
|
||||
vm: number,
|
||||
id: number,
|
||||
|
||||
enable_pick: () -> boolean,
|
||||
entity_hovering_over: (string) -> (),
|
||||
set_entity: (number) -> (),
|
||||
hovering_over: (BasePart) -> ()
|
||||
|
||||
}
|
||||
|
||||
return function(context: Context)
|
||||
|
||||
local send_mouse_entity = queue(remotes.send_mouse_entity)
|
||||
|
||||
return function()
|
||||
|
||||
for incoming, id, to_highlight, entity, components in send_mouse_entity:iter() do
|
||||
if incoming.host ~= context.host then continue end
|
||||
if incoming.from_vm ~= context.vm then continue end
|
||||
if id ~= context.id then continue end
|
||||
if context.enable_pick() == false then continue end
|
||||
context.hovering_over(to_highlight)
|
||||
context.entity_hovering_over(components)
|
||||
context.set_entity(entity)
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
end
|
||||
|
|
@ -0,0 +1,128 @@
|
|||
local vide = require(script.Parent.Parent.Parent.Parent.Parent.vide)
|
||||
local queue = require(script.Parent.Parent.Parent.Parent.Parent.modules.queue)
|
||||
local remotes = require(script.Parent.Parent.Parent.Parent.Parent.modules.remotes)
|
||||
|
||||
local effect = vide.effect
|
||||
local batch = vide.batch
|
||||
local cleanup = vide.cleanup
|
||||
|
||||
type Context = {
|
||||
host: Player | "server",
|
||||
vm: number,
|
||||
id: number,
|
||||
|
||||
columns: vide.Source<{ { any } }>,
|
||||
query: () -> string,
|
||||
|
||||
paused: () -> boolean,
|
||||
refresh: vide.Source<boolean>,
|
||||
|
||||
total_entities: (number) -> (),
|
||||
|
||||
from: () -> number,
|
||||
upto: () -> number,
|
||||
|
||||
}
|
||||
|
||||
local function generate_random_query_id()
|
||||
return math.random(2 ^ 31 - 1)
|
||||
end
|
||||
|
||||
return function(context: Context)
|
||||
|
||||
local query_changed = false
|
||||
local page_changed = false
|
||||
|
||||
effect(function()
|
||||
if #context.query() > 0 then
|
||||
query_changed = true
|
||||
end
|
||||
end)
|
||||
|
||||
effect(function()
|
||||
context.from()
|
||||
context.upto()
|
||||
page_changed = true
|
||||
end)
|
||||
|
||||
local current_query_id = -1
|
||||
local query_last_frame = 0
|
||||
local update_query_result = queue(remotes.update_query_result)
|
||||
local count_updated = queue(remotes.count_total_entities)
|
||||
|
||||
local columns = context.columns
|
||||
local outgoing = {
|
||||
host = context.host,
|
||||
to_vm = context.vm
|
||||
}
|
||||
|
||||
cleanup(function()
|
||||
remotes.disconnect_query:fire(outgoing, current_query_id)
|
||||
end)
|
||||
|
||||
local should_refresh = false
|
||||
effect(function()
|
||||
if context.refresh() ~= true then return end
|
||||
context.refresh(false)
|
||||
should_refresh = true
|
||||
end)
|
||||
|
||||
local paused_state = context.paused()
|
||||
local paused_updated = false
|
||||
effect(function()
|
||||
paused_updated = true
|
||||
paused_state = context.paused()
|
||||
end)
|
||||
|
||||
return function()
|
||||
|
||||
if query_changed then
|
||||
columns({})
|
||||
remotes.disconnect_query:fire(outgoing, current_query_id)
|
||||
current_query_id = generate_random_query_id()
|
||||
-- print("requesting new query", current_query_id)
|
||||
remotes.request_query:fire(outgoing, context.id, current_query_id, context.query())
|
||||
remotes.advance_query_page:fire(outgoing, current_query_id, context.from(), context.upto())
|
||||
query_last_frame = 0
|
||||
query_changed = false
|
||||
|
||||
remotes.pause_query:fire(outgoing, current_query_id, paused_state)
|
||||
end
|
||||
|
||||
if page_changed then
|
||||
remotes.advance_query_page:fire(outgoing, current_query_id, context.from(), context.upto())
|
||||
page_changed = false
|
||||
end
|
||||
|
||||
for incoming, query, value in count_updated:iter() do
|
||||
if query ~= current_query_id then continue end
|
||||
context.total_entities(value)
|
||||
end
|
||||
|
||||
if paused_updated then
|
||||
remotes.pause_query:fire(outgoing, current_query_id, paused_state)
|
||||
paused_updated = false
|
||||
end
|
||||
|
||||
if should_refresh then
|
||||
remotes.refresh_results:fire(outgoing, current_query_id)
|
||||
should_refresh = false
|
||||
end
|
||||
|
||||
batch(function()
|
||||
for incoming, query, frame, column, row, value in update_query_result:iter() do
|
||||
if query ~= current_query_id then continue end
|
||||
if frame < query_last_frame - 10 then continue end
|
||||
query_last_frame = math.max(query_last_frame, frame)
|
||||
|
||||
if columns()[column] == nil then
|
||||
columns()[column] = {}
|
||||
end
|
||||
|
||||
columns()[column][row] = value
|
||||
columns(columns())
|
||||
end
|
||||
end)
|
||||
|
||||
end
|
||||
end
|
||||
|
|
@ -0,0 +1,33 @@
|
|||
local UserInputService = game:GetService("UserInputService")
|
||||
|
||||
local remotes = require(script.Parent.Parent.Parent.Parent.Parent.modules.remotes)
|
||||
|
||||
type Context = {
|
||||
host: Player | "server",
|
||||
vm: number,
|
||||
id: number,
|
||||
|
||||
enable_pick: () -> boolean,
|
||||
|
||||
}
|
||||
|
||||
return function(context: Context)
|
||||
|
||||
if context.enable_pick() == false then return end
|
||||
|
||||
local location = UserInputService:GetMouseLocation()
|
||||
local camera = workspace.CurrentCamera
|
||||
|
||||
local ray = camera:ViewportPointToRay(location.X, location.Y)
|
||||
|
||||
remotes.send_mouse_pointer:fire(
|
||||
{
|
||||
host = context.host,
|
||||
to_vm = context.vm
|
||||
},
|
||||
context.id,
|
||||
ray.Origin,
|
||||
ray.Direction
|
||||
)
|
||||
|
||||
end
|
||||
|
|
@ -0,0 +1,98 @@
|
|||
local Players = game:GetService("Players")
|
||||
|
||||
local jecs = require(script.Parent.Parent.Parent.Parent.Parent.jecs)
|
||||
local vide = require(script.Parent.Parent.Parent.Parent.Parent.vide)
|
||||
local queue = require(script.Parent.Parent.Parent.Parent.Parent.modules.queue)
|
||||
local remotes = require(script.Parent.Parent.Parent.Parent.Parent.modules.remotes)
|
||||
|
||||
local effect = vide.effect
|
||||
|
||||
type Context = {
|
||||
host: Player | "server",
|
||||
vm: number,
|
||||
id: number,
|
||||
|
||||
validate_query: () -> string,
|
||||
|
||||
msg: (string) -> (),
|
||||
ok: (boolean) -> (),
|
||||
primary_entity: (any?) -> ()
|
||||
|
||||
}
|
||||
|
||||
return function(context: Context)
|
||||
|
||||
local query_changed = false
|
||||
|
||||
effect(function()
|
||||
context.validate_query()
|
||||
|
||||
query_changed = true
|
||||
end)
|
||||
|
||||
local n = 0
|
||||
local already_validated = false
|
||||
local MIN_DELAY_UNTIL_VALIDATE = 0
|
||||
|
||||
local validate_result = queue(remotes.validate_result)
|
||||
|
||||
if context.host == Players.LocalPlayer then
|
||||
MIN_DELAY_UNTIL_VALIDATE = 0.3
|
||||
elseif context.host == "server" then
|
||||
MIN_DELAY_UNTIL_VALIDATE = 0.5
|
||||
else
|
||||
MIN_DELAY_UNTIL_VALIDATE = 0.5
|
||||
end
|
||||
|
||||
return function(dt)
|
||||
if query_changed then
|
||||
n = 0
|
||||
already_validated = false
|
||||
query_changed = false
|
||||
|
||||
context.ok(false)
|
||||
context.msg("")
|
||||
end
|
||||
|
||||
for incoming, world, query, terms, ok, msg in validate_result:iter() do
|
||||
if incoming.host ~= context.host then continue end
|
||||
if incoming.from_vm ~= context.vm then continue end
|
||||
if world ~= context.id then continue end
|
||||
if query ~= context.validate_query() then continue end
|
||||
|
||||
context.ok(ok)
|
||||
context.msg(msg or "")
|
||||
context.primary_entity(nil)
|
||||
|
||||
if not terms then continue end
|
||||
if #terms.include + #terms.exclude + #terms.with > 1 then continue end
|
||||
local first = terms.include[1]
|
||||
|
||||
if first == nil then continue end
|
||||
if jecs.IS_PAIR(first) then continue end
|
||||
context.primary_entity(terms.include[1])
|
||||
end
|
||||
|
||||
n += dt
|
||||
if n < MIN_DELAY_UNTIL_VALIDATE then return end
|
||||
if already_validated then return end
|
||||
|
||||
if context.validate_query() == "" then
|
||||
context.ok(false)
|
||||
context.msg("empty query")
|
||||
return
|
||||
end
|
||||
|
||||
already_validated = true
|
||||
|
||||
remotes.validate_query:fire(
|
||||
{
|
||||
host = context.host,
|
||||
to_vm = context.vm
|
||||
},
|
||||
context.id,
|
||||
context.validate_query()
|
||||
)
|
||||
|
||||
end
|
||||
end
|
||||
503
modules/Jabby/client/apps/registry/widget.luau
Normal file
503
modules/Jabby/client/apps/registry/widget.luau
Normal file
|
|
@ -0,0 +1,503 @@
|
|||
local RunService = game:GetService("RunService")
|
||||
local UserInputService = game:GetService("UserInputService")
|
||||
|
||||
local ui = require(script.Parent.Parent.Parent.Parent.ui)
|
||||
local vide = require(script.Parent.Parent.Parent.Parent.vide)
|
||||
local tooltip = require(script.Parent.Parent.Parent.components.tooltip)
|
||||
local spawn_app = require(script.Parent.Parent.Parent.spawn_app)
|
||||
local entity = require(script.Parent.Parent.entity)
|
||||
|
||||
local create = vide.create
|
||||
local effect = vide.effect
|
||||
local source = vide.source
|
||||
local show = vide.show
|
||||
|
||||
type SystemId = number
|
||||
|
||||
type props = {
|
||||
host: Player | "server",
|
||||
vm: number,
|
||||
id: number,
|
||||
|
||||
validate_query: (string) -> (),
|
||||
ok: () -> boolean,
|
||||
msg: () -> string,
|
||||
|
||||
paused: vide.Source<boolean>,
|
||||
refresh: (boolean) -> (),
|
||||
|
||||
from: vide.Source<number>,
|
||||
upto: vide.Source<number>,
|
||||
|
||||
primary_entity: () -> number?,
|
||||
|
||||
update_system_query: (query: string) -> (),
|
||||
current_query: () -> string,
|
||||
total_entities: () -> number,
|
||||
|
||||
enable_pick: vide.Source<boolean>,
|
||||
entity_hovering_over: () -> string,
|
||||
hovering_over: () -> BasePart,
|
||||
|
||||
columns: () -> {{any}},
|
||||
|
||||
destroy: () -> ()
|
||||
}
|
||||
|
||||
local mouse_location = source(Vector2.zero)
|
||||
|
||||
RunService.PreRender:Connect(function()
|
||||
mouse_location(UserInputService:GetMouseLocation())
|
||||
end)
|
||||
|
||||
return function(props: props)
|
||||
|
||||
local page_input = source("1")
|
||||
local rows_input = source("25")
|
||||
|
||||
local page = source(1)
|
||||
local rows = source(20)
|
||||
|
||||
effect(function()
|
||||
page_input(tostring(page()))
|
||||
end)
|
||||
|
||||
effect(function()
|
||||
rows_input(tostring(rows()))
|
||||
end)
|
||||
|
||||
effect(function()
|
||||
local page = page()
|
||||
local rows_per_page = rows()
|
||||
|
||||
local from = (page - 1) * rows_per_page + 1
|
||||
local upto = from + rows_per_page - 1
|
||||
|
||||
props.from(from)
|
||||
props.upto(upto)
|
||||
end)
|
||||
|
||||
local row_template = {
|
||||
Size = UDim2.new(0, 0, 0, 26),
|
||||
AutomaticSize = Enum.AutomaticSize.X
|
||||
} :: any
|
||||
|
||||
return ui.widget {
|
||||
title = "Querying",
|
||||
subtitle = `host: {props.host} vm: {props.vm} id: {props.id}`,
|
||||
|
||||
min_size = Vector2.new(300, 300),
|
||||
|
||||
bind_to_close = props.destroy,
|
||||
|
||||
create "Frame" {
|
||||
|
||||
Size = UDim2.fromScale(1, 1),
|
||||
BackgroundTransparency = 1,
|
||||
|
||||
tooltip {
|
||||
transparency = 0.3,
|
||||
visible = function()
|
||||
return props.entity_hovering_over() and #props.entity_hovering_over() > 0 or false
|
||||
end,
|
||||
|
||||
ui.typography {
|
||||
automaticsize = Enum.AutomaticSize.XY,
|
||||
|
||||
text = function()
|
||||
return props.entity_hovering_over() or ""
|
||||
end,
|
||||
xalignment = Enum.TextXAlignment.Left,
|
||||
wrapped = true,
|
||||
code = true,
|
||||
|
||||
{ RichText = true },
|
||||
|
||||
create "UIStroke" {
|
||||
Thickness = 1,
|
||||
Color = ui.theme.bg[-5]
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
create "Highlight" {
|
||||
DepthMode = Enum.HighlightDepthMode.AlwaysOnTop,
|
||||
OutlineColor = Color3.new(1, 1, 1),
|
||||
FillColor = ui.theme.acc[0],
|
||||
FillTransparency = 0.5,
|
||||
|
||||
Adornee = props.hovering_over
|
||||
},
|
||||
|
||||
create "UIListLayout" {
|
||||
VerticalFlex = Enum.UIFlexAlignment.SpaceAround,
|
||||
Padding = UDim.new(0, 8)
|
||||
},
|
||||
|
||||
create "Frame" {
|
||||
Name = "Query + Pick",
|
||||
Size = UDim2.new(1, 0, 0, 30),
|
||||
|
||||
BackgroundTransparency = 1,
|
||||
|
||||
create "UIListLayout" {
|
||||
FillDirection = Enum.FillDirection.Horizontal,
|
||||
HorizontalFlex = Enum.UIFlexAlignment.SpaceAround,
|
||||
VerticalFlex = Enum.UIFlexAlignment.SpaceAround,
|
||||
Padding = UDim.new(0, 8)
|
||||
},
|
||||
|
||||
create "Frame" {
|
||||
|
||||
Name = "Query",
|
||||
|
||||
Size = UDim2.fromScale(0, 1),
|
||||
BackgroundTransparency = 1,
|
||||
|
||||
create "UIFlexItem" {
|
||||
FlexMode = Enum.UIFlexMode.Fill
|
||||
},
|
||||
|
||||
ui.textfield {
|
||||
size = UDim2.new(1, 0, 0, 30),
|
||||
placeholder = "Query",
|
||||
|
||||
code = true,
|
||||
|
||||
oninput = function(text)
|
||||
props.validate_query(text)
|
||||
end,
|
||||
|
||||
enter = function(text)
|
||||
props.update_system_query(text)
|
||||
end
|
||||
},
|
||||
|
||||
},
|
||||
|
||||
vide.show(function()
|
||||
return props.primary_entity()
|
||||
end, function()
|
||||
return ui.button {
|
||||
size = UDim2.fromOffset(30, 30),
|
||||
|
||||
text = "",
|
||||
|
||||
activated = function()
|
||||
spawn_app.spawn_app(entity, {
|
||||
host = props.host,
|
||||
vm = props.vm,
|
||||
id = props.id,
|
||||
entity = props.primary_entity()
|
||||
})
|
||||
end,
|
||||
|
||||
create "ImageLabel" {
|
||||
Size = UDim2.fromOffset(24, 24),
|
||||
Position = UDim2.fromScale(0.5, 0.5),
|
||||
AnchorPoint = Vector2.new(0.5, 0.5),
|
||||
|
||||
BackgroundTransparency = 1,
|
||||
|
||||
ImageColor3 = ui.theme.fg_on_bg_high[3],
|
||||
|
||||
Image = "rbxassetid://10723415903"
|
||||
},
|
||||
|
||||
}
|
||||
end),
|
||||
|
||||
ui.button {
|
||||
size = UDim2.fromOffset(30, 30),
|
||||
|
||||
text = "",
|
||||
|
||||
accent = props.enable_pick,
|
||||
|
||||
activated = function()
|
||||
props.enable_pick(not props.enable_pick())
|
||||
end,
|
||||
|
||||
create "ImageLabel" {
|
||||
Size = UDim2.fromOffset(24, 24),
|
||||
Position = UDim2.fromScale(0.5, 0.5),
|
||||
AnchorPoint = Vector2.new(0.5, 0.5),
|
||||
|
||||
BackgroundTransparency = 1,
|
||||
|
||||
ImageColor3 = ui.theme.fg_on_bg_high[3],
|
||||
|
||||
Image = "rbxassetid://10734898355"
|
||||
},
|
||||
|
||||
},
|
||||
|
||||
},
|
||||
|
||||
create "Frame" {
|
||||
Size = UDim2.new(1, 0, 0, 24),
|
||||
|
||||
BackgroundTransparency = 1,
|
||||
|
||||
Visible = function()
|
||||
return not props.ok() and #props.msg() > 0
|
||||
end,
|
||||
|
||||
ui.typography {
|
||||
text = props.msg
|
||||
},
|
||||
|
||||
},
|
||||
|
||||
create "Frame" {
|
||||
|
||||
Size = UDim2.new(1, 0, 0, 32),
|
||||
|
||||
BackgroundColor3 = ui.theme.bg[2],
|
||||
AutomaticSize = Enum.AutomaticSize.Y,
|
||||
|
||||
create "UICorner" {
|
||||
CornerRadius = UDim.new(0, 8)
|
||||
},
|
||||
|
||||
create "UIListLayout" {
|
||||
FillDirection = Enum.FillDirection.Horizontal,
|
||||
Padding = UDim.new(0, 8),
|
||||
HorizontalFlex = Enum.UIFlexAlignment.SpaceAround,
|
||||
VerticalAlignment = Enum.VerticalAlignment.Center,
|
||||
Wraps = true
|
||||
},
|
||||
|
||||
ui.row {
|
||||
spacing = UDim.new(0, 8),
|
||||
alignitems = Enum.ItemLineAlignment.Center,
|
||||
row_template,
|
||||
|
||||
ui.typography {
|
||||
text = "Page:"
|
||||
},
|
||||
|
||||
ui.textfield {
|
||||
size = UDim2.fromOffset(40, 26),
|
||||
placeholder = "1",
|
||||
|
||||
text = function()
|
||||
return page_input()
|
||||
end,
|
||||
|
||||
oninput = page_input,
|
||||
|
||||
enter = function(text)
|
||||
local n = tonumber(text)
|
||||
|
||||
if n == nil then
|
||||
page_input(tostring(page()))
|
||||
else
|
||||
page(n)
|
||||
end
|
||||
end
|
||||
},
|
||||
|
||||
ui.typography {
|
||||
text = function()
|
||||
return `/ {math.ceil(props.total_entities() / rows())}`
|
||||
end
|
||||
},
|
||||
},
|
||||
|
||||
|
||||
ui.row {
|
||||
spacing = UDim.new(0, 8),
|
||||
alignitems = Enum.ItemLineAlignment.Center,
|
||||
row_template,
|
||||
|
||||
ui.typography {
|
||||
text = "Rows:"
|
||||
},
|
||||
|
||||
ui.textfield {
|
||||
size = UDim2.fromOffset(40, 26),
|
||||
placeholder = "Rows",
|
||||
|
||||
text = function()
|
||||
return rows_input()
|
||||
end,
|
||||
|
||||
oninput = rows_input,
|
||||
|
||||
enter = function(text)
|
||||
local n = tonumber(text)
|
||||
|
||||
if n == nil then
|
||||
rows_input(tostring(rows()))
|
||||
else
|
||||
rows(n)
|
||||
end
|
||||
end
|
||||
},
|
||||
},
|
||||
|
||||
ui.row {
|
||||
spacing = UDim.new(0, 4),
|
||||
row_template,
|
||||
|
||||
ui.button {
|
||||
size = UDim2.fromOffset(26, 26),
|
||||
text = "",
|
||||
|
||||
accent = function()
|
||||
return not props.paused()
|
||||
end,
|
||||
|
||||
activated = function()
|
||||
props.paused(not props.paused())
|
||||
end,
|
||||
|
||||
{LayoutOrder = 10},
|
||||
|
||||
create "ImageLabel" {
|
||||
Size = UDim2.fromOffset(24, 24),
|
||||
Position = UDim2.fromScale(0.5, 0.5),
|
||||
AnchorPoint = Vector2.new(0.5, 0.5),
|
||||
|
||||
BackgroundTransparency = 1,
|
||||
|
||||
ImageColor3 = ui.theme.fg_on_bg_high[3],
|
||||
|
||||
Image = function()
|
||||
return if props.paused() then "rbxassetid://10735024209"
|
||||
else "rbxassetid://10734923214"
|
||||
end
|
||||
},
|
||||
|
||||
},
|
||||
|
||||
show(props.paused, function()
|
||||
return ui.button {
|
||||
size = UDim2.fromOffset(26, 26),
|
||||
text = "",
|
||||
|
||||
activated = function()
|
||||
props.refresh(true)
|
||||
end,
|
||||
|
||||
create "ImageLabel" {
|
||||
Size = UDim2.fromOffset(24, 24),
|
||||
Position = UDim2.fromScale(0.5, 0.5),
|
||||
AnchorPoint = Vector2.new(0.5, 0.5),
|
||||
|
||||
BackgroundTransparency = 1,
|
||||
|
||||
ImageColor3 = ui.theme.fg_on_bg_high[3],
|
||||
|
||||
Image = "rbxassetid://10734933222"
|
||||
},
|
||||
|
||||
}
|
||||
end)
|
||||
}
|
||||
|
||||
},
|
||||
|
||||
|
||||
ui.background {
|
||||
size = UDim2.fromScale(1, 0),
|
||||
automaticsize = Enum.AutomaticSize.Y,
|
||||
|
||||
create "UICorner" {
|
||||
CornerRadius = UDim.new(0, 8)
|
||||
},
|
||||
|
||||
create "UIFlexItem" {
|
||||
FlexMode = Enum.UIFlexMode.Fill
|
||||
},
|
||||
|
||||
ui.tablesheet {
|
||||
size = UDim2.fromScale(1, 1),
|
||||
suggested_column_sizes = { 0.1 },
|
||||
|
||||
column_sizes = function()
|
||||
local t = {}
|
||||
for i in props.columns() do
|
||||
t[i] = 200
|
||||
end
|
||||
t[1] = 50
|
||||
return t
|
||||
end,
|
||||
columns = props.columns,
|
||||
|
||||
read_value = function(c, r)
|
||||
local column = props.columns()[c]
|
||||
if not column then return "" end
|
||||
return column[r] or ""
|
||||
end,
|
||||
|
||||
on_click = function(c, r)
|
||||
if not props.columns()[1][r-1] then return end
|
||||
spawn_app.spawn_app(entity, {
|
||||
host = props.host,
|
||||
vm = props.vm,
|
||||
id = props.id,
|
||||
entity = props.columns()[1][r-1]
|
||||
})
|
||||
end,
|
||||
on_click2 = function() end,
|
||||
|
||||
below = {
|
||||
|
||||
ui.padding {
|
||||
x = UDim.new(0, 4),
|
||||
y = UDim.new(0, 2)
|
||||
},
|
||||
|
||||
create "UIListLayout" {
|
||||
FillDirection = Enum.FillDirection.Horizontal,
|
||||
VerticalAlignment = Enum.VerticalAlignment.Center,
|
||||
Padding = UDim.new(0, 8)
|
||||
},
|
||||
|
||||
ui.button {
|
||||
size = UDim2.fromOffset(70, 26),
|
||||
text = "Previous",
|
||||
|
||||
activated = function()
|
||||
page(page() - 1)
|
||||
end,
|
||||
|
||||
disabled = function()
|
||||
return page() == 1 or props.ok() == false
|
||||
end
|
||||
} :: Instance,
|
||||
|
||||
ui.button {
|
||||
size = UDim2.fromOffset(70, 26),
|
||||
text = "Next",
|
||||
|
||||
activated = function()
|
||||
page(page() + 1)
|
||||
end,
|
||||
|
||||
disabled = function()
|
||||
local max_pages = math.max(1, math.ceil(props.total_entities() / rows()))
|
||||
return page() == max_pages or props.ok() == false
|
||||
end
|
||||
} :: Instance,
|
||||
|
||||
ui.typography {
|
||||
position = UDim2.new(0, 4, 0.5, 0),
|
||||
anchorpoint = Vector2.new(0, 0.5),
|
||||
|
||||
text = function()
|
||||
return `total entities: {props.total_entities()}\tfrom: {props.from()}\tuntil: {props.upto()}`
|
||||
end,
|
||||
} :: Instance,
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
end
|
||||
81
modules/Jabby/client/apps/system/init.luau
Normal file
81
modules/Jabby/client/apps/system/init.luau
Normal file
|
|
@ -0,0 +1,81 @@
|
|||
local RunService = game:GetService("RunService")
|
||||
|
||||
local vide = require(script.Parent.Parent.Parent.vide)
|
||||
local loop = require(script.Parent.Parent.Parent.modules.loop)
|
||||
local remotes = require(script.Parent.Parent.Parent.modules.remotes)
|
||||
local types = require(script.Parent.Parent.Parent.modules.types)
|
||||
local widget = require(script.widget)
|
||||
|
||||
local cleanup = vide.cleanup
|
||||
|
||||
local system = {
|
||||
class_name = "app" :: "app",
|
||||
name = "System"
|
||||
}
|
||||
|
||||
type props = {
|
||||
host: number,
|
||||
vm: number,
|
||||
scheduler: number,
|
||||
system: number,
|
||||
|
||||
name: string
|
||||
|
||||
}
|
||||
|
||||
function system.mount(props: props, destroy: () -> ())
|
||||
|
||||
local watch_id = math.random(2^31 - 1)
|
||||
local recording = vide.source(false)
|
||||
local watching_frame = vide.source(0)
|
||||
local per_frame_data = vide.source({} :: {[number]: number})
|
||||
local changes = vide.source({
|
||||
component = {},
|
||||
entities = {},
|
||||
types = {},
|
||||
values = {}
|
||||
} :: types.WatchLoggedChanges)
|
||||
|
||||
local system_props_data = {
|
||||
watch_id = watch_id,
|
||||
host = props.host,
|
||||
vm = props.vm,
|
||||
scheduler = props.scheduler,
|
||||
system = props.system,
|
||||
name = props.name,
|
||||
|
||||
changes = changes,
|
||||
recording = recording,
|
||||
per_frame_data = per_frame_data,
|
||||
watching_frame = watching_frame,
|
||||
destroy = destroy,
|
||||
}
|
||||
|
||||
local app_loop = loop (
|
||||
"app-client-system",
|
||||
system_props_data,
|
||||
|
||||
{i = 1},
|
||||
script.systems.replicate
|
||||
)
|
||||
|
||||
local outgoing: types.OutgoingConnector = {
|
||||
host = system_props_data.host,
|
||||
to_vm = system_props_data.vm
|
||||
}
|
||||
|
||||
remotes.create_watch:fire(outgoing, props.scheduler, props.system, watch_id)
|
||||
remotes.connect_watch:fire(outgoing, watch_id)
|
||||
|
||||
cleanup(RunService.Heartbeat:Connect(app_loop))
|
||||
cleanup(function()
|
||||
remotes.disconnect_watch:fire(outgoing, watch_id)
|
||||
remotes.stop_watch:fire(outgoing, watch_id)
|
||||
remotes.remove_watch:fire(outgoing, watch_id)
|
||||
end)
|
||||
|
||||
return widget(system_props_data)
|
||||
|
||||
end
|
||||
|
||||
return system
|
||||
97
modules/Jabby/client/apps/system/systems/replicate.luau
Normal file
97
modules/Jabby/client/apps/system/systems/replicate.luau
Normal file
|
|
@ -0,0 +1,97 @@
|
|||
local vide = require(script.Parent.Parent.Parent.Parent.Parent.vide)
|
||||
local queue = require(script.Parent.Parent.Parent.Parent.Parent.modules.queue)
|
||||
local remotes = require(script.Parent.Parent.Parent.Parent.Parent.modules.remotes)
|
||||
local types = require(script.Parent.Parent.Parent.Parent.Parent.modules.types)
|
||||
|
||||
local batch = vide.batch
|
||||
|
||||
type Context = {
|
||||
host: "server" | Player,
|
||||
vm: number,
|
||||
|
||||
watch_id: number,
|
||||
scheduler: number,
|
||||
system: number,
|
||||
name: string,
|
||||
|
||||
recording: vide.Source<boolean>,
|
||||
watching_frame: vide.Source<number>,
|
||||
per_frame_data: vide.Source<{[number]: number}>,
|
||||
changes: (types.WatchLoggedChanges) -> (),
|
||||
}
|
||||
|
||||
return function(context: Context)
|
||||
|
||||
local watch_id = context.watch_id
|
||||
local outgoing: types.OutgoingConnector = {
|
||||
host = context.host,
|
||||
to_vm = context.vm
|
||||
}
|
||||
|
||||
local recording_state_changed = false
|
||||
local recording = false
|
||||
|
||||
vide.effect(function()
|
||||
recording_state_changed = true
|
||||
recording = context.recording()
|
||||
end)
|
||||
|
||||
local watching_frame_changed = false
|
||||
local watching_frame = 1
|
||||
vide.effect(function()
|
||||
watching_frame_changed = true
|
||||
watching_frame = context.watching_frame()
|
||||
end)
|
||||
|
||||
local receive_update_data = queue(remotes.update_watch_data)
|
||||
local receive_overview = queue(remotes.update_overview)
|
||||
|
||||
return function()
|
||||
|
||||
if recording_state_changed and recording then
|
||||
remotes.start_record_watch:fire(outgoing, watch_id)
|
||||
recording_state_changed = false
|
||||
elseif recording_state_changed and not recording then
|
||||
remotes.stop_watch:fire(outgoing, watch_id)
|
||||
recording_state_changed = false
|
||||
end
|
||||
|
||||
if watching_frame_changed then
|
||||
remotes.request_watch_data:fire(outgoing, watch_id, watching_frame)
|
||||
watching_frame_changed = false
|
||||
end
|
||||
|
||||
debug.profilebegin("receive update data")
|
||||
batch(function()
|
||||
for from, watch, frame, changes in receive_update_data:iter() do
|
||||
if watch ~= watch_id then continue end
|
||||
if frame ~= watching_frame then continue end
|
||||
if changes == nil then
|
||||
context.changes({
|
||||
types = {},
|
||||
entities = {},
|
||||
component = {},
|
||||
values = {},
|
||||
worlds = {}
|
||||
})
|
||||
else
|
||||
context.changes(changes)
|
||||
end
|
||||
end
|
||||
end)
|
||||
debug.profileend()
|
||||
|
||||
debug.profilebegin("receive overview")
|
||||
batch(function()
|
||||
for from, watch, frame, value in receive_overview:iter() do
|
||||
if watch ~= watch_id then continue end
|
||||
local data = context.per_frame_data()
|
||||
if data[frame] == value then continue end
|
||||
data[frame] = value
|
||||
context.per_frame_data(data)
|
||||
end
|
||||
end)
|
||||
debug.profileend()
|
||||
|
||||
end
|
||||
end
|
||||
235
modules/Jabby/client/apps/system/watch_tracker.luau
Normal file
235
modules/Jabby/client/apps/system/watch_tracker.luau
Normal file
|
|
@ -0,0 +1,235 @@
|
|||
local ui = require(script.Parent.Parent.Parent.Parent.ui)
|
||||
local vide = require(script.Parent.Parent.Parent.Parent.vide)
|
||||
local types = require(script.Parent.Parent.Parent.Parent.modules.types)
|
||||
local tooltip = require(script.Parent.Parent.Parent.components.tooltip)
|
||||
local virtualscroller_horizontal = require(script.Parent.Parent.Parent.components.virtualscroller_horizontal)
|
||||
|
||||
local create = vide.create
|
||||
local source = vide.source
|
||||
|
||||
type props = {
|
||||
host: "server" | Player,
|
||||
vm: number,
|
||||
|
||||
scheduler: number,
|
||||
system: number,
|
||||
name: string,
|
||||
|
||||
recording: vide.Source<boolean>,
|
||||
watching_frame: vide.Source<number>,
|
||||
per_frame_data: () -> {[number]: number},
|
||||
changes: () -> types.WatchLoggedChanges,
|
||||
}
|
||||
|
||||
local PROFILER_THICKNESS = 6
|
||||
|
||||
return function(props: props)
|
||||
|
||||
local is_recording = props.recording
|
||||
local watching_frame = props.watching_frame
|
||||
local per_frame_data = props.per_frame_data
|
||||
local changes = props.changes
|
||||
|
||||
local function sheet_changes()
|
||||
local changes = changes()
|
||||
|
||||
return {
|
||||
{"type", unpack(changes.types)},
|
||||
{"entity", unpack(changes.entities)},
|
||||
{"component", unpack(changes.component)},
|
||||
{"value", unpack(changes.values)}
|
||||
}
|
||||
end
|
||||
|
||||
local function max()
|
||||
return math.max(1, unpack(per_frame_data()))
|
||||
end
|
||||
|
||||
local function total_changes()
|
||||
local sum = 0
|
||||
for _, value in per_frame_data() do
|
||||
sum += value
|
||||
end
|
||||
return sum
|
||||
end
|
||||
|
||||
local hovering_over = source(false)
|
||||
|
||||
return ui.list {
|
||||
justifycontent = Enum.UIFlexAlignment.SpaceEvenly,
|
||||
spacing = UDim.new(0, 4),
|
||||
|
||||
ui.pane {
|
||||
virtualscroller_horizontal {
|
||||
item_size = PROFILER_THICKNESS,
|
||||
item = function(index)
|
||||
local function value()
|
||||
return per_frame_data()[index()] or 0
|
||||
end
|
||||
|
||||
return create "TextButton" {
|
||||
Size = function()
|
||||
return UDim2.new(0, PROFILER_THICKNESS, 1, 0)
|
||||
end,
|
||||
BackgroundTransparency = function()
|
||||
return if hovering_over() == index() then 0.5 else 1
|
||||
end,
|
||||
BackgroundColor3 = ui.theme.bg[10],
|
||||
|
||||
MouseEnter = function()
|
||||
hovering_over(index())
|
||||
end,
|
||||
MouseLeave = function()
|
||||
if hovering_over() ~= index() then return end
|
||||
hovering_over(false)
|
||||
end,
|
||||
Activated = function()
|
||||
watching_frame(index())
|
||||
end,
|
||||
AutoLocalize = false,
|
||||
|
||||
create "Frame" {
|
||||
Size = function()
|
||||
return UDim2.fromScale(1, value() / max())
|
||||
end,
|
||||
Position = UDim2.fromScale(1, 1),
|
||||
AnchorPoint = Vector2.new(1, 1),
|
||||
BackgroundColor3 = function()
|
||||
return if watching_frame() == index() then
|
||||
ui.theme.acc[20]()
|
||||
elseif hovering_over() == index() then
|
||||
ui.theme.acc[5]()
|
||||
else ui.theme.acc[0]()
|
||||
end,
|
||||
}
|
||||
|
||||
}
|
||||
end,
|
||||
max_items = function()
|
||||
return #per_frame_data()
|
||||
end,
|
||||
|
||||
create "UIStroke" {Color = ui.theme.bg[-3]},
|
||||
|
||||
{
|
||||
Size = UDim2.new(1, 0, 0, 56),
|
||||
HorizontalScrollBarInset = Enum.ScrollBarInset.ScrollBar,
|
||||
BackgroundColor3 = ui.theme.bg[-1],
|
||||
ScrollBarThickness = 6,
|
||||
|
||||
CanvasPosition = function()
|
||||
per_frame_data()
|
||||
return Vector2.new(table.maxn(per_frame_data()) * PROFILER_THICKNESS)
|
||||
end,
|
||||
}
|
||||
},
|
||||
|
||||
ui.typography {
|
||||
text = function()
|
||||
return `Recorded {#per_frame_data()} frames and tracked {total_changes()} changes`
|
||||
end
|
||||
},
|
||||
|
||||
ui.typography {
|
||||
text = function()
|
||||
return `Currently viewing frame {watching_frame()}`
|
||||
end
|
||||
}
|
||||
},
|
||||
|
||||
tooltip {
|
||||
transparency = 0,
|
||||
visible = function()
|
||||
return hovering_over() ~= false
|
||||
end,
|
||||
|
||||
ui.typography {
|
||||
automaticsize = Enum.AutomaticSize.XY,
|
||||
|
||||
text = function()
|
||||
return `\z
|
||||
Frame: #{hovering_over()}\n\z
|
||||
Changes: {per_frame_data()[hovering_over()] or 0}`
|
||||
end,
|
||||
xalignment = Enum.TextXAlignment.Left,
|
||||
wrapped = true
|
||||
}
|
||||
},
|
||||
|
||||
ui.container {
|
||||
Size = UDim2.fromScale(1, 0),
|
||||
AutomaticSize = Enum.AutomaticSize.Y,
|
||||
BackgroundColor3 = ui.theme.bg[1],
|
||||
BackgroundTransparency = 0,
|
||||
|
||||
create "UIListLayout" {
|
||||
FillDirection = Enum.FillDirection.Horizontal,
|
||||
HorizontalFlex = Enum.UIFlexAlignment.SpaceEvenly,
|
||||
VerticalFlex = Enum.UIFlexAlignment.Fill,
|
||||
Padding = UDim.new(0, 8)
|
||||
},
|
||||
|
||||
ui.padding {
|
||||
x = UDim.new(0, 4),
|
||||
y = UDim.new(0, 4)
|
||||
},
|
||||
|
||||
ui.button {
|
||||
size = UDim2.new(0, 80, 0, 30),
|
||||
automaticsize = Enum.AutomaticSize.X,
|
||||
text = function()
|
||||
return if is_recording() then "Pause" else "Record"
|
||||
end,
|
||||
|
||||
activated = function()
|
||||
is_recording(not is_recording())
|
||||
end
|
||||
},
|
||||
|
||||
ui.container {
|
||||
Size = UDim2.fromScale(0, 0),
|
||||
|
||||
create "UIFlexItem" {
|
||||
FlexMode = Enum.UIFlexMode.Fill
|
||||
},
|
||||
|
||||
ui.textfield {
|
||||
size = UDim2.new(1, 0, 1, 0),
|
||||
text = tostring(watching_frame()),
|
||||
placeholder = "frame",
|
||||
|
||||
enter = function(text: string)
|
||||
if tonumber(text) == nil then return end
|
||||
watching_frame(tonumber(text))
|
||||
end
|
||||
}
|
||||
},
|
||||
|
||||
},
|
||||
|
||||
ui.container {
|
||||
Size = UDim2.fromScale(1, 1),
|
||||
|
||||
create "UIFlexItem" {
|
||||
FlexMode = Enum.UIFlexMode.Fill
|
||||
},
|
||||
|
||||
ui.tablesheet {
|
||||
size = UDim2.fromScale(1, 1),
|
||||
|
||||
column_sizes = source {100, 80, 100, 200},
|
||||
read_value = function(column, row)
|
||||
local v = sheet_changes()[column][row]
|
||||
return if v == false then "" else v
|
||||
end,
|
||||
|
||||
on_click = function() end,
|
||||
on_click2 = function() end,
|
||||
|
||||
columns = sheet_changes
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
end
|
||||
34
modules/Jabby/client/apps/system/widget.luau
Normal file
34
modules/Jabby/client/apps/system/widget.luau
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
local ui = require(script.Parent.Parent.Parent.Parent.ui)
|
||||
local vide = require(script.Parent.Parent.Parent.Parent.vide)
|
||||
local types = require(script.Parent.Parent.Parent.Parent.modules.types)
|
||||
local watch_tracker = require(script.Parent.watch_tracker)
|
||||
|
||||
type props = {
|
||||
host: "server" | Player,
|
||||
vm: number,
|
||||
|
||||
scheduler: number,
|
||||
system: number,
|
||||
name: string,
|
||||
|
||||
destroy: () -> (),
|
||||
|
||||
recording: vide.Source<boolean>,
|
||||
watching_frame: vide.Source<number>,
|
||||
per_frame_data: () -> {[number]: number},
|
||||
changes: () -> types.WatchLoggedChanges,
|
||||
}
|
||||
|
||||
return function(props: props)
|
||||
|
||||
return ui.widget {
|
||||
title = `system - {props.name}`,
|
||||
subtitle = `host: {props.host} vm: {props.vm} scheduler: {props.scheduler} system: {props.system}`,
|
||||
bind_to_close = props.destroy,
|
||||
size = Vector2.new(350, 400),
|
||||
min_size = Vector2.new(300, 300),
|
||||
|
||||
watch_tracker(props)
|
||||
}
|
||||
|
||||
end
|
||||
60
modules/Jabby/client/components/tooltip.luau
Normal file
60
modules/Jabby/client/components/tooltip.luau
Normal file
|
|
@ -0,0 +1,60 @@
|
|||
local RunService = game:GetService("RunService")
|
||||
local UserInputService = game:GetService("UserInputService")
|
||||
|
||||
local ui = require(script.Parent.Parent.Parent.ui)
|
||||
local vide = require(script.Parent.Parent.Parent.vide)
|
||||
|
||||
local create = vide.create
|
||||
local source = vide.source
|
||||
local cleanup = vide.cleanup
|
||||
|
||||
type props = {
|
||||
visible: boolean | () -> boolean,
|
||||
transparency: number? | () -> number,
|
||||
[number]: any
|
||||
}
|
||||
|
||||
return function(props: props)
|
||||
|
||||
local mouse_location = source(Vector2.zero)
|
||||
|
||||
cleanup(RunService.PreRender:Connect(function()
|
||||
mouse_location(UserInputService:GetMouseLocation())
|
||||
end))
|
||||
|
||||
return create "ScreenGui" {
|
||||
Name = "Mouse Hover",
|
||||
IgnoreGuiInset = true,
|
||||
DisplayOrder = 1e9,
|
||||
Enabled = props.visible,
|
||||
|
||||
create "Frame" {
|
||||
Position = function()
|
||||
return UDim2.fromOffset(
|
||||
mouse_location().X + 24,
|
||||
mouse_location().Y + 24
|
||||
)
|
||||
end,
|
||||
Size = UDim2.fromOffset(400, 0),
|
||||
AutomaticSize = Enum.AutomaticSize.XY,
|
||||
BackgroundColor3 = ui.theme.bg[0],
|
||||
BackgroundTransparency = props.transparency or 0.5,
|
||||
|
||||
ui.padding {},
|
||||
|
||||
create "UICorner" {
|
||||
CornerRadius = UDim.new(0, 8)
|
||||
},
|
||||
|
||||
create "UIStroke" {
|
||||
Color = ui.theme.bg[-10],
|
||||
Thickness = 2,
|
||||
Transparency = 0.8
|
||||
},
|
||||
|
||||
unpack(props)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
end
|
||||
185
modules/Jabby/client/components/virtualscroller_horizontal.luau
Normal file
185
modules/Jabby/client/components/virtualscroller_horizontal.luau
Normal file
|
|
@ -0,0 +1,185 @@
|
|||
local ui = require(script.Parent.Parent.Parent.ui)
|
||||
local vide = require(script.Parent.Parent.Parent.vide)
|
||||
|
||||
local create = vide.create
|
||||
local source = vide.source
|
||||
local values = vide.values
|
||||
local changed = vide.changed
|
||||
local effect = vide.effect
|
||||
local untrack = vide.untrack
|
||||
|
||||
type can<T> = T | () -> T
|
||||
type props = {
|
||||
|
||||
size: can<UDim2>?,
|
||||
position: can<UDim2>?,
|
||||
anchorpoint: can<UDim2>?,
|
||||
|
||||
--- streams in items. when index is -1, should expect to be unused
|
||||
item: (index: () -> number) -> Instance,
|
||||
--- streams in separators. when index is -1, should expect to be unused
|
||||
separator: ((index: () -> number) -> Instance)?,
|
||||
|
||||
item_size: number,
|
||||
separator_size: number?,
|
||||
|
||||
max_items: (() -> number)?,
|
||||
|
||||
[number]: any
|
||||
|
||||
}
|
||||
|
||||
return function(props: props)
|
||||
|
||||
local items = source({} :: {vide.Source<number>})
|
||||
|
||||
local absolute_size = source(Vector2.zero)
|
||||
local canvas_position = source(Vector2.zero)
|
||||
|
||||
local item_size = props.item_size
|
||||
local separator_size = props.separator_size or 0
|
||||
|
||||
local item = props.item
|
||||
local separator = props.separator
|
||||
|
||||
local OVERFLOW_ONE_SIDE = 4
|
||||
|
||||
effect(function()
|
||||
local absolute_size = absolute_size()
|
||||
local canvas_position = canvas_position()
|
||||
|
||||
local child_size = item_size + separator_size
|
||||
local total_required = math.ceil(absolute_size.X / child_size) + OVERFLOW_ONE_SIDE * 2
|
||||
local sources = untrack(items)
|
||||
|
||||
local min_index = math.floor(canvas_position.X / child_size)
|
||||
local max_index = math.ceil((canvas_position.X + absolute_size.X) / child_size)
|
||||
|
||||
local max_items = math.huge
|
||||
if props.max_items then
|
||||
max_items = props.max_items()
|
||||
end
|
||||
|
||||
untrack(function()
|
||||
-- mark any sources out of range as unused
|
||||
local unused = {}
|
||||
|
||||
for i, s in sources do
|
||||
local index = s()
|
||||
|
||||
if
|
||||
index >= math.max(min_index, 1)
|
||||
and index <= math.min(max_index, max_items)
|
||||
then continue end
|
||||
unused[i] = true
|
||||
s(-1)
|
||||
end
|
||||
|
||||
-- add sources necessary
|
||||
if #sources < total_required then
|
||||
for i = #sources + 1, total_required do
|
||||
sources[i] = source(-1)
|
||||
unused[i] = true
|
||||
end
|
||||
items(sources)
|
||||
end
|
||||
|
||||
-- update indexes of any sources that went unused
|
||||
local did_not_render = {}
|
||||
|
||||
for i = math.max(min_index, 1), math.min(max_index, max_items) do
|
||||
did_not_render[i] = true
|
||||
end
|
||||
|
||||
|
||||
for _, s in sources do
|
||||
did_not_render[s()] = nil
|
||||
end
|
||||
|
||||
for index in unused do
|
||||
local s = sources[index]
|
||||
local key = next(did_not_render)
|
||||
if not key then break end
|
||||
s(key)
|
||||
did_not_render[key] = nil
|
||||
unused[index] = nil
|
||||
end
|
||||
|
||||
if next(did_not_render) then warn("missing source!", next(did_not_render)) end
|
||||
|
||||
-- remove unnecessary sources
|
||||
if #sources > total_required then
|
||||
for i = #sources, 1, -1 do
|
||||
if unused[i] then
|
||||
table.remove(sources, i)
|
||||
end
|
||||
unused[i] = nil
|
||||
if #sources < total_required then break end
|
||||
end
|
||||
items(sources)
|
||||
end
|
||||
|
||||
end)
|
||||
|
||||
end)
|
||||
|
||||
return create "ScrollingFrame" {
|
||||
|
||||
Size = props.size or UDim2.fromScale(1, 1),
|
||||
Position = props.position,
|
||||
AnchorPoint = props.anchorpoint,
|
||||
|
||||
BackgroundTransparency = 1,
|
||||
|
||||
CanvasSize = function()
|
||||
if props.max_items then
|
||||
return UDim2.fromOffset(props.max_items() * (item_size + separator_size), 0)
|
||||
else
|
||||
local absolute_size = absolute_size()
|
||||
local canvas_position = canvas_position()
|
||||
local child_size = item_size + separator_size
|
||||
local max_index = math.ceil((canvas_position.X + absolute_size.X) / child_size) + OVERFLOW_ONE_SIDE
|
||||
return UDim2.fromOffset(max_index * child_size, 0)
|
||||
end
|
||||
end,
|
||||
|
||||
values(items, function(index)
|
||||
return create "Frame" {
|
||||
Name = index,
|
||||
|
||||
Position = function()
|
||||
if index() == -1 then UDim2.fromOffset(0, -1000) end
|
||||
return UDim2.fromOffset(
|
||||
(item_size + separator_size) * (index() - 1),
|
||||
0
|
||||
)
|
||||
end,
|
||||
Size = UDim2.new(0, item_size + separator_size, 1, 0),
|
||||
|
||||
BackgroundTransparency = 1,
|
||||
|
||||
ui.container {
|
||||
Name = "Item",
|
||||
|
||||
item(index),
|
||||
},
|
||||
|
||||
if separator then
|
||||
ui.container {
|
||||
Name = "Separator",
|
||||
|
||||
separator(index)
|
||||
}
|
||||
else nil,
|
||||
|
||||
}
|
||||
end),
|
||||
|
||||
changed("AbsoluteSize", absolute_size),
|
||||
changed("CanvasPosition", canvas_position),
|
||||
|
||||
unpack(props),
|
||||
|
||||
}
|
||||
|
||||
end
|
||||
19
modules/Jabby/client/init.luau
Normal file
19
modules/Jabby/client/init.luau
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
local entity = require(script.apps.entity)
|
||||
local home = require(script.apps.home)
|
||||
local overview_scheduler = require(script.apps.overview_scheduler)
|
||||
local registry = require(script.apps.registry)
|
||||
local spawn_app = require(script.spawn_app)
|
||||
|
||||
return {
|
||||
|
||||
apps = {
|
||||
home = home,
|
||||
entity = entity,
|
||||
scheduler = overview_scheduler,
|
||||
registry = registry,
|
||||
},
|
||||
|
||||
spawn_app = spawn_app.spawn_app,
|
||||
unmount_all = spawn_app.unmount_all
|
||||
|
||||
}
|
||||
36
modules/Jabby/client/spawn_app.luau
Normal file
36
modules/Jabby/client/spawn_app.luau
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
local Players = game:GetService("Players")
|
||||
|
||||
local vide = require(script.Parent.Parent.vide)
|
||||
local types = require(script.Parent.Parent.modules.types)
|
||||
|
||||
local destroy_fn = {}
|
||||
|
||||
local function unmount_all()
|
||||
for destroy in destroy_fn do
|
||||
destroy()
|
||||
end
|
||||
end
|
||||
|
||||
local function spawn_app<T>(app: types.Application<T>, props: T): () -> ()
|
||||
return vide.root(function(destroy)
|
||||
|
||||
local destroy = function()
|
||||
destroy_fn[destroy] = nil
|
||||
destroy()
|
||||
end
|
||||
|
||||
local application = app.mount(props, destroy)
|
||||
application.Parent = Players.LocalPlayer.PlayerGui
|
||||
|
||||
vide.cleanup(application)
|
||||
|
||||
destroy_fn[destroy] = true
|
||||
|
||||
return destroy
|
||||
end)
|
||||
end
|
||||
|
||||
return {
|
||||
unmount_all = unmount_all,
|
||||
spawn_app = spawn_app
|
||||
}
|
||||
46
modules/Jabby/examples/example.luau
Normal file
46
modules/Jabby/examples/example.luau
Normal file
|
|
@ -0,0 +1,46 @@
|
|||
local RunService = game:GetService("RunService")
|
||||
|
||||
local jecs = require("@jecs")
|
||||
local jabby = require("@modules/jabby")
|
||||
|
||||
local world = jecs.world()
|
||||
|
||||
jabby.set_check_function(function()
|
||||
return true
|
||||
end)
|
||||
|
||||
local scheduler = jabby.scheduler.create()
|
||||
|
||||
jabby.register({
|
||||
applet = jabby.applets.scheduler,
|
||||
name = "Example Scheduler",
|
||||
configuration = {
|
||||
scheduler = scheduler,
|
||||
},
|
||||
})
|
||||
|
||||
jabby.register({
|
||||
applet = jabby.applets.world,
|
||||
name = "Example World",
|
||||
configuration = {
|
||||
world = world,
|
||||
},
|
||||
})
|
||||
|
||||
local system_id = scheduler:register_system({
|
||||
name = "example_system",
|
||||
module = script,
|
||||
})
|
||||
|
||||
local function example_system(_world: jecs.World, dt: number)
|
||||
return dt
|
||||
end
|
||||
|
||||
if RunService:IsClient() then
|
||||
local client = jabby.obtain_client()
|
||||
client.spawn_app(client.apps.home, nil)
|
||||
end
|
||||
|
||||
RunService.Heartbeat:Connect(function(dt)
|
||||
scheduler:run(system_id, example_system, world, dt)
|
||||
end)
|
||||
4115
modules/Jabby/jecs.luau
Normal file
4115
modules/Jabby/jecs.luau
Normal file
File diff suppressed because it is too large
Load diff
60
modules/Jabby/module.luau
Normal file
60
modules/Jabby/module.luau
Normal file
|
|
@ -0,0 +1,60 @@
|
|||
local jecs = require(script.Parent.jecs)
|
||||
local traffic_check = require(script.Parent.modules.traffic_check)
|
||||
local types = require(script.Parent.modules.types)
|
||||
local vm_id = require(script.Parent.modules.vm_id)
|
||||
local server = require(script.Parent.server)
|
||||
local public = require(script.Parent.server.public)
|
||||
local scheduler = require(script.Parent.server.scheduler)
|
||||
|
||||
type Applet<T> = {
|
||||
add_to_public: (name: string, config: T) -> ()
|
||||
}
|
||||
|
||||
local world_applet = {
|
||||
add_to_public = function(
|
||||
name: string, config: { world: jecs.World, entities: {[Instance]: jecs.Entity<any>}?, get_entity_from_part: ((part: BasePart) -> (jecs.Entity<any>, Part?))? }
|
||||
)
|
||||
public.updated = true
|
||||
table.insert(public, {
|
||||
class_name = "World",
|
||||
name = name,
|
||||
world = config.world,
|
||||
entities = config.entities,
|
||||
get_entity_from_part = config.get_entity_from_part
|
||||
})
|
||||
end
|
||||
}
|
||||
|
||||
local scheduler_applet = {
|
||||
add_to_public = function(
|
||||
name: string, config: { scheduler: types.Scheduler }
|
||||
)
|
||||
public.updated = true
|
||||
config.scheduler.name = name
|
||||
table.insert(public, config.scheduler)
|
||||
end
|
||||
}
|
||||
|
||||
return {
|
||||
set_check_function = function(callback: (Player) -> boolean)
|
||||
traffic_check.can_use_jabby = callback
|
||||
end,
|
||||
|
||||
obtain_client = function()
|
||||
return require(script.Parent.client)
|
||||
end,
|
||||
|
||||
vm_id = vm_id,
|
||||
scheduler = scheduler,
|
||||
|
||||
broadcast_server = server.broadcast,
|
||||
|
||||
applets = {
|
||||
world = world_applet,
|
||||
scheduler = scheduler_applet
|
||||
},
|
||||
|
||||
register = function<T>(info: { name: string, applet: Applet<T>, configuration: T })
|
||||
info.applet.add_to_public(info.name, info.configuration)
|
||||
end,
|
||||
}
|
||||
11
modules/Jabby/modules/average.luau
Normal file
11
modules/Jabby/modules/average.luau
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
|
||||
|
||||
local function average(n: {number})
|
||||
local sum = 0
|
||||
for i, v in n do
|
||||
sum += v
|
||||
end
|
||||
return sum / #n
|
||||
end
|
||||
|
||||
return average
|
||||
39
modules/Jabby/modules/convert_units.luau
Normal file
39
modules/Jabby/modules/convert_units.luau
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
local function convert_units(unit: string, value: number): (string)
|
||||
local s = math.sign(value)
|
||||
value = math.abs(value)
|
||||
local prefixes = {
|
||||
[4] = "T",
|
||||
[3] ="G",
|
||||
[2] ="M",
|
||||
[1] = "k",
|
||||
[0] = " ",
|
||||
[-1] = "m",
|
||||
[-2] = "u",
|
||||
[-3] = "n",
|
||||
[-4] = "p"
|
||||
}
|
||||
|
||||
local order = 0
|
||||
|
||||
while value >= 1000 do
|
||||
order += 1
|
||||
value /= 1000
|
||||
end
|
||||
|
||||
while value ~= 0 and value < 1 do
|
||||
order -= 1
|
||||
value *= 1000
|
||||
end
|
||||
|
||||
if value >= 100 then
|
||||
value = math.floor(value)
|
||||
elseif value >= 10 then
|
||||
value = math.floor(value * 1e1) / 1e1
|
||||
elseif value >= 1 then
|
||||
value = math.floor(value * 1e2) / 1e2
|
||||
end
|
||||
|
||||
return value * s .. prefixes[order] .. unit
|
||||
end
|
||||
|
||||
return convert_units
|
||||
5
modules/Jabby/modules/hash_connector.luau
Normal file
5
modules/Jabby/modules/hash_connector.luau
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
local types = require(script.Parent.types)
|
||||
|
||||
return function(connector: types.IncomingConnector | types.OutgoingConnector)
|
||||
return `{connector.host}\0{connector.from_vm or connector.to_vm}`
|
||||
end
|
||||
1044
modules/Jabby/modules/lon.luau
Normal file
1044
modules/Jabby/modules/lon.luau
Normal file
File diff suppressed because it is too large
Load diff
107
modules/Jabby/modules/loop.luau
Normal file
107
modules/Jabby/modules/loop.luau
Normal file
|
|
@ -0,0 +1,107 @@
|
|||
local scheduler = require(script.Parent.Parent.server.scheduler)
|
||||
type Array<T> = { T }
|
||||
|
||||
export type System = (any, number) -> ...(any, number) -> ()
|
||||
|
||||
type GroupInfo = { i: number?, o: number? }
|
||||
|
||||
type SystemGroup = {
|
||||
interval: number,
|
||||
offset: number,
|
||||
dt: number,
|
||||
[number]: {
|
||||
id: number,
|
||||
name: string,
|
||||
type: number,
|
||||
fn: (...any) -> ...any
|
||||
}
|
||||
}
|
||||
|
||||
local function loop_create(name: string, data: any, ...: ModuleScript | () -> () | GroupInfo)
|
||||
local jabby_scheduler = scheduler.create(name)
|
||||
|
||||
local groups = {} :: Array<SystemGroup>
|
||||
|
||||
local current_group: SystemGroup?
|
||||
|
||||
local function process_systems(array: Array<any>)
|
||||
for i, v in array do
|
||||
if type(v) == "table" then
|
||||
if v.i then
|
||||
if current_group then
|
||||
table.insert(groups, current_group)
|
||||
end
|
||||
|
||||
current_group = {
|
||||
interval = v.i or 1,
|
||||
offset = v.o or 0,
|
||||
dt = 0
|
||||
}
|
||||
else
|
||||
process_systems(v)
|
||||
end
|
||||
elseif type(v) == "function" then
|
||||
assert(current_group)
|
||||
|
||||
table.insert(current_group, {
|
||||
id = jabby_scheduler:register_system(),
|
||||
name = "UNNAMED",
|
||||
type = 0,
|
||||
fn = v
|
||||
})
|
||||
else
|
||||
assert(current_group)
|
||||
|
||||
local fn = (require :: any)(v) :: System
|
||||
local fn2 = fn(data, 0)
|
||||
|
||||
table.insert(current_group, {
|
||||
id = jabby_scheduler:register_system({name = `{v.Name}`}),
|
||||
name = v.Name,
|
||||
type = fn2 and 1 or 0,
|
||||
fn = fn2 or fn
|
||||
})
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
process_systems { ... }
|
||||
|
||||
assert(current_group)
|
||||
table.insert(groups, current_group)
|
||||
current_group = nil
|
||||
|
||||
local frame_count = 0
|
||||
|
||||
return function(dt)
|
||||
frame_count += 1
|
||||
|
||||
debug.profilebegin("ECS LOOP")
|
||||
|
||||
for _, group in groups do
|
||||
group.dt += dt
|
||||
|
||||
if frame_count % group.interval == group.offset then
|
||||
for _, system in ipairs(group) do
|
||||
debug.setmemorycategory(system.name)
|
||||
debug.profilebegin(system.name)
|
||||
|
||||
if system.type == 0 then
|
||||
jabby_scheduler:run(system.id, system.fn, data, group.dt)
|
||||
else
|
||||
jabby_scheduler:run(system.id, system.fn, group.dt)
|
||||
end
|
||||
|
||||
debug.profileend()
|
||||
end
|
||||
|
||||
group.dt = 0
|
||||
end
|
||||
end
|
||||
|
||||
debug.resetmemorycategory()
|
||||
debug.profileend()
|
||||
end, jabby_scheduler
|
||||
end
|
||||
|
||||
return loop_create
|
||||
202
modules/Jabby/modules/net.luau
Normal file
202
modules/Jabby/modules/net.luau
Normal file
|
|
@ -0,0 +1,202 @@
|
|||
--[[
|
||||
|
||||
net is a utility library designed to handle connections to other actors and
|
||||
the server for me.
|
||||
|
||||
]]
|
||||
|
||||
local Players = game:GetService("Players")
|
||||
local ReplicatedStorage = game:GetService("ReplicatedStorage")
|
||||
local RunService = game:GetService("RunService")
|
||||
|
||||
local signal = require(script.Parent.signal)
|
||||
local vm_id = require(script.Parent.vm_id)
|
||||
local traffic_check = require(script.Parent.traffic_check)
|
||||
local types = require(script.Parent.types)
|
||||
|
||||
local local_host: "server" | Player
|
||||
local MANAGER_VM = 0
|
||||
|
||||
if RunService:IsServer() then
|
||||
local_host = "server"
|
||||
else
|
||||
local_host = Players.LocalPlayer
|
||||
end
|
||||
|
||||
local function tincoming_connector(t: any): boolean
|
||||
if typeof(t) ~= "table" then return false end
|
||||
if not (t.host == "server" or (typeof(t.host) == "Instance" and t.host:IsA("Player"))) then return false end
|
||||
if typeof(t.from_vm) ~= "number" then return false end
|
||||
if t.to_vm ~= nil and typeof(t.to_vm) ~= "number" then return false end
|
||||
|
||||
return true
|
||||
end
|
||||
|
||||
local NAME = "JABBY_REMOTES"
|
||||
local folder: Instance
|
||||
local created_folder = false
|
||||
if RunService:IsServer() then
|
||||
local existing = ReplicatedStorage:FindFirstChild(NAME)
|
||||
if existing then
|
||||
folder = existing
|
||||
else
|
||||
created_folder = true
|
||||
warn("\z
|
||||
There's a bug with jabby that sometimes causes the JABBY_REMOTES folder to not replicate very rarely. \z
|
||||
Unfortunately, I still haven't thought of a fix yet -,- so please instead clone the JABBY_REMOTES \z
|
||||
folder into your game which should stop it. make sure to set archivable to true!")
|
||||
folder = Instance.new("Folder")
|
||||
folder.Name = NAME
|
||||
folder.Archivable = false
|
||||
folder.Parent = ReplicatedStorage
|
||||
end
|
||||
else
|
||||
folder = ReplicatedStorage:WaitForChild(NAME)
|
||||
end
|
||||
|
||||
local function get_remote_event(name: string, unreliable: boolean?): RemoteEvent & { actor: BindableEvent, peer: RemoteEvent }
|
||||
if RunService:IsServer() then
|
||||
return folder:FindFirstChild(name) :: RemoteEvent & { actor: BindableEvent }
|
||||
or (function()
|
||||
if not created_folder then
|
||||
warn(`btw, you are missing {name} from the JABBY_REMOTES folder`)
|
||||
end
|
||||
|
||||
local remote = Instance.new(if unreliable then "UnreliableRemoteEvent" else "RemoteEvent")
|
||||
remote.Name = name
|
||||
remote.Parent = folder
|
||||
|
||||
local fire_actor = Instance.new("BindableEvent")
|
||||
fire_actor.Name = "actor"
|
||||
fire_actor.Parent = remote
|
||||
|
||||
local peer = Instance.new("RemoteEvent")
|
||||
peer.Name = "peer"
|
||||
peer.Parent = remote
|
||||
|
||||
return remote :: RemoteEvent & { actor: BindableEvent, peer: RemoteEvent }
|
||||
end)()
|
||||
else
|
||||
return folder:WaitForChild(name) :: RemoteEvent & { actor: BindableEvent }
|
||||
end
|
||||
end
|
||||
|
||||
local function create_event<T...>(name: string, unreliable: boolean?, do_not_block_traffic: boolean?)
|
||||
local remote = get_remote_event(name, unreliable)
|
||||
local on_event_fire, fire = signal()
|
||||
|
||||
local event = {
|
||||
type = "event",
|
||||
|
||||
fire = function(_, connector: types.OutgoingConnector, ...)
|
||||
--- if the host is within this vm, we can fire it straight to
|
||||
if not traffic_check.check(local_host, connector.host, true) then return end
|
||||
|
||||
-- same host, same vm.
|
||||
if
|
||||
connector.host == local_host
|
||||
and connector.to_vm == vm_id
|
||||
then
|
||||
|
||||
local incoming = {
|
||||
host = local_host,
|
||||
from_vm = vm_id,
|
||||
to_vm = connector.to_vm
|
||||
}
|
||||
|
||||
fire(incoming, ...)
|
||||
--- if the host is the same, but in a separate actor
|
||||
--- we have to fire the actor
|
||||
elseif
|
||||
connector.host == local_host
|
||||
and connector.to_vm ~= vm_id
|
||||
then
|
||||
local incoming = {
|
||||
host = local_host,
|
||||
from_vm = vm_id,
|
||||
to_vm = connector.to_vm
|
||||
}
|
||||
|
||||
remote.actor:Fire(incoming, ...)
|
||||
--- we need to fire the server
|
||||
elseif connector.host == "server" then
|
||||
local incoming = {
|
||||
host = "server",
|
||||
from_vm = vm_id,
|
||||
to_vm = connector.to_vm
|
||||
}
|
||||
|
||||
remote:FireServer(incoming, ...)
|
||||
--- we need to fire the client
|
||||
elseif local_host == "server" then
|
||||
local incoming = {
|
||||
host = "server",
|
||||
from_vm = vm_id,
|
||||
to_vm = connector.to_vm
|
||||
}
|
||||
|
||||
remote:FireClient(connector.host, incoming, ...)
|
||||
--- we need to tell the server to redirect this to the client
|
||||
else
|
||||
local incoming = {
|
||||
host = connector.host,
|
||||
from_vm = vm_id,
|
||||
to_vm = connector.to_vm
|
||||
}
|
||||
|
||||
remote:FireServer(incoming, ...)
|
||||
end
|
||||
|
||||
end,
|
||||
|
||||
connect = function(_, callback: (types.IncomingConnector, T...) -> ())
|
||||
return on_event_fire:connect(callback :: any)
|
||||
end
|
||||
}
|
||||
|
||||
if RunService:IsServer() then
|
||||
remote.OnServerEvent:Connect(function(player, target: types.IncomingConnector, ...)
|
||||
--- check if the player is allowed to send this
|
||||
if not do_not_block_traffic and not traffic_check.check(player, target.host) then
|
||||
return
|
||||
end
|
||||
|
||||
--- check if its a proper connector
|
||||
if not tincoming_connector(target) then return end
|
||||
|
||||
if target.host == "server" and (target.to_vm == vm_id or target.to_vm == nil) then
|
||||
target.host = player
|
||||
fire(target, ...)
|
||||
elseif target.host ~= "server" and vm_id == MANAGER_VM then
|
||||
local to = target.host
|
||||
target.host = player
|
||||
remote:FireClient(
|
||||
to,
|
||||
target,
|
||||
...
|
||||
)
|
||||
end
|
||||
end)
|
||||
else
|
||||
remote.OnClientEvent:Connect(function(incoming: types.IncomingConnector, ...)
|
||||
-- print("receive", remote.Name, "from", incoming.host)
|
||||
if tincoming_connector(incoming) == false then return end
|
||||
if incoming.to_vm ~= vm_id and incoming.to_vm ~= nil then return end
|
||||
traffic_check._whitelist(local_host, incoming.host)
|
||||
|
||||
fire(incoming, ...)
|
||||
end)
|
||||
end
|
||||
|
||||
remote:WaitForChild("actor").Event:Connect(function(incoming: types.IncomingConnector, ...)
|
||||
if incoming.to_vm ~= vm_id and incoming.to_vm ~= nil then return end
|
||||
fire(incoming, ...)
|
||||
end)
|
||||
|
||||
return (event :: any) :: types.NetEvent<T...>
|
||||
end
|
||||
|
||||
return {
|
||||
create_event = create_event,
|
||||
local_host = local_host
|
||||
}
|
||||
115
modules/Jabby/modules/queue.luau
Normal file
115
modules/Jabby/modules/queue.luau
Normal file
|
|
@ -0,0 +1,115 @@
|
|||
--- Licensed under MIT from centau_ri
|
||||
export type Queue<T...> = typeof(setmetatable(
|
||||
{} :: {
|
||||
add: (self: Queue<T...>, T...) -> (),
|
||||
clear: (self: Queue<T...>) -> (),
|
||||
iter: (self: Queue<T...>) -> () -> T...,
|
||||
},
|
||||
{} :: {
|
||||
__len: (self: Queue<T...>) -> number,
|
||||
__iter: (self: Queue<T...>) -> () -> T...,
|
||||
}
|
||||
))
|
||||
|
||||
type Array<T> = { T }
|
||||
|
||||
local Queue = {}
|
||||
do
|
||||
Queue.__index = Queue
|
||||
|
||||
type _Queue = Queue<...any> & {
|
||||
size: number,
|
||||
columns: Array<Array<unknown>>,
|
||||
}
|
||||
|
||||
function Queue.new<T...>(): Queue<T...>
|
||||
local self: _Queue = setmetatable({
|
||||
size = 0,
|
||||
columns = {},
|
||||
}, Queue) :: any
|
||||
|
||||
setmetatable(self.columns, {
|
||||
__index = function(columns: Array<Array<unknown>>, idx: number)
|
||||
columns[idx] = {}
|
||||
return columns[idx]
|
||||
end,
|
||||
})
|
||||
|
||||
return self :: Queue<T...>
|
||||
end
|
||||
|
||||
function Queue.add(self: _Queue, ...: unknown)
|
||||
-- iteration will stop if first value is `nil`
|
||||
assert((...) ~= nil, "first argument cannot be nil")
|
||||
|
||||
local columns = self.columns
|
||||
local n = self.size + 1
|
||||
self.size = n
|
||||
|
||||
for i = 1, select("#", ...) do
|
||||
columns[i][n] = select(i, ...)
|
||||
end
|
||||
end
|
||||
|
||||
function Queue.clear(self: _Queue)
|
||||
self.size = 0
|
||||
for _, column in next, self.columns do
|
||||
table.clear(column)
|
||||
end
|
||||
end
|
||||
|
||||
local function iter(self: _Queue)
|
||||
local columns = self.columns
|
||||
local n = self.size
|
||||
local i = 0
|
||||
|
||||
if #columns <= 1 then
|
||||
local column = columns[1]
|
||||
return function()
|
||||
i += 1
|
||||
local value = column[i]
|
||||
if i == n then self:clear() end
|
||||
return value
|
||||
end
|
||||
else
|
||||
local tuple = table.create(#columns)
|
||||
return function()
|
||||
i += 1
|
||||
for ci, column in next, columns do
|
||||
tuple[ci] = column[i]
|
||||
end
|
||||
if i == n then self:clear() end
|
||||
return unpack(tuple)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
Queue.iter = iter
|
||||
Queue.__iter = iter
|
||||
|
||||
function Queue.__len(self: _Queue)
|
||||
return self.size
|
||||
end
|
||||
end
|
||||
|
||||
type ISignal<T...> = {
|
||||
connect: (self: any, listener: (T...) -> ()) -> (),
|
||||
} | {
|
||||
Connect: (self: any, listener: (T...) -> ()) -> (),
|
||||
}
|
||||
|
||||
local queue_create = function<T...>(signal: ISignal<T...>?): Queue<T...>
|
||||
local queue = Queue.new()
|
||||
|
||||
if signal then
|
||||
local connector = (signal :: any).connect or (signal :: any).Connect
|
||||
assert(connector, "signal has no member `connect()`")
|
||||
connector(signal, function(...)
|
||||
queue:add(...)
|
||||
end)
|
||||
end
|
||||
|
||||
return queue
|
||||
end :: (<T...>() -> Queue<T...>) & (<T...>(signal: ISignal<T...>) -> Queue<T...>)
|
||||
|
||||
return queue_create
|
||||
359
modules/Jabby/modules/remotes.luau
Normal file
359
modules/Jabby/modules/remotes.luau
Normal file
|
|
@ -0,0 +1,359 @@
|
|||
local jecs = require(script.Parent.Parent.jecs)
|
||||
local net = require(script.Parent.net)
|
||||
local types = require(script.Parent.types)
|
||||
|
||||
--todo: redo this file
|
||||
type Query = {
|
||||
include: { jecs.Entity<any> },
|
||||
exclude: { jecs.Entity<any> },
|
||||
with: { jecs.Entity<any> },
|
||||
}
|
||||
|
||||
return {
|
||||
|
||||
--[=[
|
||||
Broadcasts to clients that a new server has been registered.
|
||||
Accepts no params.
|
||||
]=]
|
||||
new_server_registered = net.create_event("server_registered", false, true)
|
||||
:: types.NetEvent<>,
|
||||
|
||||
--[=[
|
||||
Pings all servers and make them respond with new_server_registered
|
||||
]=]
|
||||
ping = net.create_event("ping", false, true)
|
||||
:: types.NetEvent<>,
|
||||
|
||||
--[=[
|
||||
Broadcasts to servers that a new client has been registered
|
||||
Accepts no params.
|
||||
]=]
|
||||
bind_to_server_core = net.create_event("client_registered")
|
||||
:: types.NetEvent<>,
|
||||
|
||||
--[=[
|
||||
Sends a update to a client about a new server
|
||||
]=]
|
||||
update_server_data =
|
||||
net.create_event("update_server_data")
|
||||
:: types.NetEvent<{
|
||||
worlds: {{id: number, name: string}},
|
||||
schedulers: {{id: number, name: string}}
|
||||
}>,
|
||||
|
||||
--[=[
|
||||
The client will use this to send the mouse pointer to the server
|
||||
]=]
|
||||
send_mouse_pointer =
|
||||
net.create_event("send_mouse_pointer")
|
||||
:: types.NetEvent<number, Vector3, Vector3>,
|
||||
|
||||
--[[
|
||||
|
||||
|
||||
]]
|
||||
send_mouse_entity =
|
||||
net.create_event("send_mouse_entity", true)
|
||||
:: types.NetEvent<number, Part?, number?, string?>,
|
||||
|
||||
--[=[
|
||||
Requests the server to validate a query
|
||||
world: number
|
||||
query: string
|
||||
]=]
|
||||
validate_query =
|
||||
net.create_event("validate_query")
|
||||
:: types.NetEvent<number, string>,
|
||||
|
||||
--[=[
|
||||
Result of the validation
|
||||
world: number query: string, terms: {}, ok: boolean, message: string?
|
||||
]=]
|
||||
validate_result =
|
||||
net.create_event("validate_result")
|
||||
:: types.NetEvent<number, string, Query?, boolean, string?>,
|
||||
|
||||
--[=[
|
||||
Requests a server to initiate replication of a query.
|
||||
|
||||
world: number
|
||||
id: number
|
||||
query_id: number
|
||||
query: string
|
||||
]=]
|
||||
request_query =
|
||||
net.create_event("replicate_query")
|
||||
:: types.NetEvent<number, number, string>,
|
||||
|
||||
--[=[
|
||||
Disconnects query
|
||||
|
||||
query_id: number
|
||||
]=]
|
||||
disconnect_query =
|
||||
net.create_event("disconnect_query")
|
||||
:: types.NetEvent<number>,
|
||||
|
||||
--[=[
|
||||
Changes the offsets to query for
|
||||
|
||||
query_id: number
|
||||
from: number
|
||||
to: number
|
||||
]=]
|
||||
advance_query_page =
|
||||
net.create_event("advance_query_page")
|
||||
:: types.NetEvent<number, number, number>,
|
||||
|
||||
--- pause the query
|
||||
--- query id
|
||||
--- should pause
|
||||
pause_query =
|
||||
net.create_event("pause_query")
|
||||
:: types.NetEvent<number, boolean>,
|
||||
|
||||
--- refreshes query results
|
||||
--- query_id
|
||||
refresh_results =
|
||||
net.create_event("refresh_query")
|
||||
:: types.NetEvent<number>,
|
||||
|
||||
--[=[
|
||||
Updates a single result
|
||||
|
||||
query_id: number
|
||||
frame: number
|
||||
column: number
|
||||
row: number
|
||||
value: any
|
||||
]=]
|
||||
update_query_result =
|
||||
net.create_event("update_query_result", true)
|
||||
:: types.NetEvent<(number, number, number, number, any)>,
|
||||
|
||||
--[=[
|
||||
Counts the total number of entities
|
||||
|
||||
query id: number
|
||||
count: number
|
||||
]=]
|
||||
|
||||
count_total_entities =
|
||||
net.create_event("count_total_entities", true)
|
||||
:: types.NetEvent<number, number>,
|
||||
|
||||
--[=[
|
||||
Requests a server to initiate replication of a scheduler
|
||||
]=]
|
||||
request_scheduler =
|
||||
net.create_event("initiate_replicate_scheduler") ::
|
||||
types.NetEvent<number>,
|
||||
|
||||
--[=[
|
||||
Requests the server to stop replicating a scheduler
|
||||
]=]
|
||||
disconnect_scheduler =
|
||||
net.create_event("disconnect_replicate_scheduler") ::
|
||||
types.NetEvent<number>,
|
||||
|
||||
--[=[
|
||||
Updates any static data about systems (like new systems)
|
||||
|
||||
systemid: number
|
||||
static_data: {}
|
||||
]=]
|
||||
scheduler_system_static_update =
|
||||
net.create_event("scheduler_system_update_static") ::
|
||||
types.NetEvent<number, number, types.SystemData?>,
|
||||
|
||||
--[=[
|
||||
Appends a frame to a system
|
||||
|
||||
systemid: number
|
||||
frame_count: number
|
||||
time_took: number
|
||||
]=]
|
||||
scheduler_system_update =
|
||||
net.create_event("append_frame_system", true) ::
|
||||
types.NetEvent<number, number, number, number>,
|
||||
|
||||
--[=[
|
||||
Pauses a system
|
||||
|
||||
scheduler: number
|
||||
systemid: number
|
||||
paused: boolean
|
||||
]=]
|
||||
scheduler_system_pause =
|
||||
net.create_event("scheduler_pause") ::
|
||||
types.NetEvent<number, number, boolean>,
|
||||
|
||||
--[[
|
||||
Validates a component
|
||||
|
||||
world: number
|
||||
component: string
|
||||
]]
|
||||
validate_entity_component =
|
||||
net.create_event("validate_entity_component") ::
|
||||
types.NetEvent<number, string>,
|
||||
|
||||
--[[
|
||||
entity component reslt
|
||||
|
||||
world: number
|
||||
component: string
|
||||
ok: boolean
|
||||
reason: string
|
||||
]]
|
||||
validate_entity_component_result =
|
||||
net.create_event("validate_entity_component_result") ::
|
||||
types.NetEvent<number, string, boolean, string>,
|
||||
|
||||
--[[
|
||||
Inspect a entity's components
|
||||
|
||||
world: number
|
||||
entity: number,
|
||||
inspectid: number
|
||||
]]
|
||||
|
||||
inspect_entity =
|
||||
net.create_event("inspect_entity") ::
|
||||
types.NetEvent<number, number, number>,
|
||||
|
||||
--[=[
|
||||
Gets the component of an entity
|
||||
|
||||
inspect: number
|
||||
component: string
|
||||
]=]
|
||||
get_component =
|
||||
net.create_event("get_entity_component") ::
|
||||
types.NetEvent<number, string>,
|
||||
|
||||
--[=[
|
||||
Returns the component of an entity
|
||||
|
||||
inspect: number
|
||||
component: string
|
||||
value: string
|
||||
]=]
|
||||
return_component =
|
||||
net.create_event("return_entity_component") ::
|
||||
types.NetEvent<number, string, string>,
|
||||
|
||||
--[[
|
||||
Delete entity
|
||||
|
||||
inspectid: number
|
||||
]]
|
||||
delete_entity =
|
||||
net.create_event("delete_entity") ::
|
||||
types.NetEvent<number>,
|
||||
|
||||
--[[
|
||||
Stops inspecting a entity
|
||||
|
||||
inspectid: number
|
||||
]]
|
||||
stop_inspect_entity =
|
||||
net.create_event("stop_inspect_entity") ::
|
||||
types.NetEvent<number>,
|
||||
|
||||
--[[
|
||||
Updates a entity
|
||||
|
||||
inspectid: number
|
||||
changes: {[component]: string}
|
||||
]]
|
||||
update_entity =
|
||||
net.create_event("update_entity") ::
|
||||
types.NetEvent<number, {[string]: string}>,
|
||||
|
||||
--[[
|
||||
Update the settings when dealing with inspecting
|
||||
|
||||
inspectid: nuimber,
|
||||
settings: {}
|
||||
]]
|
||||
update_inspect_settings =
|
||||
net.create_event("inspect_entity_settings_update") ::
|
||||
types.NetEvent<number, {paused: boolean}>,
|
||||
|
||||
--[[
|
||||
Inspector update
|
||||
|
||||
inspectid: number
|
||||
key: string
|
||||
value: string
|
||||
]]
|
||||
inspect_entity_update =
|
||||
net.create_event("inspect_entity_update") ::
|
||||
types.NetEvent<number, string, string?>,
|
||||
|
||||
--[[
|
||||
Creates a watch on a system
|
||||
|
||||
scheduler: number,
|
||||
system: number
|
||||
watchid: number
|
||||
]]
|
||||
create_watch =
|
||||
net.create_event("create_watch") ::
|
||||
types.NetEvent<number, number, number>,
|
||||
|
||||
--[[
|
||||
Removes a watch on a system
|
||||
|
||||
watchid: number
|
||||
]]
|
||||
remove_watch =
|
||||
net.create_event("remove_watch") ::
|
||||
types.NetEvent<number>,
|
||||
|
||||
--[[
|
||||
Retrieves data about a frame for a watch
|
||||
|
||||
watchid: number
|
||||
frame: number
|
||||
]]
|
||||
request_watch_data =
|
||||
net.create_event("request_watch_data") ::
|
||||
types.NetEvent<number, number>,
|
||||
|
||||
--[[
|
||||
Updates watch data for a frame
|
||||
|
||||
watchid: number
|
||||
frame: number
|
||||
changes: types.WatchLoggedChanges
|
||||
]]
|
||||
update_watch_data =
|
||||
net.create_event("update_watch_data") ::
|
||||
types.NetEvent<number, number, types.WatchLoggedChanges?>,
|
||||
|
||||
start_record_watch =
|
||||
net.create_event("start_record_watch") ::
|
||||
types.NetEvent<number>,
|
||||
|
||||
stop_watch =
|
||||
net.create_event("stop_watch") ::
|
||||
types.NetEvent<number>,
|
||||
|
||||
clear_watch =
|
||||
net.create_event("clear_watch") ::
|
||||
types.NetEvent<number>,
|
||||
|
||||
connect_watch =
|
||||
net.create_event("connect_to_watch") ::
|
||||
types.NetEvent<number>,
|
||||
|
||||
disconnect_watch =
|
||||
net.create_event("disconnect_watch") ::
|
||||
types.NetEvent<number>,
|
||||
|
||||
update_overview =
|
||||
net.create_event("update_watch_overview", true) ::
|
||||
types.NetEvent<number, number, number>
|
||||
}
|
||||
11
modules/Jabby/modules/reverse_connector.luau
Normal file
11
modules/Jabby/modules/reverse_connector.luau
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
--!nocheck
|
||||
local types = require(script.Parent.types)
|
||||
|
||||
local function reverse(connector: types.IncomingConnector): types.OutgoingConnector
|
||||
return {
|
||||
host = connector.host,
|
||||
to_vm = connector.from_vm,
|
||||
}
|
||||
end
|
||||
|
||||
return reverse
|
||||
75
modules/Jabby/modules/signal.luau
Normal file
75
modules/Jabby/modules/signal.luau
Normal file
|
|
@ -0,0 +1,75 @@
|
|||
--[[
|
||||
|
||||
A rudimentary signal class. Yielding may cause bugs.
|
||||
|
||||
]]
|
||||
|
||||
local signal = {}
|
||||
signal.__index = signal
|
||||
|
||||
type Connection = { disconnect: (any?) -> (), reconnect: (any?) -> () }
|
||||
export type Signal<T... = ...unknown> = {
|
||||
|
||||
class_name: "Signal",
|
||||
|
||||
connect: (Signal<T...>, callback: (T...) -> ()) -> Connection,
|
||||
wait: (Signal<T...>) -> T...,
|
||||
once: (Signal<T...>, callback: (T...) -> ()) -> Connection,
|
||||
|
||||
callbacks: { [(T...) -> ()]: true },
|
||||
}
|
||||
export type SignalInternal<T... = ...unknown> = Signal<T...> & {
|
||||
fire: (SignalInternal<T...>, T...) -> (),
|
||||
}
|
||||
|
||||
function signal.connect<T...>(self: Signal<T...>, callback: (T...) -> ())
|
||||
assert(type(callback) == "function")
|
||||
self.callbacks[callback] = true
|
||||
|
||||
return {
|
||||
disconnect = function() self.callbacks[callback] = nil end,
|
||||
reconnect = function() self.callbacks[callback] = true end,
|
||||
}
|
||||
end
|
||||
|
||||
function signal.fire<T...>(self: Signal<T...>, ...: T...)
|
||||
for callback in self.callbacks do
|
||||
callback(...)
|
||||
end
|
||||
end
|
||||
|
||||
function signal.once<T...>(self: Signal<T...>, callback: (T...) -> ())
|
||||
local connection
|
||||
connection = self:connect(function(...)
|
||||
connection:disconnect()
|
||||
callback(...)
|
||||
end)
|
||||
|
||||
return connection
|
||||
end
|
||||
|
||||
function signal.wait<T...>(self: Signal<T...>)
|
||||
local thread = coroutine.running()
|
||||
|
||||
local connection = self:connect(function(...) coroutine.resume(thread, ...) end)
|
||||
local packed = { coroutine.yield() }
|
||||
connection:disconnect()
|
||||
return unpack(packed)
|
||||
end
|
||||
|
||||
local function new_signal<T...>(): (Signal<T...>, (T...) -> ())
|
||||
local self = setmetatable({
|
||||
class_name = "Signal",
|
||||
callbacks = {},
|
||||
}, signal)
|
||||
|
||||
local function fire(...)
|
||||
for callback in self.callbacks :: any do
|
||||
callback(...)
|
||||
end
|
||||
end
|
||||
|
||||
return self :: any, fire
|
||||
end
|
||||
|
||||
return new_signal
|
||||
82
modules/Jabby/modules/traffic_check.luau
Normal file
82
modules/Jabby/modules/traffic_check.luau
Normal file
|
|
@ -0,0 +1,82 @@
|
|||
local Players = game:GetService("Players")
|
||||
--[[
|
||||
|
||||
a utility library to handle checking traffic and determining if the sender is
|
||||
permitted to send the given data.
|
||||
|
||||
]]
|
||||
|
||||
local signal = require(script.Parent.signal)
|
||||
|
||||
local traffic_check = {}
|
||||
|
||||
local whitelist_player_to = {}
|
||||
|
||||
local on_fail, fire = signal()
|
||||
|
||||
--- A function that needs to be overwritten by the user.
|
||||
--- This function is used to find out what permissions a user may have.
|
||||
traffic_check.can_use_jabby = function(player: Player)
|
||||
local is_studio = game:GetService("RunService"):IsStudio()
|
||||
|
||||
return is_studio --is_owner or is_studio
|
||||
end
|
||||
|
||||
--- Runs a callback defined by the developer to determine if a player is allowed
|
||||
--- to use a given function
|
||||
local function communication_is_allowed(from: "server" | Player, to: "server" | Player, dont_whitelist: boolean?)
|
||||
if from == "server" then return true end
|
||||
|
||||
whitelist_player_to[from] = whitelist_player_to[from] or {}
|
||||
whitelist_player_to[to] = whitelist_player_to[to] or {}
|
||||
|
||||
if traffic_check.can_use_jabby(from) or whitelist_player_to[from][to] then
|
||||
if dont_whitelist then return true end
|
||||
whitelist_player_to[to][from] = from
|
||||
return true
|
||||
else
|
||||
return false
|
||||
end
|
||||
end
|
||||
|
||||
--- Runs the given check and fires the on_fail signal if the player fails the
|
||||
--- check.
|
||||
local function check(from: "server" | Player, to: "server" | Player, dont_whitelist: boolean?)
|
||||
if communication_is_allowed(from, to, dont_whitelist) then
|
||||
return true
|
||||
else
|
||||
fire(from)
|
||||
return false
|
||||
end
|
||||
end
|
||||
|
||||
local function check_no_wl(from: "server" | Player)
|
||||
if from == "server" then return true end
|
||||
if traffic_check.can_use_jabby(from) then
|
||||
return true
|
||||
else
|
||||
-- print(from, "cant use jabby")
|
||||
fire(from)
|
||||
return false
|
||||
end
|
||||
end
|
||||
|
||||
local function _whitelist(from: "server" | Player, to: "server" | Player)
|
||||
whitelist_player_to[from] = whitelist_player_to[from] or {}
|
||||
whitelist_player_to[to] = whitelist_player_to[to] or {}
|
||||
whitelist_player_to[from][to] = from
|
||||
end
|
||||
|
||||
traffic_check.communication_is_allowed = communication_is_allowed
|
||||
traffic_check.check_no_wl = check_no_wl
|
||||
traffic_check.check = check
|
||||
|
||||
traffic_check._whitelist = _whitelist
|
||||
|
||||
traffic_check.on_fail = on_fail
|
||||
|
||||
Players.PlayerRemoving:Connect(function(player)
|
||||
whitelist_player_to[player] = nil
|
||||
end)
|
||||
|
||||
return traffic_check
|
||||
117
modules/Jabby/modules/types.luau
Normal file
117
modules/Jabby/modules/types.luau
Normal file
|
|
@ -0,0 +1,117 @@
|
|||
local jecs = require(script.Parent.Parent.jecs)
|
||||
type host = "client" | "server"
|
||||
|
||||
export type IncomingConnector = {
|
||||
host: Player | "server",
|
||||
from_vm: number,
|
||||
to_vm: number
|
||||
}
|
||||
|
||||
export type OutgoingConnector = {
|
||||
host: Player | "server",
|
||||
to_vm: number?, -- not specifying a vm makes it received by all
|
||||
from_vm: nil
|
||||
}
|
||||
|
||||
export type NetEvent<T...> = {
|
||||
|
||||
type: "event",
|
||||
|
||||
fire: (any, connector: OutgoingConnector, T...) -> (),
|
||||
connect: (any, callback: (connector: IncomingConnector, T...) -> ()) -> RBXScriptConnection,
|
||||
}
|
||||
|
||||
export type NetCallback<T..., U...> = {
|
||||
|
||||
type: "callback",
|
||||
|
||||
invoke: (any, connector: OutgoingConnector, T...) -> U...,
|
||||
set_callback: (any, callback: (connector: IncomingConnector, T...) -> U...) -> (),
|
||||
}
|
||||
|
||||
export type SystemId = number
|
||||
|
||||
export type SystemTag = "processing" | "finished" | "paused"
|
||||
export type SystemSettingData = {
|
||||
name: string?,
|
||||
phase: string?,
|
||||
layout_order: number?,
|
||||
paused: boolean?
|
||||
}
|
||||
export type SystemData = {
|
||||
name: string,
|
||||
phase: string?,
|
||||
layout_order: number,
|
||||
paused: boolean
|
||||
}
|
||||
|
||||
type ChangeTypes = "remove" | "clear" | "delete" | "add" | "set" | "entity" | "component"
|
||||
export type WatchLoggedChanges = {
|
||||
types: {ChangeTypes},
|
||||
entities: {jecs.Entity<any>},
|
||||
component: {jecs.Entity<any>},
|
||||
values: {string},
|
||||
worlds: {jecs.World}
|
||||
}
|
||||
|
||||
export type SystemWatch = {
|
||||
--- enables Lua Object Notation.
|
||||
--- incurs a significant performance penalty.
|
||||
enable_lon: boolean,
|
||||
--- the current frame to process
|
||||
frame: number,
|
||||
|
||||
frames: {[number]: WatchLoggedChanges}
|
||||
}
|
||||
|
||||
export type SystemLabel = {}
|
||||
|
||||
export type SystemFrame = {
|
||||
i: number,
|
||||
s: number
|
||||
}
|
||||
|
||||
type WatchData = {active: boolean, watch: SystemWatch, untrack: () -> ()}
|
||||
|
||||
export type Scheduler = {
|
||||
class_name: "Scheduler",
|
||||
name: string,
|
||||
|
||||
valid_system_ids: {[SystemId]: true},
|
||||
system_data: {[SystemId]: SystemData},
|
||||
system_data_updated: {[SystemId]: true},
|
||||
system_frames: {[SystemId]: SystemFrame},
|
||||
system_frames_updated: {[SystemId]: {[SystemFrame]: true}},
|
||||
system_watches: {[SystemId]: {WatchData}},
|
||||
|
||||
register_system: (Scheduler, settings: SystemSettingData?) -> SystemId,
|
||||
set_system_data: (Scheduler, system: SystemId, settings: SystemSettingData) -> (),
|
||||
get_system_data: (Scheduler, system: SystemId) -> SystemSettingData,
|
||||
create_watch_for_system: (Scheduler, system: SystemId) -> WatchData,
|
||||
remove_system: (Scheduler, system: SystemId) -> (),
|
||||
|
||||
-- mark_system_frame_start: (Scheduler, system: SystemId) -> (),
|
||||
-- mark_system_frame_end: (Scheduler, system: SystemId, s: number?) -> (),
|
||||
-- append_extra_frame_data: (Scheduler, system: SystemId, label: SystemLabel) -> (),
|
||||
--- this should call mark_system_frame_start and mark_system_frame_end for you
|
||||
run: <T...>(Scheduler, system: SystemId, system: () -> (), T...) -> (),
|
||||
|
||||
}
|
||||
|
||||
export type World = {
|
||||
class_name: "World",
|
||||
name: string,
|
||||
world: jecs.World,
|
||||
|
||||
entities: {[Instance]: jecs.Entity<any>}?,
|
||||
get_entity_from_part: ((part: BasePart) -> (jecs.Entity<any>?, Part?))?
|
||||
}
|
||||
|
||||
export type Application<T> = {
|
||||
class_name: "app",
|
||||
name: string,
|
||||
|
||||
mount: (props: T, destroy: () -> ()) -> Instance
|
||||
}
|
||||
|
||||
return nil
|
||||
71
modules/Jabby/modules/videx.luau
Normal file
71
modules/Jabby/modules/videx.luau
Normal file
|
|
@ -0,0 +1,71 @@
|
|||
--------------------------------------------------------------------------------
|
||||
-- videx/store.luau
|
||||
--------------------------------------------------------------------------------
|
||||
local vide = require(script.Parent.Parent.vide)
|
||||
local source = vide.source
|
||||
|
||||
local NULL = newproxy()
|
||||
|
||||
local Store = {}
|
||||
|
||||
--[=[
|
||||
Creates a new store object that receives some initial state and then returns
|
||||
a table with the same structure, but all keys of the given table will be reactive.
|
||||
|
||||
When accessed inside a reactive scope, the reactive scope will update whenever
|
||||
the key that is accessed is changed.
|
||||
|
||||
@param initial_state `T : {[string]: any}` The initial state the store will start in.
|
||||
@param mutations `() -> {[string]: (T, ...any) -> ...any}?` A list of functions that mutate the data.
|
||||
@return `T & U` A resulting table that
|
||||
]=]
|
||||
function Store.new<T, U>(
|
||||
initial_state: T & {},
|
||||
mutations: (T & U) -> U
|
||||
): T & U
|
||||
local sources = {}
|
||||
|
||||
for i, v in initial_state :: any do
|
||||
local src = source(v ~= NULL and v or nil)
|
||||
sources[i] = src
|
||||
end
|
||||
|
||||
local internal_proxy = {}
|
||||
|
||||
setmetatable(internal_proxy, {
|
||||
__index = function(_, index)
|
||||
return sources[index]()
|
||||
end,
|
||||
__newindex = function(_, index, value)
|
||||
sources[index](value)
|
||||
end
|
||||
})
|
||||
|
||||
local external_proxy = {}
|
||||
|
||||
setmetatable(external_proxy :: any, {
|
||||
__index = function(_, index)
|
||||
local src = sources[index]
|
||||
if src == nil then error(`invalid index {index}`, 2) end
|
||||
return src()
|
||||
end,
|
||||
|
||||
__newindex = function(_, index, value)
|
||||
sources[index](value)
|
||||
end
|
||||
})
|
||||
|
||||
for i, v in next, mutations(internal_proxy :: any) :: any do
|
||||
if rawget(external_proxy, i) then
|
||||
error(`duplicate field "{i}"`, 2)
|
||||
end
|
||||
rawset(external_proxy, i, v)
|
||||
end
|
||||
|
||||
return external_proxy :: T & U & {}
|
||||
end
|
||||
|
||||
--- A special symbol used to indicate that a value should be nil within a Store.
|
||||
Store.null = NULL :: nil
|
||||
|
||||
return Store
|
||||
14
modules/Jabby/modules/vm_id.luau
Normal file
14
modules/Jabby/modules/vm_id.luau
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
--[[
|
||||
|
||||
Provides a unique identifier for a VM.
|
||||
|
||||
This currently cannot be tested unless there is some parallel system for jest.
|
||||
|
||||
]]
|
||||
|
||||
local SharedTableRegistry = game:GetService("SharedTableRegistry")
|
||||
|
||||
local shared_table = SharedTableRegistry:GetSharedTable("_gorp_common_vm_count")
|
||||
shared_table.id = shared_table.id or 0
|
||||
|
||||
return SharedTable.increment(shared_table, "id", 1)
|
||||
42
modules/Jabby/server/init.luau
Normal file
42
modules/Jabby/server/init.luau
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
local Players = game:GetService("Players")
|
||||
local RunService = game:GetService("RunService")
|
||||
|
||||
local loop = require(script.Parent.modules.loop)
|
||||
local remotes = require(script.Parent.modules.remotes)
|
||||
local traffic_check = require(script.Parent.modules.traffic_check)
|
||||
local vm_id = require(script.Parent.modules.vm_id)
|
||||
local function broadcast()
|
||||
for _, player in Players:GetPlayers() do
|
||||
if not traffic_check.can_use_jabby(player) then continue end
|
||||
remotes.new_server_registered:fire({
|
||||
host = player,
|
||||
})
|
||||
end
|
||||
end
|
||||
|
||||
task.delay(0, broadcast)
|
||||
|
||||
local systems = script.systems
|
||||
local loop = loop (
|
||||
`jabby-host:{
|
||||
if RunService:IsServer() then "server" else "client"
|
||||
}-vm:{vm_id}`,
|
||||
nil,
|
||||
{i = 1},
|
||||
|
||||
systems.ping,
|
||||
systems.replicate_core,
|
||||
systems.replicate_scheduler,
|
||||
systems.replicate_registry,
|
||||
systems.replicate_system_watch,
|
||||
systems.mouse_pointer,
|
||||
systems.entity
|
||||
)
|
||||
|
||||
RunService.PostSimulation:Connect(loop)
|
||||
|
||||
return {
|
||||
|
||||
broadcast = broadcast
|
||||
|
||||
}
|
||||
16
modules/Jabby/server/public.luau
Normal file
16
modules/Jabby/server/public.luau
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
--[[
|
||||
|
||||
anything in here is considered "public" and will be visible to jabby clients
|
||||
|
||||
]]
|
||||
|
||||
local self = {
|
||||
|
||||
updated = false,
|
||||
|
||||
} :: {
|
||||
updated: boolean,
|
||||
[number]: any
|
||||
}
|
||||
|
||||
return self
|
||||
402
modules/Jabby/server/query_parser.luau
Normal file
402
modules/Jabby/server/query_parser.luau
Normal file
|
|
@ -0,0 +1,402 @@
|
|||
--!strict
|
||||
|
||||
local escape_chars = {
|
||||
a = "\a",
|
||||
b = "\b",
|
||||
f = "\f",
|
||||
n = "\n",
|
||||
r = "\r",
|
||||
t = "\t",
|
||||
v = "\v",
|
||||
["\\"] = "\\",
|
||||
["\""] = "\"",
|
||||
["\'"] = "\'"
|
||||
}
|
||||
|
||||
export type Position = {
|
||||
line: number,
|
||||
pos: number,
|
||||
col: number
|
||||
}
|
||||
|
||||
export type String = {type: "string", s: string}
|
||||
export type Number = {type: "number", s: number}
|
||||
export type Identifier = {type: "identifier", s: string}
|
||||
export type Operator = {type: "operator", s: "#" | "!" | "*" | "$"}
|
||||
export type Symbol = {type: "symbol", s: "(" | ")" | ";" | ","}
|
||||
export type EOF = {type: "eof", s: "eof"}
|
||||
|
||||
export type Token =
|
||||
String
|
||||
| Number
|
||||
| Identifier
|
||||
| Symbol
|
||||
| Operator
|
||||
| EOF
|
||||
|
||||
|
||||
export type Stream = {
|
||||
next: () -> Token,
|
||||
peek: (n: number?) -> Token,
|
||||
eof: () -> boolean,
|
||||
croak: (msg: string) -> (),
|
||||
pos: () -> Position
|
||||
}
|
||||
|
||||
local function stream(input: string)
|
||||
|
||||
local pos = 0
|
||||
local line = 1
|
||||
local col = 1
|
||||
|
||||
local function peek(): string
|
||||
return string.sub(input, pos+1, pos+1)
|
||||
end
|
||||
|
||||
local function next(): string
|
||||
local char = peek()
|
||||
pos += 1
|
||||
|
||||
if char == "\n" then line += 1; col = 1
|
||||
else col += 1 end
|
||||
|
||||
return char
|
||||
end
|
||||
|
||||
local function eof(): boolean
|
||||
return peek() == ""
|
||||
end
|
||||
|
||||
local function position()
|
||||
return {
|
||||
pos = pos,
|
||||
line = line,
|
||||
col = col
|
||||
}
|
||||
end
|
||||
|
||||
local function croak(msg)
|
||||
error(`{msg} ({line}:{col})`, 0)
|
||||
end
|
||||
|
||||
return {
|
||||
peek = peek,
|
||||
next = next,
|
||||
eof = eof,
|
||||
croak = croak,
|
||||
pos = position
|
||||
}
|
||||
|
||||
end
|
||||
|
||||
local function lex(source: string): Stream
|
||||
|
||||
local input = stream(source)
|
||||
|
||||
local function is_whitespace(char: string)
|
||||
return not not string.match(char, "[\t ]")
|
||||
end
|
||||
|
||||
local function is_digit(char: string)
|
||||
return not not (string.match(char, "%d"))
|
||||
end
|
||||
|
||||
local function is_start_identifier(char: string)
|
||||
return not not string.match(char, "[%a_]")
|
||||
end
|
||||
|
||||
local function is_identifier(char: string)
|
||||
return not not string.match(char, "[%a_:%.]")
|
||||
end
|
||||
|
||||
local function is_op_char(char: string)
|
||||
return char == "#" or char == "!" or char == "*" or char == "$"
|
||||
end
|
||||
|
||||
local function is_punc(char: string)
|
||||
return not not string.match(char, "[%(%);,]")
|
||||
end
|
||||
|
||||
local function read_while(predicate: (char: string) -> boolean)
|
||||
local str = ""
|
||||
while input.eof() == false and predicate(input.peek()) do
|
||||
str ..= input.next()
|
||||
end
|
||||
return str
|
||||
end
|
||||
|
||||
local function skip_whitespace()
|
||||
read_while(is_whitespace)
|
||||
end
|
||||
|
||||
local function read_string(): String
|
||||
local escaped = false
|
||||
local token = ""
|
||||
local eliminator = input.next()
|
||||
local from = input.pos()
|
||||
|
||||
while input.eof() == false and (input.peek() ~= eliminator or escaped) do
|
||||
local char = input.next()
|
||||
|
||||
if char == "\\" then
|
||||
escaped = true
|
||||
end
|
||||
|
||||
if escaped then
|
||||
token ..= escape_chars[char] or input.croak(`cannot escape {char}`)
|
||||
escaped = false
|
||||
else
|
||||
token ..= char
|
||||
end
|
||||
end
|
||||
|
||||
local to = input.pos()
|
||||
|
||||
-- print("t", token, eliminator, input.peek())
|
||||
|
||||
if input.peek() ~= eliminator then input.croak("unterminated string") end
|
||||
input.next()
|
||||
return {type = "string", s = token, from = from, to = to}
|
||||
end
|
||||
|
||||
local function read_number(): Number
|
||||
local decimal_pointer = false
|
||||
local from = input.pos()
|
||||
local token = read_while(function(char)
|
||||
if decimal_pointer and char == "." then return false end
|
||||
if char == "." then decimal_pointer = true end
|
||||
return is_digit(char)
|
||||
end)
|
||||
local to = input.pos()
|
||||
|
||||
local n = tonumber(token)
|
||||
|
||||
if not n then
|
||||
input.croak(`could not read {token} as number`)
|
||||
end
|
||||
|
||||
return {type = "number", s = assert(n), from = from, to = to}
|
||||
end
|
||||
|
||||
local function read_identifier(): Identifier
|
||||
local from = input.pos()
|
||||
local token = read_while(is_identifier)
|
||||
local to = input.pos()
|
||||
|
||||
-- if table.find(keywords, token) then
|
||||
-- return {type = "keyword", s = token :: any, from = from, to = to}
|
||||
-- else
|
||||
return {type = "identifier", s = token, from = from, to = to}
|
||||
-- end
|
||||
end
|
||||
|
||||
local function read_next(): Token
|
||||
skip_whitespace()
|
||||
if input.eof() then return {type = "eof", s = "eof"} end
|
||||
|
||||
local char = input.peek()
|
||||
|
||||
if char == "\"" or char == "'" then return read_string() end
|
||||
if is_digit(char) then return read_number() end
|
||||
if is_start_identifier(char) then return read_identifier() end
|
||||
if is_op_char(char) then return {type = "operator", s = input.next() :: any} end
|
||||
if is_punc(char) then return {type = "symbol", s = input.next() :: any} end
|
||||
|
||||
input.croak(`cannot lex {char}`)
|
||||
error("fail")
|
||||
end
|
||||
|
||||
local current: {Token} = {}
|
||||
|
||||
local function next()
|
||||
local token = table.remove(current, 1)
|
||||
return if token == nil then read_next() else token
|
||||
end
|
||||
|
||||
local function peek(n: number?)
|
||||
local n = n or 1
|
||||
while #current < n do
|
||||
table.insert(current, read_next())
|
||||
end
|
||||
return current[n]
|
||||
end
|
||||
|
||||
local function eof()
|
||||
return peek().type == "eof"
|
||||
end
|
||||
|
||||
return {
|
||||
|
||||
peek = peek,
|
||||
next = next,
|
||||
eof = eof,
|
||||
croak = input.croak,
|
||||
pos = input.pos
|
||||
|
||||
}
|
||||
end
|
||||
|
||||
type Wildcard = {
|
||||
type: "Wildcard",
|
||||
name: "*"
|
||||
}
|
||||
|
||||
export type Value = {
|
||||
type: "Name",
|
||||
name: string
|
||||
} | {
|
||||
type: "Entity",
|
||||
entity: number
|
||||
}
|
||||
|
||||
export type PureComponent = {
|
||||
type: "Component",
|
||||
query: boolean,
|
||||
exclude: boolean,
|
||||
value: Value,
|
||||
}
|
||||
|
||||
export type Relationship = {
|
||||
type: "Relationship",
|
||||
query: boolean,
|
||||
exclude: boolean,
|
||||
left: PureComponent | Wildcard,
|
||||
right: PureComponent | Wildcard
|
||||
}
|
||||
|
||||
type Component = Relationship | PureComponent | Wildcard
|
||||
|
||||
local function parse(input: string): {PureComponent | Relationship}
|
||||
local lexer = lex(input)
|
||||
|
||||
local result: {PureComponent | Relationship} = {}
|
||||
|
||||
local should_query = true
|
||||
local should_exclude = false
|
||||
local should_relationship = false
|
||||
local interpret_pointer = false
|
||||
local components: {Component} = {}
|
||||
|
||||
while true do
|
||||
local symbol = lexer.peek()
|
||||
-- print2(symbol)
|
||||
if symbol.type == "eof" then
|
||||
break
|
||||
elseif interpret_pointer or symbol.type == "number" then
|
||||
if not interpret_pointer then
|
||||
lexer.croak("expected $")
|
||||
elseif symbol.type ~= "number" then
|
||||
lexer.croak("expected number")
|
||||
error("")
|
||||
end
|
||||
|
||||
table.insert(components, {
|
||||
type = "Component",
|
||||
query = should_query,
|
||||
exclude = should_exclude,
|
||||
value = {type = "Entity", entity = tonumber(lexer.next().s) :: number}
|
||||
})
|
||||
|
||||
should_query = if should_relationship then should_query else true
|
||||
should_exclude = if should_relationship then should_exclude else false
|
||||
interpret_pointer = false
|
||||
if lexer.peek().type ~= "symbol" and lexer.peek().type ~= "eof" then lexer.croak("expected symbol or eof after identifier") end
|
||||
elseif symbol.type == "operator" then
|
||||
if symbol.s == "#" then
|
||||
if should_relationship then lexer.croak("cannot tag inside relationship") end
|
||||
should_query = false
|
||||
lexer.next()
|
||||
elseif symbol.s == "!" then
|
||||
if should_relationship then lexer.croak("cannot exclude in relationship") end
|
||||
should_exclude = true
|
||||
should_query = false
|
||||
lexer.next()
|
||||
elseif symbol.s == "$" then
|
||||
interpret_pointer = true
|
||||
lexer.next()
|
||||
elseif symbol.s == "*" then
|
||||
if not should_relationship then lexer.croak("cannot use wildcards outside relationship") end
|
||||
table.insert(components, {
|
||||
type = "Wildcard",
|
||||
name = "*"
|
||||
})
|
||||
lexer.next()
|
||||
end
|
||||
elseif symbol.type == "symbol" then
|
||||
if symbol.s == "(" then
|
||||
if should_relationship == true then lexer.croak("relationship within relationship") end
|
||||
should_relationship = true
|
||||
lexer.next()
|
||||
elseif symbol.s == ")" then
|
||||
if should_relationship == false then lexer.croak("missing (") end
|
||||
if #components == 2 then
|
||||
local right = table.remove(components) :: Component
|
||||
local left = table.remove(components) :: Component
|
||||
|
||||
if left.type == "Wildcard" and right.type == "Wildcard" then
|
||||
lexer.croak("both components are wildcards")
|
||||
end
|
||||
|
||||
components = {{
|
||||
type = "Relationship",
|
||||
query = should_query,
|
||||
exclude = should_exclude,
|
||||
left = left :: any,
|
||||
right = right :: any
|
||||
}}
|
||||
|
||||
should_query = true
|
||||
should_exclude = false
|
||||
should_relationship = false
|
||||
lexer.next()
|
||||
else
|
||||
lexer.croak(`expected 2 components, got {#components}`)
|
||||
end
|
||||
elseif symbol.s == "," or symbol.s == ";" then
|
||||
if should_relationship then
|
||||
lexer.next()
|
||||
continue
|
||||
end
|
||||
|
||||
local ctype = table.remove(components)
|
||||
if ctype == nil then
|
||||
lexer.croak("no component provided")
|
||||
error("")
|
||||
end
|
||||
|
||||
table.insert(result, ctype :: any)
|
||||
|
||||
should_query = true
|
||||
should_exclude = false
|
||||
lexer.next()
|
||||
end
|
||||
elseif symbol.type == "identifier" then
|
||||
table.insert(components, {
|
||||
type = "Component",
|
||||
query = should_query,
|
||||
exclude = should_exclude,
|
||||
value = {type = "Name", name = lexer.next().s :: string}
|
||||
})
|
||||
|
||||
should_query = if should_relationship then should_query else true
|
||||
should_exclude = if should_relationship then should_exclude else false
|
||||
if lexer.peek().type ~= "symbol" and lexer.peek().type ~= "eof" then lexer.croak("expected symbol or eof after identifier") end
|
||||
elseif symbol.type == "string" then
|
||||
table.insert(components, {
|
||||
type = "Component",
|
||||
query = should_query,
|
||||
exclude = should_exclude,
|
||||
value = {type = "Name", name = lexer.next().s :: string}
|
||||
})
|
||||
should_query = if should_relationship then should_query else true
|
||||
should_exclude = if should_relationship then should_exclude else false
|
||||
if lexer.peek().type ~= "symbol" and lexer.peek().type ~= "eof" then lexer.croak("expected symbol or eof after string") end
|
||||
end
|
||||
end
|
||||
|
||||
table.insert(result, table.remove(components) :: any)
|
||||
|
||||
return result
|
||||
end
|
||||
|
||||
return parse
|
||||
197
modules/Jabby/server/scheduler.luau
Normal file
197
modules/Jabby/server/scheduler.luau
Normal file
|
|
@ -0,0 +1,197 @@
|
|||
local types = require(script.Parent.Parent.modules.types)
|
||||
local watch = require(script.Parent.watch)
|
||||
|
||||
type SystemId = types.SystemId
|
||||
type SystemSettingData = types.SystemSettingData
|
||||
type SystemTag = types.SystemTag
|
||||
|
||||
type SystemData = types.SystemData
|
||||
type ProcessingFrame = {
|
||||
started_at: number
|
||||
}
|
||||
type SystemFrame = types.SystemFrame
|
||||
|
||||
local MAX_BUFFER_SIZE = 50
|
||||
|
||||
local n = 0
|
||||
local schedulers = {}
|
||||
|
||||
local function unit() end
|
||||
|
||||
local function create_scheduler()
|
||||
|
||||
local count = 1
|
||||
local frames = 0
|
||||
|
||||
local scheduler = {
|
||||
class_name = "Scheduler",
|
||||
name = "Scheduler",
|
||||
|
||||
--- contains a map of valid system ids
|
||||
valid_system_ids = {} :: {[SystemId]: true},
|
||||
--- contains a list of static system data that is updated infrequently
|
||||
system_data = {} :: {[SystemId]: SystemData},
|
||||
--- list of system data that has updated
|
||||
system_data_updated = {} :: {[SystemId]: true},
|
||||
--- contains a buffer of the last couple frames of system data that is
|
||||
--- refreshed constantly
|
||||
system_frames = {} :: {[SystemId]: {SystemFrame}},
|
||||
--- stores the frames that have been updated
|
||||
system_frames_updated = {} :: {[SystemId]: {[SystemFrame]: true}},
|
||||
--- contains the current frame that a system is processing
|
||||
processing_frame = {} :: {[SystemId]: ProcessingFrame},
|
||||
--- contains a list of watches for each system
|
||||
system_watches = {} :: {[SystemId]: {{active: boolean, watch: types.SystemWatch}}}
|
||||
|
||||
}
|
||||
|
||||
local function ENABLE_WATCHES(id: SystemId)
|
||||
local watches = scheduler.system_watches[id]
|
||||
local cleanup = {}
|
||||
|
||||
for i, system_watch in watches do
|
||||
if system_watch.active == false then continue end
|
||||
watch.step_watch(system_watch.watch)
|
||||
cleanup[i] = watch.track_watch(system_watch.watch)
|
||||
end
|
||||
|
||||
return function()
|
||||
for _, stop in cleanup do
|
||||
stop()
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local function ASSERT_SYSTEM_VALID(id: SystemId)
|
||||
assert(scheduler.valid_system_ids[id], `attempt to use unknown system with id #{id}`)
|
||||
end
|
||||
|
||||
function scheduler:register_system(settings: types.SystemSettingData?)
|
||||
local id = count; count += 1
|
||||
scheduler.valid_system_ids[id] = true
|
||||
scheduler.system_data[id] = {
|
||||
name = "UNNAMED",
|
||||
phase = nil,
|
||||
layout_order = 0,
|
||||
paused = false
|
||||
}
|
||||
scheduler.system_frames[id] = {}
|
||||
scheduler.system_frames_updated[id] = {}
|
||||
|
||||
if settings then
|
||||
scheduler:set_system_data(id, settings)
|
||||
end
|
||||
|
||||
return id
|
||||
end
|
||||
|
||||
function scheduler:set_system_data(id: SystemId, settings: types.SystemSettingData)
|
||||
ASSERT_SYSTEM_VALID(id)
|
||||
|
||||
for key, value in settings do
|
||||
scheduler.system_data[id][key] = value
|
||||
end
|
||||
scheduler.system_data_updated[id] = true
|
||||
end
|
||||
|
||||
function scheduler:get_system_data(id: SystemId)
|
||||
ASSERT_SYSTEM_VALID(id)
|
||||
return scheduler.system_data[id]
|
||||
end
|
||||
|
||||
function scheduler:remove_system(id: SystemId)
|
||||
scheduler.valid_system_ids[id] = nil
|
||||
scheduler.system_data[id] = nil
|
||||
scheduler.system_frames[id] = nil
|
||||
scheduler.system_frames_updated[id] = nil
|
||||
scheduler.system_data_updated[id] = true
|
||||
scheduler.system_watches[id] = nil
|
||||
end
|
||||
|
||||
function scheduler:_mark_system_frame_start(id: SystemId)
|
||||
ASSERT_SYSTEM_VALID(id)
|
||||
|
||||
scheduler.processing_frame[id] = {
|
||||
started_at = os.clock()
|
||||
}
|
||||
end
|
||||
|
||||
function scheduler:_mark_system_frame_end(id: SystemId, s: number?)
|
||||
ASSERT_SYSTEM_VALID(id)
|
||||
local now = os.clock()
|
||||
local pending_frame_data = scheduler.processing_frame[id]
|
||||
assert(pending_frame_data ~= nil, "no processing frame")
|
||||
local frame = {
|
||||
i = frames,
|
||||
s = now - pending_frame_data.started_at
|
||||
}
|
||||
|
||||
frames += 1
|
||||
|
||||
scheduler.processing_frame[id] = nil
|
||||
scheduler.system_frames_updated[id][frame] = true
|
||||
local last_frame = scheduler.system_frames[id][MAX_BUFFER_SIZE]
|
||||
if last_frame then
|
||||
scheduler.system_frames_updated[id][last_frame] = nil
|
||||
end
|
||||
|
||||
table.insert(scheduler.system_frames[id], 1, frame)
|
||||
table.remove(scheduler.system_frames[id], MAX_BUFFER_SIZE + 1)
|
||||
end
|
||||
|
||||
function scheduler:append_extra_frame_data(id: SystemId, label: {})
|
||||
--todo
|
||||
error("todo")
|
||||
end
|
||||
|
||||
function scheduler:run<T...>(id: SystemId, system: (T...) -> (), ...: T...)
|
||||
ASSERT_SYSTEM_VALID(id)
|
||||
local system_data = scheduler.system_data[id]
|
||||
|
||||
if system_data.paused then return end
|
||||
|
||||
local watches = scheduler.system_watches[id]
|
||||
local cleanup_watches = unit
|
||||
|
||||
if watches then
|
||||
cleanup_watches = ENABLE_WATCHES(id)
|
||||
end
|
||||
|
||||
scheduler:_mark_system_frame_start(id)
|
||||
system(...)
|
||||
scheduler:_mark_system_frame_end(id)
|
||||
|
||||
cleanup_watches()
|
||||
end
|
||||
|
||||
function scheduler:create_watch_for_system(id: SystemId)
|
||||
ASSERT_SYSTEM_VALID(id)
|
||||
|
||||
local new_watch = watch.create_watch()
|
||||
local watch_data
|
||||
scheduler.system_watches[id] = scheduler.system_watches[id] or {} :: never
|
||||
|
||||
local function untrack()
|
||||
local idx = table.find(scheduler.system_watches[id], watch_data)
|
||||
table.remove(scheduler.system_watches[id], idx)
|
||||
end
|
||||
|
||||
watch_data = {active = false, watch = new_watch, untrack = untrack}
|
||||
table.insert(scheduler.system_watches[id], watch_data)
|
||||
|
||||
return watch_data
|
||||
end
|
||||
|
||||
schedulers[n + 1] = scheduler
|
||||
n = n + 1
|
||||
|
||||
return scheduler
|
||||
|
||||
end
|
||||
|
||||
return {
|
||||
|
||||
create = create_scheduler,
|
||||
schedulers = schedulers
|
||||
|
||||
}
|
||||
294
modules/Jabby/server/systems/entity.luau
Normal file
294
modules/Jabby/server/systems/entity.luau
Normal file
|
|
@ -0,0 +1,294 @@
|
|||
local jecs = require(script.Parent.Parent.Parent.jecs)
|
||||
local lon = require(script.Parent.Parent.Parent.modules.lon)
|
||||
local queue = require(script.Parent.Parent.Parent.modules.queue)
|
||||
local remotes = require(script.Parent.Parent.Parent.modules.remotes)
|
||||
local reverse_connector = require(script.Parent.Parent.Parent.modules.reverse_connector)
|
||||
local traffic_check = require(script.Parent.Parent.Parent.modules.traffic_check)
|
||||
local types = require(script.Parent.Parent.Parent.modules.types)
|
||||
local public = require(script.Parent.Parent.public)
|
||||
local query_parser = require(script.Parent.Parent.query_parser)
|
||||
|
||||
local entity_index_try_get = jecs.entity_index_try_get
|
||||
local IS_PAIR = jecs.IS_PAIR
|
||||
local pair = jecs.pair
|
||||
local pair_first = jecs.pair_first
|
||||
local pair_second = jecs.pair_second
|
||||
local empty_table = {}
|
||||
|
||||
local function get_all_components(world, entity): {}
|
||||
local record = entity_index_try_get(world.entity_index, entity)
|
||||
|
||||
if not record then return empty_table end
|
||||
local archetype = record.archetype
|
||||
if not archetype then return empty_table end
|
||||
|
||||
local components = {}
|
||||
for _, ty in archetype.types do
|
||||
table.insert(components, ty)
|
||||
end
|
||||
return components
|
||||
end
|
||||
|
||||
local function convert_component(world, debug, entity): string
|
||||
if IS_PAIR(entity) then
|
||||
local left = convert_component(world, debug, pair_first(world, entity))
|
||||
local right = convert_component(world, debug, pair_second(world, entity))
|
||||
return `({left}, {right})`
|
||||
else
|
||||
return world:get(entity, debug) or `${tostring(entity)}`
|
||||
end
|
||||
end
|
||||
|
||||
--- Indicates a value is a tag
|
||||
local TAG = newproxy()
|
||||
--- Indicates a value is set to nil; this is not allowed in 0.3.0
|
||||
local BAD_VALUE = newproxy()
|
||||
|
||||
local function get_component(ctype_name: string, map_components: {[string]: number})
|
||||
|
||||
local function get_entity(ctype: query_parser.PureComponent)
|
||||
local value = ctype.value
|
||||
|
||||
if value.type == "Entity" then
|
||||
return value.entity
|
||||
elseif value.type == "Name" then
|
||||
return map_components[value.name]
|
||||
end
|
||||
error("bad")
|
||||
end
|
||||
|
||||
local entity_to_set
|
||||
local parse = query_parser(ctype_name)[1]
|
||||
if parse.type == "Component" then
|
||||
entity_to_set = get_entity(parse)
|
||||
elseif parse.type == "Relationship" then
|
||||
local left, right = jecs.Wildcard, jecs.Wildcard
|
||||
|
||||
if parse.left.type == "Component" then
|
||||
left = get_entity(parse.left)
|
||||
end
|
||||
|
||||
if parse.right.type == "Component" then
|
||||
right = get_entity(parse.right)
|
||||
end
|
||||
|
||||
entity_to_set = pair(left, right)
|
||||
end
|
||||
|
||||
return entity_to_set
|
||||
end
|
||||
|
||||
return function()
|
||||
|
||||
local inspect_entity = queue(remotes.inspect_entity)
|
||||
local update_inspect_settings = queue(remotes.update_inspect_settings)
|
||||
local stop_inspect_entity = queue(remotes.stop_inspect_entity)
|
||||
local update_entity = queue(remotes.update_entity)
|
||||
local delete_entity = queue(remotes.delete_entity)
|
||||
|
||||
local get_entity_component = queue(remotes.get_component)
|
||||
local validate_entity = queue(remotes.validate_entity_component)
|
||||
|
||||
local inspectors = {}
|
||||
|
||||
return function()
|
||||
|
||||
for incoming, world_id, ctype_name in validate_entity:iter() do
|
||||
local world: types.World = public[world_id]
|
||||
local outgoing = reverse_connector(incoming)
|
||||
|
||||
if not traffic_check.check_no_wl(incoming.host) then continue end
|
||||
if not world or world.class_name ~= "World" then continue end
|
||||
|
||||
local map_components = {}
|
||||
for id, name in world.world:query(jecs.Name):iter() do
|
||||
map_components[name] = id
|
||||
end
|
||||
|
||||
local ok, reason = pcall(get_component, ctype_name, map_components)
|
||||
|
||||
remotes.validate_entity_component_result:fire(
|
||||
outgoing, world_id, ctype_name, ok, not ok and reason or nil
|
||||
)
|
||||
end
|
||||
|
||||
for incoming, world_id, entity, inspect_id in inspect_entity:iter() do
|
||||
local world: types.World = public[world_id]
|
||||
local outgoing = reverse_connector(incoming)
|
||||
|
||||
if not traffic_check.check_no_wl(incoming.host) then continue end
|
||||
if not world or world.class_name ~= "World" then continue end
|
||||
|
||||
inspectors[inspect_id] = {
|
||||
outgoing = outgoing,
|
||||
world = world,
|
||||
entity = entity,
|
||||
paused = false,
|
||||
|
||||
new_values = {},
|
||||
old_values = {}
|
||||
}
|
||||
end
|
||||
|
||||
for incoming, inspect_id in stop_inspect_entity:iter() do
|
||||
if not traffic_check.check_no_wl(incoming.host) then continue end
|
||||
inspectors[inspect_id] = nil
|
||||
end
|
||||
|
||||
for incoming, inspect_id in delete_entity:iter() do
|
||||
if not traffic_check.check_no_wl(incoming.host) then continue end
|
||||
local inspect_data = inspectors[inspect_id]
|
||||
local world_data = inspect_data.world
|
||||
local world = world_data.world
|
||||
local entity = inspect_data.entity
|
||||
|
||||
world:delete(entity)
|
||||
end
|
||||
|
||||
for incoming, inspect_id, settings in update_inspect_settings:iter() do
|
||||
if not traffic_check.check_no_wl(incoming.host) then continue end
|
||||
local inspect_data = inspectors[inspect_id]
|
||||
if not inspect_data then continue end
|
||||
inspect_data.paused = settings.paused
|
||||
end
|
||||
|
||||
for incoming, inspect_id, component in get_entity_component:iter() do
|
||||
if not traffic_check.check_no_wl(incoming.host) then continue end
|
||||
local inspect_data = inspectors[inspect_id]
|
||||
local world_data = inspect_data.world
|
||||
local world = world_data.world
|
||||
local entity = inspect_data.entity
|
||||
local to = reverse_connector(incoming)
|
||||
|
||||
local map_components = {}
|
||||
for id, name in world:query(jecs.Name):iter() do
|
||||
map_components[name] = id
|
||||
end
|
||||
|
||||
local ok, component = pcall(get_component, component, map_components)
|
||||
|
||||
if component and ok then
|
||||
remotes.return_component:fire(to, inspect_id, component, lon.output(world:get(entity, component), true, false))
|
||||
else
|
||||
remotes.return_component:fire(to, inspect_id, component, "nil")
|
||||
end
|
||||
end
|
||||
|
||||
for incoming, inspect_id, changes in update_entity:iter() do
|
||||
if not traffic_check.check_no_wl(incoming.host) then continue end
|
||||
local inspect_data = inspectors[inspect_id]
|
||||
local world_data = inspect_data.world
|
||||
local world = world_data.world
|
||||
local entity = inspect_data.entity
|
||||
|
||||
local map_components = {}
|
||||
for id, name in world:query(jecs.Name):iter() do
|
||||
map_components[name] = id
|
||||
end
|
||||
|
||||
for ctype_name, value in changes do
|
||||
|
||||
-- get the component we need to set
|
||||
local ok, entity_to_set = pcall(get_component, ctype_name, map_components)
|
||||
local old = world:get(entity, entity_to_set)
|
||||
|
||||
if not ok then
|
||||
warn("attempted to set", ctype_name, "to", value)
|
||||
warn(entity_to_set)
|
||||
continue
|
||||
end
|
||||
|
||||
local ok, result = pcall(lon.parse, value)
|
||||
|
||||
if not ok then
|
||||
warn("attempted to set", ctype_name, "to", value)
|
||||
warn(result)
|
||||
continue
|
||||
end
|
||||
|
||||
local ok, data = pcall(
|
||||
lon.compile,
|
||||
result,
|
||||
{ tag = TAG, old = old }
|
||||
)
|
||||
|
||||
if not ok then
|
||||
warn("attempted to set", ctype_name, "to", value)
|
||||
warn(data)
|
||||
continue
|
||||
end
|
||||
|
||||
if data == nil then
|
||||
world:remove(entity, entity_to_set)
|
||||
elseif data == TAG then
|
||||
-- trying to use world:set with a tag will result in an error,
|
||||
-- even if the data is nil, so instead we use world:add
|
||||
world:add(entity, entity_to_set)
|
||||
else
|
||||
world:set(entity, entity_to_set, data)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
for inspect_id, inspector_data in inspectors do
|
||||
local world = inspector_data.world.world
|
||||
local entity = inspector_data.entity
|
||||
|
||||
if inspector_data.paused then continue end
|
||||
if world:contains(entity) == false then continue end
|
||||
|
||||
local new_values = inspector_data.new_values
|
||||
local old_values = inspector_data.old_values
|
||||
|
||||
local components = get_all_components(world, entity)
|
||||
|
||||
local function is_tag(id: jecs.Entity<any>)
|
||||
return jecs.is_tag(world, id)
|
||||
end
|
||||
|
||||
for _, component in components do
|
||||
local name = convert_component(world, jecs.Name, component)
|
||||
local is_tag = is_tag(component)
|
||||
|
||||
if is_tag then
|
||||
new_values[name] = TAG
|
||||
else
|
||||
local value = world:get(entity, component)
|
||||
new_values[name] = if value == nil then BAD_VALUE else value
|
||||
end
|
||||
end
|
||||
|
||||
for name, new_value in new_values do
|
||||
local old_value = old_values[name]
|
||||
|
||||
if old_value == new_value and typeof(new_value) ~= "table" then continue end
|
||||
remotes.inspect_entity_update:fire(
|
||||
inspector_data.outgoing,
|
||||
inspect_id,
|
||||
name,
|
||||
if new_value == TAG then "tag"
|
||||
--todo: figure out a better way to say that you are not allowed to store nil in a component
|
||||
elseif new_value == BAD_VALUE then "nil (not allowed)"
|
||||
else lon.output(new_value, true, true)
|
||||
)
|
||||
end
|
||||
|
||||
for name, value in old_values do
|
||||
local new_value = new_values[name]
|
||||
|
||||
if new_value ~= nil then continue end
|
||||
remotes.inspect_entity_update:fire(
|
||||
inspector_data.outgoing,
|
||||
inspect_id,
|
||||
name,
|
||||
nil
|
||||
)
|
||||
end
|
||||
|
||||
table.clear(old_values)
|
||||
|
||||
inspector_data.new_values = old_values
|
||||
inspector_data.old_values = new_values
|
||||
end
|
||||
end
|
||||
end
|
||||
209
modules/Jabby/server/systems/mouse_pointer.luau
Normal file
209
modules/Jabby/server/systems/mouse_pointer.luau
Normal file
|
|
@ -0,0 +1,209 @@
|
|||
local jecs = require(script.Parent.Parent.Parent.jecs)
|
||||
local queue = require(script.Parent.Parent.Parent.modules.queue)
|
||||
local remotes = require(script.Parent.Parent.Parent.modules.remotes)
|
||||
local reverse_connector = require(script.Parent.Parent.Parent.modules.reverse_connector)
|
||||
local traffic_check = require(script.Parent.Parent.Parent.modules.traffic_check)
|
||||
local types = require(script.Parent.Parent.Parent.modules.types)
|
||||
local public = require(script.Parent.Parent.public)
|
||||
|
||||
local entity_index_try_get = jecs.entity_index_try_get
|
||||
local IS_PAIR = jecs.IS_PAIR
|
||||
local pair_first = jecs.pair_first
|
||||
local pair_second = jecs.pair_second
|
||||
local empty_table = {}
|
||||
|
||||
|
||||
local function convert_component(world, debug, entity): string
|
||||
if IS_PAIR(entity) then
|
||||
local left = convert_component(world, debug, pair_first(world, entity))
|
||||
local right = convert_component(world, debug, pair_second(world, entity))
|
||||
return `({left}, {right})`
|
||||
else
|
||||
return world:get(entity, debug) or `${tostring(entity)}`
|
||||
end
|
||||
end
|
||||
|
||||
local function get_type(t: { [any]: any }): string
|
||||
local key, value = next(t)
|
||||
if key == nil then return "" end
|
||||
return `[{typeof(key)}]: {typeof(value)}`
|
||||
end
|
||||
|
||||
local function get_string_keys(t: { [any]: any }): ({ string }, boolean)
|
||||
local keys = {}
|
||||
local i = 0
|
||||
for key in t do
|
||||
if i > 3 then return keys, true end
|
||||
if typeof(key) ~= "string" then continue end
|
||||
table.insert(keys, key)
|
||||
i += 1
|
||||
end
|
||||
return keys, false
|
||||
end
|
||||
|
||||
local function is_tag(world: jecs.World, id: jecs.Entity<any>)
|
||||
return jecs.is_tag(world, id)
|
||||
end
|
||||
|
||||
local function get_all_components(world, entity): {}
|
||||
local record = entity_index_try_get(world.entity_index, entity)
|
||||
|
||||
if not record then return empty_table end
|
||||
local archetype = record.archetype
|
||||
if not archetype then return empty_table end
|
||||
|
||||
local components = {}
|
||||
for _, ty in archetype.types do
|
||||
table.insert(components, ty)
|
||||
end
|
||||
|
||||
table.sort(components, function(a, b)
|
||||
return if is_tag(world, a) and is_tag(world, b) then a < b
|
||||
elseif is_tag(world, a) then true
|
||||
elseif is_tag(world, b) then false
|
||||
else a < b
|
||||
end)
|
||||
|
||||
return components
|
||||
end
|
||||
|
||||
local function obtain_string(entity: jecs.Entity<any>, world: jecs.World)
|
||||
local MAX_SIZE = 840
|
||||
local has_more = false
|
||||
local entity_name = world:get(entity, jecs.Name)
|
||||
local strings = {`<b>{if entity_name then `{entity_name} #` else "#"}{entity}</b>\n`}
|
||||
local n = #strings[1]
|
||||
|
||||
for _, id in get_all_components(world, entity) do
|
||||
if id == jecs.Name then continue end
|
||||
local name = convert_component(world, jecs.Name, id)
|
||||
local value = if is_tag(world, id) then nil else world:get(entity, id)
|
||||
local to_append
|
||||
|
||||
if typeof(value) == "table" then
|
||||
local string_keys = get_string_keys(value)
|
||||
|
||||
if #string_keys > 0 then
|
||||
local temp_b = {`<b>{name}</b>:`}
|
||||
local temp_n = #temp_b[1]
|
||||
|
||||
for key, value in pairs(value) do
|
||||
if #temp_b > 0 then
|
||||
table.insert(temp_b, "\n")
|
||||
end
|
||||
|
||||
local str_of_v = if type(value) == "string" then `{value}`
|
||||
elseif typeof(value) == "table" then get_type(value)
|
||||
else tostring(value)
|
||||
if #str_of_v > 32 then
|
||||
str_of_v = `{string.sub(str_of_v, 1, 30)}..`
|
||||
end
|
||||
|
||||
local str = `{key}: {str_of_v}`
|
||||
|
||||
if temp_n + #str + 2 > 32 then
|
||||
table.insert(temp_b, "...")
|
||||
break
|
||||
else
|
||||
table.insert(temp_b, str)
|
||||
end
|
||||
end
|
||||
|
||||
to_append = `{table.concat(temp_b)}`
|
||||
else
|
||||
to_append = `<b>{name}</b>: {get_type(value)}`
|
||||
end
|
||||
elseif is_tag(world, id) then
|
||||
to_append = `<b>{name}</b>`
|
||||
else
|
||||
local value = tostring(value)
|
||||
if #value > 32 then
|
||||
to_append = `<b>{name}</b>: {string.sub(value, 1, 30)}..`
|
||||
else
|
||||
to_append = `<b>{name}</b>: {value}`
|
||||
end
|
||||
end
|
||||
|
||||
if MAX_SIZE > n + #to_append then
|
||||
n += #to_append
|
||||
table.insert(strings, to_append)
|
||||
else
|
||||
has_more = true
|
||||
end
|
||||
end
|
||||
|
||||
local str = table.concat(strings, "\n")
|
||||
if has_more then str = str .. "..." end
|
||||
|
||||
return str
|
||||
end
|
||||
|
||||
return function()
|
||||
|
||||
local send_mouse_pointer = queue(remotes.send_mouse_pointer)
|
||||
|
||||
return function()
|
||||
|
||||
for incoming, world_id, pos, dir in send_mouse_pointer:iter() do
|
||||
if not traffic_check.check_no_wl(incoming.host) then continue end
|
||||
local world_data: types.World = public[world_id]
|
||||
local world = world_data.world
|
||||
local outgoing = reverse_connector(incoming)
|
||||
|
||||
if world_data.entities == nil and world_data.get_entity_from_part == nil then continue end
|
||||
if not world_data or world_data.class_name ~= "World" then continue end
|
||||
|
||||
local result = workspace:Raycast(pos, dir * 1000)
|
||||
|
||||
if not result then
|
||||
remotes.send_mouse_entity:fire(
|
||||
outgoing,
|
||||
world_id
|
||||
)
|
||||
continue
|
||||
end
|
||||
|
||||
local part = result.Instance
|
||||
local entity
|
||||
|
||||
-- no way to obtain the entity
|
||||
if world_data.get_entity_from_part == nil and world_data.entities == nil then
|
||||
remotes.send_mouse_entity:fire(
|
||||
outgoing,
|
||||
world_id
|
||||
)
|
||||
continue
|
||||
end
|
||||
|
||||
if world_data.get_entity_from_part == nil then
|
||||
entity = world_data.entities[part]
|
||||
|
||||
while entity == nil and part.Parent ~= game do
|
||||
part, entity = part.Parent, world_data.entities[part]
|
||||
end
|
||||
else
|
||||
entity, part = world_data.get_entity_from_part(part)
|
||||
end
|
||||
|
||||
if not entity then
|
||||
remotes.send_mouse_entity:fire(
|
||||
outgoing,
|
||||
world_id
|
||||
)
|
||||
continue
|
||||
end
|
||||
|
||||
local str = obtain_string(entity, world, jecs.Name)
|
||||
|
||||
remotes.send_mouse_entity:fire(
|
||||
outgoing,
|
||||
world_id,
|
||||
part,
|
||||
entity,
|
||||
str
|
||||
)
|
||||
|
||||
end
|
||||
|
||||
end
|
||||
end
|
||||
28
modules/Jabby/server/systems/ping.luau
Normal file
28
modules/Jabby/server/systems/ping.luau
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
local Players = game:GetService("Players")
|
||||
|
||||
local net = require(script.Parent.Parent.Parent.modules.net)
|
||||
local queue = require(script.Parent.Parent.Parent.modules.queue)
|
||||
local remotes = require(script.Parent.Parent.Parent.modules.remotes)
|
||||
local reverse_connector = require(script.Parent.Parent.Parent.modules.reverse_connector)
|
||||
local traffic_check = require(script.Parent.Parent.Parent.modules.traffic_check)
|
||||
|
||||
|
||||
return function()
|
||||
|
||||
local ping = queue(remotes.ping)
|
||||
|
||||
for _, player in Players:GetPlayers() do
|
||||
if traffic_check.communication_is_allowed(net.local_host, player, true) then
|
||||
remotes.new_server_registered:fire({
|
||||
host = player,
|
||||
})
|
||||
end
|
||||
end
|
||||
|
||||
return function()
|
||||
for connector in ping:iter() do
|
||||
local outgoing = reverse_connector(connector)
|
||||
remotes.new_server_registered:fire(outgoing)
|
||||
end
|
||||
end
|
||||
end
|
||||
80
modules/Jabby/server/systems/replicate_core.luau
Normal file
80
modules/Jabby/server/systems/replicate_core.luau
Normal file
|
|
@ -0,0 +1,80 @@
|
|||
local queue = require(script.Parent.Parent.Parent.modules.queue)
|
||||
local remotes = require(script.Parent.Parent.Parent.modules.remotes)
|
||||
local reverse_connector = require(script.Parent.Parent.Parent.modules.reverse_connector)
|
||||
local traffic_check = require(script.Parent.Parent.Parent.modules.traffic_check)
|
||||
local public = require(script.Parent.Parent.public)
|
||||
|
||||
return function()
|
||||
|
||||
local connected = {}
|
||||
|
||||
local bind_to_core = queue(remotes.bind_to_server_core)
|
||||
|
||||
return function()
|
||||
|
||||
for connector in bind_to_core:iter() do
|
||||
local outgoing = reverse_connector(connector)
|
||||
|
||||
if not traffic_check.check_no_wl(connector.host) then continue end
|
||||
-- print("help")
|
||||
|
||||
table.insert(connected, outgoing)
|
||||
local schedulers = {}
|
||||
local worlds = {}
|
||||
|
||||
for idx, data in ipairs(public) do
|
||||
|
||||
if data.class_name == "Scheduler" then
|
||||
table.insert(schedulers, {
|
||||
name = data.name :: string,
|
||||
id = idx
|
||||
})
|
||||
elseif data.class_name == "World" then
|
||||
table.insert(worlds, {
|
||||
name = data.name :: string,
|
||||
id = idx
|
||||
})
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
remotes.update_server_data:fire(outgoing, {
|
||||
schedulers = schedulers,
|
||||
worlds = worlds
|
||||
})
|
||||
end
|
||||
|
||||
if public.updated == false then return end
|
||||
public.updated = false
|
||||
|
||||
local schedulers = {}
|
||||
local worlds = {}
|
||||
|
||||
for idx, data in ipairs(public) do
|
||||
|
||||
if data.class_name == "Scheduler" then
|
||||
table.insert(schedulers, {
|
||||
name = data.name :: string,
|
||||
id = idx
|
||||
})
|
||||
elseif data.class_name == "World" then
|
||||
table.insert(worlds, {
|
||||
name = data.name :: string,
|
||||
id = idx
|
||||
})
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
local fired_to = {}
|
||||
for _, connector in connected do
|
||||
if fired_to[connector] then return end
|
||||
fired_to[connector] = true
|
||||
remotes.update_server_data:fire(connector, {
|
||||
schedulers = schedulers,
|
||||
worlds = worlds
|
||||
})
|
||||
end
|
||||
|
||||
end
|
||||
end
|
||||
503
modules/Jabby/server/systems/replicate_registry.luau
Normal file
503
modules/Jabby/server/systems/replicate_registry.luau
Normal file
|
|
@ -0,0 +1,503 @@
|
|||
local jecs = require(script.Parent.Parent.Parent.jecs)
|
||||
local queue = require(script.Parent.Parent.Parent.modules.queue)
|
||||
local remotes = require(script.Parent.Parent.Parent.modules.remotes)
|
||||
local reverse_connector = require(script.Parent.Parent.Parent.modules.reverse_connector)
|
||||
local traffic_check = require(script.Parent.Parent.Parent.modules.traffic_check)
|
||||
local types = require(script.Parent.Parent.Parent.modules.types)
|
||||
local public = require(script.Parent.Parent.public)
|
||||
local query_parser = require(script.Parent.Parent.query_parser)
|
||||
|
||||
type Connection = {
|
||||
|
||||
outgoing: types.OutgoingConnector,
|
||||
query_id: number,
|
||||
frame: number,
|
||||
|
||||
paused: boolean,
|
||||
refresh: boolean,
|
||||
|
||||
world: types.World,
|
||||
|
||||
include: {jecs.Entity<any>},
|
||||
exclude: {jecs.Entity<any>},
|
||||
with: {jecs.Entity<any>},
|
||||
|
||||
new_columns: {{any}},
|
||||
old_columns: {{any}},
|
||||
|
||||
from: number,
|
||||
upto: number
|
||||
|
||||
}
|
||||
|
||||
local NIL = newproxy() -- NULL is displayed if the value exists, buth as no value
|
||||
|
||||
local function clear_columns(columns: {{any}})
|
||||
for _, column in columns do
|
||||
local name = column[1]
|
||||
table.clear(column)
|
||||
column[1] = name
|
||||
assert(column[1] == name)
|
||||
end
|
||||
return columns
|
||||
end
|
||||
|
||||
local function reverse_columns(columns: {{any}}, size: number)
|
||||
for _, column in columns do
|
||||
for i = 0, size // 2 - 1 do
|
||||
column[i + 2], column[(size + 1) - i] = column[(size + 1) - i], column[i + 2]
|
||||
end
|
||||
end
|
||||
return columns
|
||||
end
|
||||
|
||||
return function()
|
||||
|
||||
local processing_queries: {[number]: Connection} = {}
|
||||
|
||||
local validate_query = queue(remotes.validate_query)
|
||||
local request_query = queue(remotes.request_query)
|
||||
local disconnect_query = queue(remotes.disconnect_query)
|
||||
local advance_query_page = queue(remotes.advance_query_page)
|
||||
local pause_query = queue(remotes.pause_query)
|
||||
local refresh_query = queue(remotes.refresh_results)
|
||||
|
||||
local function check_if_query_valid(world: types.World, query: string): (boolean, string)
|
||||
local map_components = {}
|
||||
local ok, result = pcall(query_parser, query)
|
||||
local msg = nil
|
||||
|
||||
if not ok then
|
||||
return ok, result :: any
|
||||
end
|
||||
|
||||
for id, name in world.world:query(jecs.Name):iter() do
|
||||
map_components[name] = id
|
||||
end
|
||||
|
||||
local total_to_query = 0
|
||||
|
||||
for _, ctype in result do
|
||||
if not ok then break end
|
||||
|
||||
if ctype.query and not ctype.exclude then
|
||||
total_to_query += 1
|
||||
end
|
||||
|
||||
if ctype.type == "Component" then
|
||||
if ctype.value.type == "Entity" then
|
||||
if world.world:contains(ctype.value.entity) then continue end
|
||||
return false, "entity does not exist"
|
||||
elseif ctype.value.type == "Name" then
|
||||
if map_components[ctype.value.name] then continue end
|
||||
return false, `unknown component called {ctype.value.name}`
|
||||
else
|
||||
return false, "what"
|
||||
end
|
||||
elseif ctype.type == "Relationship" then
|
||||
local both_wildcard = ctype.left.type == "Wildcard" and ctype.right.type == "Wildcard"
|
||||
if both_wildcard then
|
||||
return false, `(*, *) is not a valid relationship`
|
||||
end
|
||||
|
||||
local left = ctype.left
|
||||
local right = ctype.right
|
||||
|
||||
if left.type == "Component" then
|
||||
if left.value.type == "Entity" then
|
||||
if world.world:contains(left.value.entity) then continue end
|
||||
return false, "entity does not exist"
|
||||
elseif left.value.type == "Name" then
|
||||
if map_components[left.value.name] then continue end
|
||||
return false, `unknown component called {left.value.name}`
|
||||
else
|
||||
return false, "what"
|
||||
end
|
||||
end
|
||||
|
||||
if right.type == "Component" then
|
||||
if right.value.type == "Entity" then
|
||||
if world.world:contains(right.value.entity) then continue end
|
||||
return false, "entity does not exist"
|
||||
elseif right.value.type == "Name" then
|
||||
if map_components[right.value.name] then continue end
|
||||
return false, `unknown component called {right.value.name}`
|
||||
else
|
||||
return false, "what"
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
if total_to_query > 26 then
|
||||
warn("attempting to observe too many values")
|
||||
return false, "attempting to observe too many entities"
|
||||
end
|
||||
|
||||
return ok, msg
|
||||
end
|
||||
|
||||
--fixme: contains is missing from types
|
||||
local function check_if_still_valid(world: any, entities: {any})
|
||||
for _, id in entities do
|
||||
if jecs.IS_PAIR(id) then
|
||||
if not (world:contains(jecs.pair_first(world, id) and jecs.pair_second(world, id))) then
|
||||
return false
|
||||
end
|
||||
elseif not world:contains(id) then
|
||||
return false
|
||||
end
|
||||
end
|
||||
return true
|
||||
end
|
||||
|
||||
local function get_terms(query: string, world: jecs.World)
|
||||
local result = query_parser(query)
|
||||
local include = {}
|
||||
local exclude = {}
|
||||
local with = {}
|
||||
|
||||
local map_components = {}
|
||||
local map_entity: {[any]: any} = {}
|
||||
for id, name in world:query(jecs.Name):iter() do
|
||||
map_components[name] = id
|
||||
end
|
||||
|
||||
local function get_entity(ctype: query_parser.PureComponent)
|
||||
local value = ctype.value
|
||||
|
||||
if value.type == "Entity" then
|
||||
return value.entity
|
||||
elseif value.type == "Name" then
|
||||
return map_components[value.name]
|
||||
end
|
||||
error("bad")
|
||||
end
|
||||
|
||||
for _, ctype in result do
|
||||
if ctype.type == "Component" then
|
||||
map_entity[ctype] = get_entity(ctype)
|
||||
elseif ctype.type == "Relationship" then
|
||||
local left, right = jecs.Wildcard, jecs.Wildcard
|
||||
|
||||
if ctype.left.type == "Component" then
|
||||
left = get_entity(ctype.left)
|
||||
end
|
||||
|
||||
if ctype.right.type == "Component" then
|
||||
right = get_entity(ctype.right)
|
||||
end
|
||||
|
||||
local pair = jecs.pair(left, right)
|
||||
map_entity[ctype] = pair
|
||||
end
|
||||
end
|
||||
|
||||
for _, ctype in result do
|
||||
local entity = map_entity[ctype]
|
||||
if ctype.exclude then
|
||||
table.insert(exclude, entity)
|
||||
elseif ctype.query then
|
||||
-- local name = if ctype.type == "Component" then ctype.name else `({ctype.left.name}, {ctype.right.name})`
|
||||
table.insert(include, entity)
|
||||
else
|
||||
table.insert(with, entity)
|
||||
end
|
||||
end
|
||||
|
||||
return include, exclude, with
|
||||
end
|
||||
|
||||
return function()
|
||||
|
||||
for incoming, world_id, query in validate_query:iter() do
|
||||
if not traffic_check.check_no_wl(incoming.host) then continue end
|
||||
|
||||
local world: types.World = public[world_id]
|
||||
local outgoing = reverse_connector(incoming)
|
||||
|
||||
if not world or world.class_name ~= "World" then
|
||||
remotes.validate_result:fire(outgoing, world_id, query, nil, false, "world does not exist")
|
||||
continue
|
||||
end
|
||||
|
||||
local ok, message = check_if_query_valid(world, query)
|
||||
local include, exclude, with
|
||||
|
||||
if ok then include, exclude, with = get_terms(query, world.world) end
|
||||
|
||||
remotes.validate_result:fire(outgoing, world_id, query, ok and {
|
||||
include = include,
|
||||
exclude = exclude,
|
||||
with = with
|
||||
}, ok, message)
|
||||
end
|
||||
|
||||
for incoming, query_id in disconnect_query:iter() do
|
||||
processing_queries[query_id] = nil
|
||||
end
|
||||
|
||||
for incoming, world_id, query_id, query in request_query:iter() do
|
||||
if not traffic_check.check_no_wl(incoming.host) then continue end
|
||||
|
||||
local world: types.World = public[world_id]
|
||||
local outgoing = reverse_connector(incoming)
|
||||
|
||||
if not world or world.class_name ~= "World" then continue end
|
||||
|
||||
local ok = check_if_query_valid(world, query)
|
||||
|
||||
if not ok then continue end
|
||||
|
||||
local include, exclude, with = get_terms(query, world.world)
|
||||
local new_columns = {}
|
||||
local old_columns = {}
|
||||
|
||||
table.insert(new_columns, {})
|
||||
table.insert(old_columns, {})
|
||||
|
||||
for _, ctype in include do
|
||||
table.insert(new_columns, {})
|
||||
table.insert(old_columns, {})
|
||||
end
|
||||
|
||||
if processing_queries[query_id] then
|
||||
local connection = processing_queries[query_id]
|
||||
|
||||
connection.outgoing = outgoing
|
||||
connection.query_id = query_id
|
||||
connection.world = world
|
||||
connection.refresh = true
|
||||
|
||||
connection.include = include
|
||||
connection.exclude = exclude
|
||||
connection.with = with
|
||||
|
||||
connection.new_columns = new_columns
|
||||
connection.old_columns = old_columns
|
||||
connection.from = 1
|
||||
connection.upto = 25
|
||||
else
|
||||
local connection: Connection = {
|
||||
|
||||
outgoing = outgoing,
|
||||
query_id = query_id,
|
||||
frame = 0,
|
||||
|
||||
world = world,
|
||||
|
||||
paused = false,
|
||||
refresh = false,
|
||||
|
||||
include = include,
|
||||
exclude = exclude,
|
||||
with = with,
|
||||
|
||||
new_columns = new_columns,
|
||||
old_columns = old_columns,
|
||||
|
||||
from = 1,
|
||||
upto = 25
|
||||
}
|
||||
|
||||
processing_queries[query_id] = connection
|
||||
end
|
||||
end
|
||||
|
||||
for incoming, query_id in refresh_query:iter() do
|
||||
if not traffic_check.check_no_wl(incoming.host) then continue end
|
||||
local query = processing_queries[query_id]
|
||||
if not query then continue end
|
||||
query.refresh = true
|
||||
end
|
||||
|
||||
for incoming, query_id, state in pause_query:iter() do
|
||||
if not traffic_check.check_no_wl(incoming.host) then continue end
|
||||
local query = processing_queries[query_id]
|
||||
if not query then continue end
|
||||
query.paused = state
|
||||
end
|
||||
|
||||
for incoming, query_id, from, to in advance_query_page:iter() do
|
||||
if not traffic_check.check_no_wl(incoming.host) then continue end
|
||||
local query = processing_queries[query_id]
|
||||
if not query then continue end
|
||||
|
||||
query.refresh = true
|
||||
query.from = from
|
||||
query.upto = to
|
||||
end
|
||||
|
||||
for query_id, query_data in processing_queries do
|
||||
if query_data.paused and query_data.refresh ~= true then continue end
|
||||
debug.profilebegin("process query")
|
||||
query_data.refresh = false
|
||||
local world_data = query_data.world
|
||||
local world = world_data.world
|
||||
local debug_trait = jecs.Name
|
||||
|
||||
if
|
||||
not (check_if_still_valid(world, query_data.include)
|
||||
and check_if_still_valid(world, query_data.exclude)
|
||||
and check_if_still_valid(world, query_data.with))
|
||||
then
|
||||
-- query is no longer valid!
|
||||
--todo: query is invalid, notify the client about this
|
||||
debug.profileend()
|
||||
continue
|
||||
end
|
||||
|
||||
local query = world:query(unpack(query_data.include))
|
||||
|
||||
if #query_data.exclude > 0 then
|
||||
query = query:without(unpack(query_data.exclude))
|
||||
end
|
||||
|
||||
if #query_data.with > 0 then
|
||||
query = query:with(unpack(query_data.with))
|
||||
end
|
||||
|
||||
local from = query_data.from
|
||||
local upto = query_data.upto
|
||||
|
||||
local new_columns = query_data.new_columns
|
||||
local old_columns = query_data.old_columns
|
||||
-- set the names of each column
|
||||
--todo: fix type
|
||||
local function get_name(entity: any)
|
||||
if jecs.IS_PAIR(entity) then
|
||||
local left = jecs.pair_first(world, entity)
|
||||
local right = jecs.pair_second(world, entity)
|
||||
return `({get_name(left)}, {get_name(right)})`
|
||||
elseif entity == jecs.Wildcard :: any then
|
||||
return "*"
|
||||
elseif world:has(entity, debug_trait) then
|
||||
return world:get(entity, debug_trait)
|
||||
else
|
||||
return `${entity}`
|
||||
end
|
||||
end
|
||||
|
||||
-- set column names
|
||||
for index, column in new_columns do
|
||||
local e = query_data.include[index - 1]
|
||||
|
||||
if e then
|
||||
column[1] = get_name(e)
|
||||
else
|
||||
column[1] = "id"
|
||||
end
|
||||
end
|
||||
|
||||
-- process the data into columns
|
||||
-- we inline the query here, as jecs queries are in reverse to prevent iterator invalidation
|
||||
-- this is usually fine, but it's annoying, as now entities are added to the first page.
|
||||
--todo: pause button
|
||||
local total_entities = 0
|
||||
local archetypes = query:archetypes()
|
||||
|
||||
for _, archetype: jecs.Archetype in archetypes do
|
||||
total_entities += #archetype.entities
|
||||
end
|
||||
|
||||
local entities = table.create(total_entities)
|
||||
local at = total_entities
|
||||
local row_entity = 1
|
||||
|
||||
for _, archetype: jecs.Archetype in archetypes do
|
||||
for row = #archetype.entities, 1, -1 do
|
||||
local entity = archetype.entities[row]
|
||||
table.insert(entities, entity)
|
||||
end
|
||||
end
|
||||
|
||||
table.sort(entities)
|
||||
|
||||
for i = from, upto do
|
||||
row_entity += 1
|
||||
local entity = entities[i]
|
||||
if not entity then continue end
|
||||
new_columns[1][row_entity] = entity
|
||||
for idx, ctype in query_data.include do
|
||||
local value = world:get(entity, ctype)
|
||||
new_columns[idx + 1][row_entity] = if value == nil then NIL else value
|
||||
end
|
||||
end
|
||||
|
||||
--- reverse the order of each array
|
||||
|
||||
remotes.count_total_entities:fire(
|
||||
query_data.outgoing,
|
||||
query_id,
|
||||
total_entities
|
||||
)
|
||||
|
||||
-- diff the columns and replicate any new values
|
||||
for column = 1, math.max(#new_columns, #old_columns) do
|
||||
for row = 1, upto do
|
||||
local new_value = new_columns[column][row]
|
||||
local old_value = old_columns[column][row]
|
||||
|
||||
if new_value ~= old_value or typeof(new_value) == "table" then
|
||||
-- todo: improve replication of the new value
|
||||
-- ideally, we would figure out if the value is a certain type and needs special replication
|
||||
-- if we for example determine a value is a string, or table, we cap it at MAX_CHARACTERS
|
||||
-- or we tostring a couple keys of the table until we reach MAX_CHARACTERS.
|
||||
-- we wanna be able to replicate every single. value
|
||||
|
||||
local MAX_CHARS = 750
|
||||
local str
|
||||
|
||||
if typeof(new_value) == "string" then
|
||||
str = `"{string.sub(new_value, 1, MAX_CHARS-2)}"`
|
||||
elseif typeof(new_value) == "table" then
|
||||
local temp_n = 0
|
||||
local temp_b = {}
|
||||
|
||||
for key, value in new_value do
|
||||
if #temp_b > 0 then
|
||||
table.insert(temp_b, "; ")
|
||||
end
|
||||
|
||||
local str_of_v = if type(value) == "string" then `"{value}"` else tostring(value)
|
||||
local str = `{key}: {str_of_v}`
|
||||
|
||||
if temp_n + #str + 2 > MAX_CHARS then
|
||||
table.insert(temp_b, "...")
|
||||
break
|
||||
else
|
||||
table.insert(temp_b, str)
|
||||
end
|
||||
end
|
||||
|
||||
str = `\{{table.concat(temp_b)}\}`
|
||||
elseif new_value == NIL then
|
||||
str = "" -- important distinction, this is still a valid component
|
||||
elseif new_value == nil then
|
||||
str = nil -- but this isnt
|
||||
else
|
||||
str = string.sub(tostring(new_value), 1, MAX_CHARS-2)
|
||||
end
|
||||
|
||||
if row == 1 then str = new_value end
|
||||
remotes.update_query_result:fire(
|
||||
query_data.outgoing,
|
||||
query_id,
|
||||
query_data.frame,
|
||||
column,
|
||||
row,
|
||||
str
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
query_data.new_columns = clear_columns(old_columns)
|
||||
query_data.old_columns = new_columns
|
||||
query_data.frame += 1
|
||||
debug.profileend()
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
end
|
||||
104
modules/Jabby/server/systems/replicate_scheduler.luau
Normal file
104
modules/Jabby/server/systems/replicate_scheduler.luau
Normal file
|
|
@ -0,0 +1,104 @@
|
|||
--!nolint LocalShadow
|
||||
local hash = require(script.Parent.Parent.Parent.modules.hash_connector)
|
||||
local queue = require(script.Parent.Parent.Parent.modules.queue)
|
||||
local remotes = require(script.Parent.Parent.Parent.modules.remotes)
|
||||
local reverse_connector = require(script.Parent.Parent.Parent.modules.reverse_connector)
|
||||
local traffic_check = require(script.Parent.Parent.Parent.modules.traffic_check)
|
||||
local types = require(script.Parent.Parent.Parent.modules.types)
|
||||
local public = require(script.Parent.Parent.public)
|
||||
|
||||
return function()
|
||||
|
||||
local connected = {}
|
||||
|
||||
local request_scheduler = queue(remotes.request_scheduler)
|
||||
local disconnect_scheduler = queue(remotes.disconnect_scheduler)
|
||||
local schedule_pause = queue(remotes.scheduler_system_pause)
|
||||
|
||||
return function()
|
||||
|
||||
for incoming, id in request_scheduler:iter() do
|
||||
if not traffic_check.check_no_wl(incoming.host) then continue end
|
||||
|
||||
local scheduler = public[id]
|
||||
|
||||
if scheduler.class_name ~= "Scheduler" then continue end
|
||||
if scheduler == nil then continue end
|
||||
|
||||
local outgoing = reverse_connector(incoming)
|
||||
connected[id] = connected[id] or {}
|
||||
table.insert(connected[id], outgoing)
|
||||
-- print("connected")
|
||||
|
||||
for system_id, data in scheduler.system_data do
|
||||
remotes.scheduler_system_static_update:fire(outgoing, id, system_id, data)
|
||||
end
|
||||
|
||||
for system_id, frames in scheduler.system_frames do
|
||||
local frame = frames[1]
|
||||
if not frame then continue end
|
||||
remotes.scheduler_system_update:fire(outgoing, id, system_id, frame.i, frame.s)
|
||||
end
|
||||
end
|
||||
|
||||
for incoming, id in disconnect_scheduler:iter() do
|
||||
if not traffic_check.check_no_wl(incoming.host) then continue end
|
||||
|
||||
if not connected[id] then continue end
|
||||
|
||||
local scheduler_connected = connected[id]
|
||||
|
||||
for i = #scheduler_connected, 1, -1 do
|
||||
local connector = scheduler_connected[i]
|
||||
if connector.host ~= incoming.host then continue end
|
||||
if connector.to_vm ~= incoming.from_vm then continue end
|
||||
scheduler_connected[i] = scheduler_connected[#scheduler_connected]
|
||||
scheduler_connected[#scheduler_connected] = nil
|
||||
break
|
||||
end
|
||||
end
|
||||
|
||||
for incoming, id, system, paused in schedule_pause:iter() do
|
||||
if not traffic_check.check_no_wl(incoming.host) then continue end
|
||||
local scheduler: types.Scheduler = public[id]
|
||||
|
||||
if not scheduler then return end
|
||||
|
||||
scheduler:set_system_data(system, {
|
||||
paused = paused
|
||||
})
|
||||
end
|
||||
|
||||
for id, connected in connected do
|
||||
local scheduler: types.Scheduler = public[id]
|
||||
if #connected == 0 then continue end
|
||||
|
||||
for system_id in scheduler.system_data_updated do
|
||||
local map = {}
|
||||
local data = scheduler.system_data[system_id]
|
||||
|
||||
for _, connector in connected do
|
||||
if map[hash(connector)] then continue end
|
||||
map[hash(connector)] = true
|
||||
remotes.scheduler_system_static_update:fire(connector, id, system_id, data)
|
||||
end
|
||||
|
||||
scheduler.system_data_updated[system_id] = nil
|
||||
end
|
||||
|
||||
for system_id, frames in scheduler.system_frames_updated do
|
||||
local map = {}
|
||||
for frame in frames do
|
||||
for _, connector in connected do
|
||||
if map[hash(connector)] then continue end
|
||||
map[hash(connector)] = true
|
||||
remotes.scheduler_system_update:fire(connector, id, system_id, frame.i, frame.s)
|
||||
end
|
||||
end
|
||||
table.clear(frames)
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
end
|
||||
end
|
||||
175
modules/Jabby/server/systems/replicate_system_watch.luau
Normal file
175
modules/Jabby/server/systems/replicate_system_watch.luau
Normal file
|
|
@ -0,0 +1,175 @@
|
|||
--[[
|
||||
|
||||
Handles the API for the system watch.
|
||||
Users will be able to add watches to their systems to track changes.
|
||||
|
||||
Users will be able to learn about what actions a system performs on a jecs world
|
||||
through this.
|
||||
|
||||
Hooked API's:
|
||||
|
||||
component()
|
||||
entity()
|
||||
remove()
|
||||
clear()
|
||||
delete()
|
||||
add()
|
||||
set()
|
||||
|
||||
]]
|
||||
|
||||
local jecs = require(script.Parent.Parent.Parent.jecs)
|
||||
local hash_connector = require(script.Parent.Parent.Parent.modules.hash_connector)
|
||||
local lon = require(script.Parent.Parent.Parent.modules.lon)
|
||||
local queue = require(script.Parent.Parent.Parent.modules.queue)
|
||||
local remotes = require(script.Parent.Parent.Parent.modules.remotes)
|
||||
local reverse_connector = require(script.Parent.Parent.Parent.modules.reverse_connector)
|
||||
local types = require(script.Parent.Parent.Parent.modules.types)
|
||||
local public = require(script.Parent.Parent.public)
|
||||
local watch = require(script.Parent.Parent.watch)
|
||||
|
||||
local NIL = watch.NIL
|
||||
|
||||
return function()
|
||||
|
||||
local stored_watches = {}
|
||||
local connected_watches = {}
|
||||
|
||||
local function create_watch_for_id(
|
||||
scheduler: types.Scheduler,
|
||||
system: types.SystemId,
|
||||
watch_id: number
|
||||
)
|
||||
local watch = scheduler:create_watch_for_system(system)
|
||||
stored_watches[watch_id] = watch
|
||||
end
|
||||
|
||||
local function send_watch_data_to(host: types.OutgoingConnector, watch_id: number, frame: number)
|
||||
local map_worlds_to_name = {}
|
||||
local watch = stored_watches[watch_id]
|
||||
local frames = watch.watch.frames
|
||||
local data = frames[frame]
|
||||
|
||||
if not data then
|
||||
remotes.update_watch_data:fire(host, watch_id, frame, nil)
|
||||
return
|
||||
end
|
||||
|
||||
for _, world in ipairs(public) do
|
||||
if world.world == nil then continue end
|
||||
map_worlds_to_name[world.world] = jecs.Name
|
||||
end
|
||||
|
||||
local to_send = {
|
||||
types = data.types,
|
||||
entities = data.entities,
|
||||
component = table.clone(data.component),
|
||||
values = table.clone(data.values)
|
||||
}
|
||||
|
||||
for idx, ctype in to_send.component do
|
||||
local world = data.worlds[idx]
|
||||
to_send.component[idx] = world:get(ctype, map_worlds_to_name[world]) or ctype
|
||||
end
|
||||
|
||||
for idx, value in to_send.values do
|
||||
if value == NIL then to_send.values[idx] = "" end
|
||||
to_send.values[idx] = lon.output(value, false)
|
||||
end
|
||||
|
||||
remotes.update_watch_data:fire(host, watch_id, frame, to_send :: any)
|
||||
end
|
||||
|
||||
local function remove_watch_id(watch_id: number)
|
||||
if not stored_watches[watch_id] then return end
|
||||
stored_watches[watch_id].untrack()
|
||||
stored_watches[watch_id] = nil
|
||||
connected_watches[watch_id] = nil
|
||||
end
|
||||
|
||||
local function start_record_watch(watch_id: number)
|
||||
local watch = stored_watches[watch_id]
|
||||
watch.active = true
|
||||
end
|
||||
|
||||
local function stop_record_watch(watch_id: number)
|
||||
local watch = stored_watches[watch_id]
|
||||
watch.active = false
|
||||
end
|
||||
|
||||
local function connect_watch(host: types.OutgoingConnector, watch_id: number)
|
||||
connected_watches[watch_id] = connected_watches[watch_id] or {}
|
||||
connected_watches[watch_id][hash_connector(host)] = host
|
||||
|
||||
local watch = stored_watches[watch_id]
|
||||
for i, frame in watch.watch.frames do
|
||||
remotes.update_overview:fire(host, watch_id, i, #frame.types)
|
||||
end
|
||||
end
|
||||
|
||||
local function disconnect_watch(host: types.OutgoingConnector, watch_id: number)
|
||||
if not connected_watches[watch_id] then return end
|
||||
connected_watches[watch_id][hash_connector(host)] = nil
|
||||
end
|
||||
|
||||
local request_create_watch = queue(remotes.create_watch)
|
||||
local request_remove_watch = queue(remotes.remove_watch)
|
||||
local request_watch_data = queue(remotes.request_watch_data)
|
||||
local request_stop_watch = queue(remotes.stop_watch)
|
||||
local request_record_watch = queue(remotes.start_record_watch)
|
||||
local request_connect_watch = queue(remotes.connect_watch)
|
||||
local request_disconnect_watch = queue(remotes.disconnect_watch)
|
||||
-- local set_lon_enabled = queue(remotes.set_lon_enabled)
|
||||
|
||||
return function()
|
||||
|
||||
for from, scheduler_id, system, watch_id in request_create_watch:iter() do
|
||||
local scheduler = public[scheduler_id]
|
||||
|
||||
if scheduler.class_name ~= "Scheduler" then continue end
|
||||
if scheduler == nil then continue end
|
||||
|
||||
create_watch_for_id(scheduler, system, watch_id)
|
||||
end
|
||||
|
||||
for from, watch_id in request_stop_watch:iter() do
|
||||
stop_record_watch(watch_id)
|
||||
end
|
||||
|
||||
for from, watch_id in request_remove_watch:iter() do
|
||||
remove_watch_id(watch_id)
|
||||
end
|
||||
|
||||
for from, watch_id, frame in request_watch_data:iter() do
|
||||
send_watch_data_to(
|
||||
reverse_connector(from),
|
||||
watch_id,
|
||||
frame
|
||||
)
|
||||
end
|
||||
|
||||
for from, watch_id in request_record_watch:iter() do
|
||||
start_record_watch(watch_id)
|
||||
end
|
||||
|
||||
for from, watch_id in request_connect_watch:iter() do
|
||||
connect_watch(reverse_connector(from), watch_id)
|
||||
end
|
||||
|
||||
for from, watch_id in request_disconnect_watch:iter() do
|
||||
disconnect_watch(from, watch_id)
|
||||
end
|
||||
|
||||
for watch_id, hosts in connected_watches do
|
||||
local watch = stored_watches[watch_id]
|
||||
local current_frame = watch.watch.frame
|
||||
local frame_data = watch.watch.frames[current_frame] or {types = {}}
|
||||
local changes = #frame_data.types
|
||||
|
||||
for _, host in hosts do
|
||||
remotes.update_overview:fire(host, watch_id, current_frame, changes)
|
||||
end
|
||||
end
|
||||
|
||||
end
|
||||
end
|
||||
129
modules/Jabby/server/watch.luau
Normal file
129
modules/Jabby/server/watch.luau
Normal file
|
|
@ -0,0 +1,129 @@
|
|||
local types = require(script.Parent.Parent.modules.types)
|
||||
local world_hook = require(script.Parent.world_hook)
|
||||
|
||||
local NIL = newproxy()
|
||||
|
||||
type ChangeTypes = "remove" | "clear" | "delete" | "add" | "set" | "entity" | "component"
|
||||
type Changes = types.WatchLoggedChanges
|
||||
|
||||
export type SystemWatch = {
|
||||
--- enables Lua Object Notation.
|
||||
--- incurs a significant performance penalty.
|
||||
enable_lon: boolean,
|
||||
--- the current frame to process
|
||||
frame: number,
|
||||
|
||||
frames: {[number]: Changes}
|
||||
}
|
||||
|
||||
local function create_changes()
|
||||
return {
|
||||
types = {},
|
||||
entities = {},
|
||||
component = {},
|
||||
values = {},
|
||||
worlds = {}
|
||||
}
|
||||
end
|
||||
|
||||
local function step_watch(watch: SystemWatch)
|
||||
watch.frame += 1
|
||||
watch.frames[watch.frame] = create_changes()
|
||||
end
|
||||
|
||||
local function track_watch(watch: SystemWatch)
|
||||
|
||||
local hooks = {
|
||||
|
||||
world_hook.hook_onto("remove", function(self, id, component)
|
||||
local frame = watch.frames[watch.frame]
|
||||
|
||||
table.insert(frame.types, "remove")
|
||||
table.insert(frame.entities, id)
|
||||
table.insert(frame.component, component)
|
||||
table.insert(frame.values, NIL)
|
||||
table.insert(frame.worlds, self)
|
||||
end),
|
||||
|
||||
world_hook.hook_onto("clear", function(self, id)
|
||||
local frame = watch.frames[watch.frame]
|
||||
|
||||
table.insert(frame.types, "clear")
|
||||
table.insert(frame.entities, id)
|
||||
table.insert(frame.component, NIL)
|
||||
table.insert(frame.values, NIL)
|
||||
table.insert(frame.worlds, self)
|
||||
end),
|
||||
|
||||
world_hook.hook_onto("delete", function(self, id)
|
||||
local frame = watch.frames[watch.frame]
|
||||
|
||||
table.insert(frame.types, "delete")
|
||||
table.insert(frame.entities, id)
|
||||
table.insert(frame.component, NIL)
|
||||
table.insert(frame.values, NIL)
|
||||
table.insert(frame.worlds, self)
|
||||
end),
|
||||
|
||||
world_hook.hook_onto("add", function(self, id, component)
|
||||
local frame = watch.frames[watch.frame]
|
||||
|
||||
table.insert(frame.types, "add")
|
||||
table.insert(frame.entities, id)
|
||||
table.insert(frame.component, component)
|
||||
table.insert(frame.values, NIL)
|
||||
table.insert(frame.worlds, self)
|
||||
end),
|
||||
|
||||
world_hook.hook_onto("set", function(self, entity, component, value)
|
||||
if self:has(entity, component) then
|
||||
local frame = watch.frames[watch.frame]
|
||||
|
||||
table.insert(frame.types, "change")
|
||||
table.insert(frame.entities, entity)
|
||||
table.insert(frame.component, component)
|
||||
table.insert(frame.values, value)
|
||||
table.insert(frame.worlds, self)
|
||||
else
|
||||
local frame = watch.frames[watch.frame]
|
||||
|
||||
table.insert(frame.types, "move")
|
||||
table.insert(frame.entities, entity)
|
||||
table.insert(frame.component, component)
|
||||
table.insert(frame.values, value)
|
||||
table.insert(frame.worlds, self)
|
||||
end
|
||||
end)
|
||||
|
||||
}
|
||||
|
||||
--- stops all hooks
|
||||
local function stop_hook()
|
||||
for _, destroy in hooks do
|
||||
destroy()
|
||||
end
|
||||
end
|
||||
|
||||
return stop_hook
|
||||
end
|
||||
|
||||
local function create_watch()
|
||||
local watch: SystemWatch = {
|
||||
enable_lon = false,
|
||||
|
||||
frame = 0,
|
||||
frames = {}
|
||||
}
|
||||
|
||||
return watch
|
||||
end
|
||||
|
||||
return {
|
||||
|
||||
create_watch = create_watch,
|
||||
track_watch = track_watch,
|
||||
step_watch = step_watch,
|
||||
|
||||
NIL = NIL
|
||||
|
||||
}
|
||||
75
modules/Jabby/server/world_hook.luau
Normal file
75
modules/Jabby/server/world_hook.luau
Normal file
|
|
@ -0,0 +1,75 @@
|
|||
|
||||
local public = require(script.Parent.public)
|
||||
|
||||
local function i_hook_onto(key: string, hooks: {(...any) -> ()})
|
||||
|
||||
local to_unhook = {}
|
||||
for _, world_data in ipairs(public) do
|
||||
local world = world_data.world
|
||||
if not world then continue end
|
||||
|
||||
local method: any = world[key]
|
||||
|
||||
assert(typeof(method) == "function", "can only hook onto functions")
|
||||
|
||||
-- create a new wrapper function
|
||||
local function run_hook(...)
|
||||
for _, hook in hooks do
|
||||
hook(...)
|
||||
end
|
||||
return method(...)
|
||||
end
|
||||
|
||||
-- print(debug.info(world[key], "s"))
|
||||
world[key] = run_hook
|
||||
to_unhook[world] = method
|
||||
end
|
||||
|
||||
return function()
|
||||
for world, method in to_unhook do
|
||||
world[key] = method
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local hooks = {}
|
||||
|
||||
local function find_swap_pop<T>(list: {T}, value: T)
|
||||
local idx = table.find(list, value)
|
||||
if not idx then return end
|
||||
list[idx] = list[#list]
|
||||
list[#list] = nil
|
||||
end
|
||||
|
||||
local function hook_onto(key: string, hook: (...any) -> ())
|
||||
|
||||
if hooks[key] == nil then
|
||||
local callbacks = {}
|
||||
local cleanup = i_hook_onto(key, callbacks)
|
||||
hooks[key] = {
|
||||
cleanup = cleanup,
|
||||
callbacks = callbacks
|
||||
}
|
||||
end
|
||||
|
||||
local hook_info = hooks[key]
|
||||
local dead = false
|
||||
table.insert(hook_info.callbacks, hook)
|
||||
|
||||
local function unhook()
|
||||
if dead then return end
|
||||
dead = true
|
||||
find_swap_pop(hook_info.callbacks, hook)
|
||||
|
||||
if hook_info.callbacks[1] == nil then
|
||||
hook_info.cleanup()
|
||||
hooks[key] = nil
|
||||
end
|
||||
end
|
||||
|
||||
return unhook
|
||||
end
|
||||
|
||||
return {
|
||||
hook_onto = hook_onto
|
||||
}
|
||||
121
modules/Jabby/ui/components/display/accordion.luau
Normal file
121
modules/Jabby/ui/components/display/accordion.luau
Normal file
|
|
@ -0,0 +1,121 @@
|
|||
local vide = require(script.Parent.Parent.Parent.Parent.vide)
|
||||
local theme = require(script.Parent.Parent.Parent.util.theme)
|
||||
local container = require(script.Parent.Parent.util.container)
|
||||
local padding = require(script.Parent.Parent.util.padding)
|
||||
local typography = require(script.Parent.typography)
|
||||
|
||||
local create = vide.create
|
||||
local source = vide.source
|
||||
local changed = vide.changed
|
||||
local spring = vide.spring
|
||||
|
||||
local CHEVRON_DOWN = "rbxassetid://10709790948"
|
||||
local CHEVRON_UP = "rbxassetid://10709791523"
|
||||
|
||||
type can<T> = T | () -> T
|
||||
type props = {
|
||||
text: can<string>,
|
||||
expanded: () -> boolean,
|
||||
set_expanded: (boolean) -> (),
|
||||
|
||||
[any]: any
|
||||
}
|
||||
|
||||
return function(props: props)
|
||||
|
||||
local gui_state = source(Enum.GuiState.Idle)
|
||||
local container_size = source(Vector2.zero)
|
||||
|
||||
return container {
|
||||
Name = "Accordion",
|
||||
Size = spring(function()
|
||||
if props.expanded() == false then return UDim2.new(1, 0, 0, 32) end
|
||||
return UDim2.new(1, 0, 0, 40 + container_size().Y)
|
||||
end, 0.1),
|
||||
ClipsDescendants = true,
|
||||
|
||||
create "ImageButton" {
|
||||
Name = "Accordion",
|
||||
AutoLocalize = false,
|
||||
Size = UDim2.new(1, 0, 0, 32),
|
||||
|
||||
BackgroundColor3 = spring(function()
|
||||
return if gui_state() == Enum.GuiState.Press then
|
||||
theme.bg[-1]()
|
||||
elseif gui_state() == Enum.GuiState.Hover then
|
||||
theme.bg[3]()
|
||||
else
|
||||
theme.bg[0]()
|
||||
end, 0.1),
|
||||
|
||||
padding {x = UDim.new(0, 8)},
|
||||
create "UIListLayout" {
|
||||
FillDirection = Enum.FillDirection.Horizontal,
|
||||
HorizontalFlex = Enum.UIFlexAlignment.SpaceBetween,
|
||||
VerticalAlignment = Enum.VerticalAlignment.Center,
|
||||
Padding = UDim.new(0, 8)
|
||||
},
|
||||
|
||||
create "UICorner" {
|
||||
CornerRadius = UDim.new(0, 8)
|
||||
},
|
||||
|
||||
container {
|
||||
Size = UDim2.fromOffset(16, 16),
|
||||
create "ImageLabel" {
|
||||
Size = UDim2.fromOffset(16, 16),
|
||||
BackgroundTransparency = 1,
|
||||
AutoLocalize = false,
|
||||
Image = CHEVRON_DOWN,
|
||||
|
||||
Rotation = spring(function()
|
||||
return if props.expanded() then 180 else 0
|
||||
end, 0.1)
|
||||
},
|
||||
},
|
||||
|
||||
typography {
|
||||
size = UDim2.fromScale(0, 1),
|
||||
text = props.text,
|
||||
truncate = Enum.TextTruncate.SplitWord,
|
||||
xalignment = Enum.TextXAlignment.Left,
|
||||
|
||||
create "UIFlexItem" {
|
||||
FlexMode = Enum.UIFlexMode.Fill
|
||||
}
|
||||
},
|
||||
|
||||
Activated = function()
|
||||
props.set_expanded(not props.expanded())
|
||||
end,
|
||||
|
||||
changed("GuiState", gui_state)
|
||||
|
||||
},
|
||||
|
||||
container {
|
||||
Name = "Children",
|
||||
|
||||
Position = UDim2.fromOffset(0, 40),
|
||||
AutomaticSize = Enum.AutomaticSize.None,
|
||||
Size = function()
|
||||
return UDim2.new(1, 0, 0, container_size().Y)
|
||||
end,
|
||||
BackgroundColor3 = theme.bg[3],
|
||||
|
||||
ClipsDescendants = true,
|
||||
|
||||
container {
|
||||
Size = UDim2.fromScale(1, 0),
|
||||
AutomaticSize = Enum.AutomaticSize.Y,
|
||||
|
||||
unpack(props),
|
||||
|
||||
changed("AbsoluteSize", container_size)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
end
|
||||
46
modules/Jabby/ui/components/display/background.luau
Normal file
46
modules/Jabby/ui/components/display/background.luau
Normal file
|
|
@ -0,0 +1,46 @@
|
|||
local vide = require(script.Parent.Parent.Parent.Parent.vide)
|
||||
local theme = require(script.Parent.Parent.Parent.util.theme)
|
||||
|
||||
local create = vide.create
|
||||
local read = vide.read
|
||||
|
||||
type can<T> = (() -> T) | T
|
||||
|
||||
type Background = {
|
||||
position: can<UDim2>?,
|
||||
size: can<UDim2>?,
|
||||
anchorpoint: can<UDim2>?,
|
||||
automaticsize: can<Enum.AutomaticSize>?,
|
||||
|
||||
layoutorder: can<number>?,
|
||||
zindex: can<number>?,
|
||||
|
||||
depth: can<number>?,
|
||||
accent: can<boolean>?,
|
||||
|
||||
[number]: any
|
||||
}
|
||||
|
||||
return function(props: Background)
|
||||
|
||||
return create "Frame" {
|
||||
Position = props.position,
|
||||
Size = props.size or UDim2.fromScale(1, 1),
|
||||
AnchorPoint = props.anchorpoint,
|
||||
AutomaticSize = props.automaticsize,
|
||||
AutoLocalize = false,
|
||||
|
||||
LayoutOrder = props.layoutorder,
|
||||
|
||||
ZIndex = props.zindex,
|
||||
|
||||
BackgroundColor3 = function()
|
||||
return
|
||||
if read(props.accent) then theme.acc[read(props.depth) or 0]()
|
||||
else theme.bg[read(props.depth) or 0]()
|
||||
end,
|
||||
|
||||
unpack(props)
|
||||
|
||||
}
|
||||
end
|
||||
64
modules/Jabby/ui/components/display/checkbox.luau
Normal file
64
modules/Jabby/ui/components/display/checkbox.luau
Normal file
|
|
@ -0,0 +1,64 @@
|
|||
local vide = require(script.Parent.Parent.Parent.Parent.vide)
|
||||
local anim = require(script.Parent.Parent.Parent.util.anim)
|
||||
local theme = require(script.Parent.Parent.Parent.util.theme)
|
||||
|
||||
local create = vide.create
|
||||
local read = vide.read
|
||||
|
||||
type can<T> = (() -> T) | T
|
||||
|
||||
type Background = {
|
||||
position: can<UDim2>?,
|
||||
size: can<UDim2>?,
|
||||
anchorpoint: can<UDim2>?,
|
||||
automaticsize: can<Enum.AutomaticSize>?,
|
||||
|
||||
layoutorder: can<number>?,
|
||||
zindex: can<number>?,
|
||||
|
||||
checked: can<boolean>,
|
||||
|
||||
[number]: any
|
||||
}
|
||||
|
||||
return function(props: Background)
|
||||
|
||||
return create "Frame" {
|
||||
Position = props.position,
|
||||
Size = props.size or UDim2.fromOffset(24, 24),
|
||||
AnchorPoint = props.anchorpoint or Vector2.new(0.5, 0.5),
|
||||
AutomaticSize = props.automaticsize,
|
||||
AutoLocalize = false,
|
||||
|
||||
LayoutOrder = props.layoutorder,
|
||||
|
||||
ZIndex = props.zindex,
|
||||
|
||||
BackgroundColor3 = anim(function()
|
||||
return if read(props.checked) then theme.acc[3]() else theme.bg[1]()
|
||||
end),
|
||||
|
||||
create "UIStroke" {
|
||||
Color = function()
|
||||
return if read(props.checked) then theme.acc[0]() else theme.bg[-3]()
|
||||
end
|
||||
},
|
||||
|
||||
create "UICorner" {
|
||||
CornerRadius = UDim.new(0, 4)
|
||||
},
|
||||
|
||||
create "ImageLabel" {
|
||||
Size = UDim2.fromScale(1, 1),
|
||||
BackgroundTransparency = 1,
|
||||
|
||||
Image = "rbxassetid://100188624502987",
|
||||
ImageTransparency = anim(function()
|
||||
return if read(props.checked) then 0 else 1
|
||||
end)
|
||||
},
|
||||
|
||||
unpack(props)
|
||||
|
||||
}
|
||||
end
|
||||
23
modules/Jabby/ui/components/display/divider.luau
Normal file
23
modules/Jabby/ui/components/display/divider.luau
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
local vide = require(script.Parent.Parent.Parent.Parent.vide)
|
||||
local theme = require(script.Parent.Parent.Parent.util.theme)
|
||||
|
||||
local create = vide.create
|
||||
local read = vide.read
|
||||
|
||||
type can<T> = T | () -> T
|
||||
type props = {
|
||||
thickness: can<number>?,
|
||||
position: can<UDim2>?,
|
||||
}
|
||||
|
||||
return function(props: props)
|
||||
|
||||
return create "Frame" {
|
||||
BackgroundColor3 = theme.bg[-2],
|
||||
Position = props.position,
|
||||
AutoLocalize = false,
|
||||
Size = function()
|
||||
return UDim2.new(1, 0, 0, read(props.thickness) or 1)
|
||||
end,
|
||||
}
|
||||
end
|
||||
118
modules/Jabby/ui/components/display/pages.luau
Normal file
118
modules/Jabby/ui/components/display/pages.luau
Normal file
|
|
@ -0,0 +1,118 @@
|
|||
local vide = require(script.Parent.Parent.Parent.Parent.vide)
|
||||
local anim = require(script.Parent.Parent.Parent.util.anim)
|
||||
local theme = require(script.Parent.Parent.Parent.util.theme)
|
||||
local button = require(script.Parent.Parent.interactable.button)
|
||||
local container = require(script.Parent.Parent.util.container)
|
||||
local list = require(script.Parent.Parent.util.list)
|
||||
local padding = require(script.Parent.Parent.util.padding)
|
||||
local rounded_frame = require(script.Parent.Parent.util.rounded_frame)
|
||||
local divider = require(script.Parent.divider)
|
||||
local typography = require(script.Parent.typography)
|
||||
|
||||
local create = vide.create
|
||||
local source = vide.source
|
||||
local changed = vide.changed
|
||||
local indexes = vide.indexes
|
||||
local untrack = vide.untrack
|
||||
local cleanup = vide.cleanup
|
||||
|
||||
type can<T> = T | () -> T
|
||||
type props = {
|
||||
labels: () -> {
|
||||
{
|
||||
title: string,
|
||||
ui: () -> Instance | {Instance}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return function(props: props)
|
||||
|
||||
local selected = source(1)
|
||||
|
||||
return list {
|
||||
justifycontent = Enum.UIFlexAlignment.Fill,
|
||||
spacing = UDim.new(),
|
||||
|
||||
create "Frame" {
|
||||
Size = UDim2.new(1, 0, 0, 32),
|
||||
AutoLocalize = false,
|
||||
BackgroundColor3 = theme.bg[3],
|
||||
|
||||
divider {
|
||||
position = UDim2.fromScale(0, 1),
|
||||
},
|
||||
|
||||
container {
|
||||
create "UIListLayout" {
|
||||
FillDirection = Enum.FillDirection.Horizontal
|
||||
},
|
||||
|
||||
indexes(props.labels, function(value, key)
|
||||
local guistate = source(Enum.GuiState.Idle)
|
||||
|
||||
return rounded_frame {
|
||||
name = key,
|
||||
size = UDim2.fromOffset(50, 30),
|
||||
automaticsize = Enum.AutomaticSize.X,
|
||||
topleft = UDim.new(0, 4),
|
||||
topright = UDim.new(0, 4),
|
||||
|
||||
color = function()
|
||||
return if selected() == key then theme.bg[0]()
|
||||
elseif guistate() == Enum.GuiState.Idle then theme.bg[3]()
|
||||
else theme.bg[1]()
|
||||
end,
|
||||
|
||||
create "TextButton" {
|
||||
Size = UDim2.fromScale(1, 1),
|
||||
AutoLocalize = false,
|
||||
BackgroundTransparency = 1,
|
||||
|
||||
Activated = function()
|
||||
selected(key)
|
||||
end,
|
||||
|
||||
typography {
|
||||
|
||||
position = UDim2.fromScale(0.5, 0.5),
|
||||
anchorpoint = Vector2.new(0.5, 0.5),
|
||||
|
||||
text = function()
|
||||
return value().title
|
||||
end,
|
||||
|
||||
textsize = 16
|
||||
},
|
||||
|
||||
|
||||
padding {
|
||||
x = UDim.new(0, 24),
|
||||
y = UDim.new(0, 2)
|
||||
},
|
||||
|
||||
changed("GuiState", guistate)
|
||||
}
|
||||
}
|
||||
end),
|
||||
},
|
||||
|
||||
ZIndex = 100,
|
||||
|
||||
},
|
||||
|
||||
create "Frame" {
|
||||
Size = UDim2.new(1, 0, 1, 0),
|
||||
AutoLocalize = false,
|
||||
|
||||
BackgroundColor3 = theme.bg[0],
|
||||
|
||||
function()
|
||||
return untrack(props.labels()[selected()].ui)
|
||||
end
|
||||
|
||||
},
|
||||
|
||||
}
|
||||
|
||||
end
|
||||
70
modules/Jabby/ui/components/display/pane.luau
Normal file
70
modules/Jabby/ui/components/display/pane.luau
Normal file
|
|
@ -0,0 +1,70 @@
|
|||
local vide = require(script.Parent.Parent.Parent.Parent.vide)
|
||||
local theme = require(script.Parent.Parent.Parent.util.theme)
|
||||
local list = require(script.Parent.Parent.util.list)
|
||||
local padding = require(script.Parent.Parent.util.padding)
|
||||
local background = require(script.Parent.background)
|
||||
local typography = require(script.Parent.typography)
|
||||
|
||||
local create = vide.create
|
||||
local read = vide.read
|
||||
local show = vide.show
|
||||
|
||||
type can<T> = T | () -> T
|
||||
type props = {
|
||||
|
||||
size: can<UDim2>?,
|
||||
position: can<UDim2>?,
|
||||
anchorpoint: can<UDim2>?,
|
||||
layoutorder: can<number>?,
|
||||
automaticsize: can<Enum.AutomaticSize>?,
|
||||
|
||||
name: can<string>?,
|
||||
|
||||
[number]: any
|
||||
|
||||
}
|
||||
|
||||
return function(props: props)
|
||||
|
||||
return create "Frame" {
|
||||
Name = props.name,
|
||||
Size = props.size or UDim2.fromScale(1, 0),
|
||||
Position = props.position,
|
||||
AnchorPoint = props.anchorpoint,
|
||||
LayoutOrder = props.layoutorder,
|
||||
AutomaticSize = props.automaticsize or Enum.AutomaticSize.Y,
|
||||
BackgroundColor3 = theme.bg[0],
|
||||
AutoLocalize = false,
|
||||
|
||||
create "UICorner" {
|
||||
CornerRadius = UDim.new(0, 8)
|
||||
},
|
||||
create "UIStroke" {
|
||||
Color = theme.bg[-3]
|
||||
},
|
||||
show(function()
|
||||
return if read(props.name) then #read(props.name) > 0 else false
|
||||
end, function()
|
||||
return background {
|
||||
size = UDim2.new(),
|
||||
position = UDim2.fromOffset(4, -16),
|
||||
automaticsize = Enum.AutomaticSize.XY,
|
||||
|
||||
typography {
|
||||
text = props.name,
|
||||
disabled = true,
|
||||
|
||||
textsize = 14
|
||||
},
|
||||
|
||||
padding {x = UDim.new(0, 2), y = UDim.new()}
|
||||
}
|
||||
end),
|
||||
|
||||
padding {},
|
||||
list {
|
||||
unpack(props)
|
||||
}
|
||||
|
||||
}
|
||||
end
|
||||
207
modules/Jabby/ui/components/display/resizeable_bar.luau
Normal file
207
modules/Jabby/ui/components/display/resizeable_bar.luau
Normal file
|
|
@ -0,0 +1,207 @@
|
|||
local RunService = game:GetService("RunService")
|
||||
local UserInputService = game:GetService("UserInputService")
|
||||
local vide = require(script.Parent.Parent.Parent.Parent.vide)
|
||||
local theme = require(script.Parent.Parent.Parent.util.theme)
|
||||
local padding = require(script.Parent.Parent.util.padding)
|
||||
local rounded_frame = require(script.Parent.Parent.util.rounded_frame)
|
||||
local typography = require(script.Parent.typography)
|
||||
|
||||
local create = vide.create
|
||||
local source = vide.source
|
||||
local derive = vide.derive
|
||||
local effect = vide.effect
|
||||
local cleanup = vide.cleanup
|
||||
local indexes = vide.indexes
|
||||
local changed = vide.changed
|
||||
local untrack = vide.untrack
|
||||
|
||||
local MAX_PIXELS_OFFSET = 32
|
||||
|
||||
local BEFORE = source(0)
|
||||
local AFTER = source(1)
|
||||
|
||||
type ResizeableBar = {
|
||||
meaning: () -> { string },
|
||||
min_sizes: (() -> { vide.source<number>? })?,
|
||||
sizes: vide.source<{ vide.source<number> }>,
|
||||
suggested_sizes: { number }?,
|
||||
|
||||
splits: (vide.source<{ vide.source<number> }>)?,
|
||||
base_splits: { number }?
|
||||
|
||||
}
|
||||
|
||||
return function(props: ResizeableBar)
|
||||
local meaning = props.meaning
|
||||
local sizes = props.sizes
|
||||
local min_sizes = props.min_sizes or source({}) :: never
|
||||
local suggested_sizes = props.suggested_sizes or {}
|
||||
|
||||
local absolute_size = source(Vector2.one)
|
||||
local absolute_position = source(Vector2.one)
|
||||
|
||||
local total = derive(function()
|
||||
return #meaning()
|
||||
end)
|
||||
|
||||
local splits = props.splits or source {}
|
||||
local total_columns = derive(function()
|
||||
return #props.meaning()
|
||||
end)
|
||||
|
||||
effect(function(previous)
|
||||
local new = {}
|
||||
|
||||
for i = 1, total_columns() - 1 do
|
||||
local old_split = vide.read(previous and previous[i] or nil)
|
||||
new[i] = source(math.min(if old_split and old_split ~= 1 then old_split else suggested_sizes[i] or 1, i / total_columns()))
|
||||
end
|
||||
|
||||
splits(new)
|
||||
return new
|
||||
end)
|
||||
|
||||
for i, split in (props.base_splits :: never) or {} do
|
||||
splits()[i](split)
|
||||
end
|
||||
|
||||
local function get_size(index: number)
|
||||
local split_before = splits()[index - 1] or BEFORE :: never
|
||||
local split_after = splits()[index] or AFTER :: never
|
||||
|
||||
local size = split_after() - split_before()
|
||||
return size
|
||||
end
|
||||
|
||||
local function get_min_size(i: number)
|
||||
local min_size = min_sizes()[i]
|
||||
return min_size and min_size() or 0.025
|
||||
end
|
||||
|
||||
effect(function()
|
||||
local new = setmetatable({}, {
|
||||
__index = function()
|
||||
return function() return 0 end
|
||||
end,
|
||||
})
|
||||
|
||||
for i = 1, total() do
|
||||
min_sizes()[i] = min_sizes()[i] or source(0.025)
|
||||
untrack(function()
|
||||
new[i] = derive(function()
|
||||
return get_size(i)
|
||||
end)
|
||||
end)
|
||||
end
|
||||
|
||||
sizes(new :: any)
|
||||
end)
|
||||
|
||||
local down = false
|
||||
local updating = 0
|
||||
|
||||
return rounded_frame {
|
||||
size = function()
|
||||
return UDim2.new(1, 0, 0, 32)
|
||||
end,
|
||||
topleft = UDim.new(0, 8),
|
||||
topright = UDim.new(0, 8),
|
||||
color = theme.bg[1],
|
||||
|
||||
create "TextButton" {
|
||||
Size = UDim2.fromScale(1, 1),
|
||||
BackgroundTransparency = 1,
|
||||
AutoLocalize = false,
|
||||
Text = "",
|
||||
|
||||
create "UIListLayout" {
|
||||
FillDirection = Enum.FillDirection.Horizontal,
|
||||
Padding = UDim.new(0, 0)
|
||||
},
|
||||
|
||||
indexes(meaning, function(column, i)
|
||||
return typography {
|
||||
size = function()
|
||||
return UDim2.fromScale(get_size(i), 1)
|
||||
end,
|
||||
automaticsize = Enum.AutomaticSize.None,
|
||||
text = function()
|
||||
return column() or ""
|
||||
end,
|
||||
xalignment = Enum.TextXAlignment.Left,
|
||||
truncate = Enum.TextTruncate.AtEnd,
|
||||
header = true,
|
||||
textsize = 18,
|
||||
|
||||
padding {x = UDim.new(0, 8)}
|
||||
}
|
||||
end),
|
||||
|
||||
changed("AbsoluteSize", absolute_size),
|
||||
changed("AbsolutePosition", absolute_position),
|
||||
|
||||
MouseButton1Down = function(x: number)
|
||||
-- find the nearest split
|
||||
x -= absolute_position().X
|
||||
local absolute_size = absolute_size()
|
||||
local nearest = -1
|
||||
for i, location in splits() do
|
||||
local absolute_x = absolute_size.X * location()
|
||||
if math.abs(x - absolute_x) > MAX_PIXELS_OFFSET then continue end
|
||||
|
||||
nearest = i
|
||||
end
|
||||
|
||||
down = nearest ~= -1
|
||||
updating = nearest
|
||||
end,
|
||||
|
||||
MouseButton1Up = function()
|
||||
down = false
|
||||
end,
|
||||
|
||||
cleanup(RunService.Heartbeat:Connect(function()
|
||||
local x = UserInputService:GetMouseLocation().X
|
||||
|
||||
x -= absolute_position().X
|
||||
if down == false then return end
|
||||
down = UserInputService:IsMouseButtonPressed(Enum.UserInputType.MouseButton1) == true
|
||||
|
||||
local relative = x / absolute_size().X
|
||||
local current = splits()[updating]()
|
||||
local left_to_move = relative - current
|
||||
|
||||
if left_to_move > 0 then
|
||||
for i = updating, total() - 1, 1 do
|
||||
local min_size = get_min_size(i + 1)
|
||||
local size = get_size(i + 1)
|
||||
|
||||
local new_size = math.max(size - left_to_move, min_size)
|
||||
local difference = size - new_size
|
||||
|
||||
splits()[i](splits()[i]() + difference)
|
||||
left_to_move -= difference
|
||||
|
||||
if left_to_move == 0 then break end
|
||||
end
|
||||
else
|
||||
for i = updating, 1, -1 do
|
||||
local min_size = get_min_size(i)
|
||||
|
||||
local size = math.max(get_size(i), min_size) -- this is changing, which it isnt supposed to do
|
||||
|
||||
local new_size = math.max(size + left_to_move, min_size)
|
||||
local difference = new_size - size
|
||||
|
||||
splits()[i](splits()[i]() + difference)
|
||||
--assert((new_size + difference) == get_size(i - 1))
|
||||
left_to_move -= difference
|
||||
|
||||
if left_to_move == 0 then break end
|
||||
end
|
||||
end
|
||||
end)),
|
||||
}
|
||||
|
||||
}
|
||||
end
|
||||
22
modules/Jabby/ui/components/display/scroll_frame.luau
Normal file
22
modules/Jabby/ui/components/display/scroll_frame.luau
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
local vide = require(script.Parent.Parent.Parent.Parent.vide)
|
||||
local theme = require(script.Parent.Parent.Parent.util.theme)
|
||||
|
||||
local create = vide.create
|
||||
|
||||
type props = vide.vScrollingFrame
|
||||
|
||||
return function(props: props)
|
||||
|
||||
return create "ScrollingFrame" {
|
||||
AutoLocalize = false,
|
||||
ScrollBarThickness = 6,
|
||||
ScrollBarImageColor3 = theme.fg_on_bg_low[0],
|
||||
CanvasSize = UDim2.new(),
|
||||
|
||||
BackgroundTransparency = 1,
|
||||
|
||||
props
|
||||
|
||||
}
|
||||
|
||||
end
|
||||
123
modules/Jabby/ui/components/display/snapping.luau
Normal file
123
modules/Jabby/ui/components/display/snapping.luau
Normal file
|
|
@ -0,0 +1,123 @@
|
|||
local UserInputService = game:GetService("UserInputService")
|
||||
|
||||
local vide = require(script.Parent.Parent.Parent.Parent.vide)
|
||||
|
||||
local create = vide.create
|
||||
local source = vide.source
|
||||
local cleanup = vide.cleanup
|
||||
local effect = vide.effect
|
||||
local action = vide.action
|
||||
local changed = vide.changed
|
||||
local untrack = vide.untrack
|
||||
local read = vide.read
|
||||
|
||||
type can<T> = T | () -> T
|
||||
|
||||
type snap_area = {
|
||||
zindex: can<number>?,
|
||||
snapped: (boolean) -> ()
|
||||
}
|
||||
|
||||
type snappable = {
|
||||
--- tells the object that you are dragging it
|
||||
dragging: () -> boolean,
|
||||
--- allows making the widget float by itself without being anchored to anything
|
||||
allow_floating: can<boolean>,
|
||||
|
||||
--- callbacks that update the position and size
|
||||
snapped: (boolean) -> (),
|
||||
position: (UDim2) -> (),
|
||||
size: (UDim2) -> ()
|
||||
}
|
||||
|
||||
local function in_bounds(mpos: Vector2, pos: Vector2, size: Vector2)
|
||||
return mpos.X >= pos.X and mpos.X <= pos.X + size.X and mpos.Y >= pos.Y and mpos.Y <= pos.Y + size.Y
|
||||
end
|
||||
|
||||
return function()
|
||||
|
||||
local snap_areas = {}
|
||||
local mouse_position = source(Vector2.zero)
|
||||
|
||||
local function snap_area(props: snap_area)
|
||||
local position = source(Vector2.zero)
|
||||
local size = source(Vector2.zero)
|
||||
local docked = source(false)
|
||||
|
||||
return create "Frame" {
|
||||
Name = "SnapArea",
|
||||
AutoLocalize = false,
|
||||
Size = UDim2.fromScale(1, 1),
|
||||
|
||||
BackgroundTransparency = 1,
|
||||
|
||||
changed("AbsoluteSize", size),
|
||||
changed("AbsolutePosition", position),
|
||||
|
||||
action(function(ref)
|
||||
snap_areas[ref] = {
|
||||
position = position,
|
||||
docked = docked,
|
||||
size = size,
|
||||
zindex = props.zindex or 0
|
||||
}
|
||||
|
||||
cleanup(function()
|
||||
snap_areas[ref] = nil
|
||||
end)
|
||||
end)
|
||||
}
|
||||
end
|
||||
|
||||
local function snappable(props: snappable)
|
||||
local snapped_to = source()
|
||||
|
||||
effect(function()
|
||||
if props.dragging() == false then return end
|
||||
local mpos = mouse_position()
|
||||
|
||||
untrack(function()
|
||||
if snapped_to() then snapped_to().docked(false) end
|
||||
|
||||
local snap_to
|
||||
|
||||
for _, data in snap_areas do
|
||||
if not in_bounds(mpos, data.position(), data.size()) then continue end
|
||||
if snap_to and read(data.zindex) <= read(snap_to.zindex) then continue end
|
||||
snap_to = data
|
||||
end
|
||||
|
||||
if not snap_to and read(props.allow_floating) == false then return end
|
||||
if snap_to and snap_to.docked() then return end
|
||||
if snap_to then snap_to.docked(true) end
|
||||
|
||||
snapped_to(snap_to)
|
||||
end)
|
||||
end)
|
||||
|
||||
effect(function()
|
||||
props.snapped(if snapped_to() then true else false)
|
||||
end)
|
||||
|
||||
effect(function()
|
||||
if not snapped_to() then return end
|
||||
local data = snapped_to()
|
||||
local pos = data.position()
|
||||
local size = data.size()
|
||||
|
||||
props.position(UDim2.fromOffset(pos.X, pos.Y))
|
||||
props.size(UDim2.fromOffset(size.X, size.Y))
|
||||
end)
|
||||
end
|
||||
|
||||
cleanup(UserInputService.InputChanged:Connect(function(input)
|
||||
if input.UserInputType ~= Enum.UserInputType.MouseMovement then return end
|
||||
mouse_position(Vector2.new(input.Position.X, input.Position.Y))
|
||||
end))
|
||||
|
||||
return {
|
||||
snap_area = snap_area,
|
||||
snappable = snappable
|
||||
}
|
||||
|
||||
end
|
||||
179
modules/Jabby/ui/components/display/tablesheet.luau
Normal file
179
modules/Jabby/ui/components/display/tablesheet.luau
Normal file
|
|
@ -0,0 +1,179 @@
|
|||
local vide = require(script.Parent.Parent.Parent.Parent.vide)
|
||||
local theme = require(script.Parent.Parent.Parent.util.theme)
|
||||
local button = require(script.Parent.Parent.interactable.button)
|
||||
local rounded_frame = require(script.Parent.Parent.util.rounded_frame)
|
||||
local virtualscroller = require(script.Parent.Parent.util.virtualscroller)
|
||||
local resizeable_bar = require(script.Parent.resizeable_bar)
|
||||
local scroll_frame = require(script.Parent.scroll_frame)
|
||||
|
||||
local create = vide.create
|
||||
local source = vide.source
|
||||
local derive = vide.derive
|
||||
local indexes = vide.indexes
|
||||
|
||||
type can<T> = T | () -> T
|
||||
type table = {
|
||||
size: can<UDim2>?,
|
||||
suggested_column_sizes: { number }?,
|
||||
|
||||
base_splits: { number }?,
|
||||
columns: () -> {{any}},
|
||||
|
||||
on_click: (column: number, row: number) -> (),
|
||||
on_click2: (column: number, row: number) -> (),
|
||||
read_value: (column: number, row: number) -> string,
|
||||
|
||||
below: {[number]: any}?,
|
||||
|
||||
[number]: any
|
||||
}
|
||||
|
||||
return function(props: table)
|
||||
local sizes = source({})
|
||||
local splits = source({})
|
||||
|
||||
local meaning = derive(function()
|
||||
local t = {}
|
||||
|
||||
for i, column in props.columns() do
|
||||
t[i] = column[1]
|
||||
end
|
||||
|
||||
return t
|
||||
end)
|
||||
|
||||
local function get_size(index: number)
|
||||
local split_before = splits()[index - 1] or source(0) :: never
|
||||
local split_after = splits()[index] or source(1) :: never
|
||||
|
||||
local size = split_after() - split_before()
|
||||
return size
|
||||
end
|
||||
|
||||
return scroll_frame {
|
||||
Size = props.size or UDim2.new(1, 0, 0, 8 * 32),
|
||||
CanvasSize = function()
|
||||
return UDim2.new(1, 0)
|
||||
end,
|
||||
|
||||
create "UIListLayout" {
|
||||
VerticalFlex = Enum.UIFlexAlignment.SpaceEvenly
|
||||
},
|
||||
|
||||
resizeable_bar {
|
||||
meaning = meaning,
|
||||
sizes = sizes,
|
||||
splits = splits,
|
||||
base_splits = props.base_splits,
|
||||
suggested_sizes = props.suggested_column_sizes
|
||||
},
|
||||
|
||||
create "Folder" {
|
||||
|
||||
indexes(meaning, function(_, i)
|
||||
return create "Frame" {
|
||||
Size = UDim2.new(0, 1, 1, -32),
|
||||
AutoLocalize = false,
|
||||
Position = function()
|
||||
local pos = splits()[i]
|
||||
return if not pos then UDim2.fromScale(0, 0) else UDim2.fromScale(pos(), 0)
|
||||
end,
|
||||
|
||||
BackgroundColor3 = theme.bg[-1],
|
||||
|
||||
ZIndex = 100
|
||||
}
|
||||
end)
|
||||
},
|
||||
|
||||
virtualscroller {
|
||||
size = UDim2.fromScale(1, 0),
|
||||
|
||||
create "UIFlexItem" {
|
||||
FlexMode = Enum.UIFlexMode.Grow,
|
||||
},
|
||||
|
||||
{
|
||||
BackgroundColor3 = theme.bg[0],
|
||||
VerticalScrollBarInset = Enum.ScrollBarInset.None,
|
||||
BackgroundTransparency = 0,
|
||||
},
|
||||
|
||||
item_size = 32,
|
||||
item = function(index)
|
||||
|
||||
return create "Frame" {
|
||||
Size = UDim2.new(1, 0, 0, 32),
|
||||
AutoLocalize = false,
|
||||
|
||||
BackgroundColor3 = theme.bg[2],
|
||||
|
||||
create "UIListLayout" {
|
||||
FillDirection = Enum.FillDirection.Horizontal,
|
||||
Padding = UDim.new(0, 0)
|
||||
},
|
||||
|
||||
create "UIStroke" {
|
||||
Color = theme.bg[-1],
|
||||
},
|
||||
|
||||
indexes(props.columns, function(column, i)
|
||||
return button {
|
||||
size = function()
|
||||
local column_size = get_size(i)
|
||||
return UDim2.new(column_size, 0, 0, 32)
|
||||
end,
|
||||
|
||||
text = function()
|
||||
return props.read_value(i, index() + 1) or ""
|
||||
end,
|
||||
|
||||
create "UIListLayout" {
|
||||
FillDirection = Enum.FillDirection.Horizontal,
|
||||
VerticalAlignment = Enum.VerticalAlignment.Center
|
||||
},
|
||||
|
||||
xalignment = Enum.TextXAlignment.Left,
|
||||
|
||||
corner = false,
|
||||
stroke = false,
|
||||
code = true,
|
||||
|
||||
activated = function()
|
||||
props.on_click(i, index() + 2)
|
||||
end,
|
||||
|
||||
mouse2 = function()
|
||||
props.on_click2(i, index() + 2)
|
||||
end
|
||||
} :: Instance
|
||||
end)
|
||||
|
||||
}
|
||||
|
||||
end,
|
||||
|
||||
max_items = function()
|
||||
local value = (props.columns()[1] ~= nil and #props.columns()[1] or 0)
|
||||
return value - 1
|
||||
end,
|
||||
|
||||
|
||||
},
|
||||
|
||||
rounded_frame {
|
||||
size = function()
|
||||
return UDim2.new(1, 0, 0, 32)
|
||||
end,
|
||||
|
||||
color = theme.bg[1],
|
||||
|
||||
props.below,
|
||||
|
||||
bottomleft = UDim.new(0, 8),
|
||||
bottomright = UDim.new(0, 8),
|
||||
},
|
||||
|
||||
unpack(props)
|
||||
}
|
||||
end
|
||||
86
modules/Jabby/ui/components/display/typography.luau
Normal file
86
modules/Jabby/ui/components/display/typography.luau
Normal file
|
|
@ -0,0 +1,86 @@
|
|||
local vide = require(script.Parent.Parent.Parent.Parent.vide)
|
||||
local anim = require(script.Parent.Parent.Parent.util.anim)
|
||||
local theme = require(script.Parent.Parent.Parent.util.theme)
|
||||
|
||||
local create = vide.create
|
||||
local read = vide.read
|
||||
|
||||
type can<T> = T | () -> T
|
||||
type props = {
|
||||
|
||||
size: can<UDim2>?,
|
||||
position: can<UDim2>?,
|
||||
anchorpoint: can<Vector2>?,
|
||||
automaticsize: can<Enum.AutomaticSize>?,
|
||||
|
||||
accent: can<boolean>?,
|
||||
|
||||
xalignment: can<Enum.TextXAlignment>?,
|
||||
yalignment: can<Enum.TextYAlignment>?,
|
||||
truncate: can<Enum.TextTruncate>?,
|
||||
wrapped: can<boolean>?,
|
||||
|
||||
header: can<boolean>?,
|
||||
code: can<boolean>?,
|
||||
disabled: can<boolean>?,
|
||||
|
||||
text: can<string>,
|
||||
textsize: can<number>?,
|
||||
|
||||
visible: can<boolean>?,
|
||||
|
||||
[number]: any
|
||||
|
||||
}
|
||||
|
||||
return function(props: props)
|
||||
|
||||
local function font()
|
||||
return if read(props.code) then theme.code else theme.font
|
||||
end
|
||||
|
||||
local function fg()
|
||||
local accent = read(props.accent)
|
||||
local disabled = read(props.disabled)
|
||||
|
||||
return if accent then
|
||||
if disabled then theme.fg_on_acc_low[0]()
|
||||
else theme.fg_on_acc_high[0]()
|
||||
else
|
||||
if disabled then theme.fg_on_bg_low[0]()
|
||||
else theme.fg_on_bg_high[0]()
|
||||
end
|
||||
|
||||
return create "TextLabel" {
|
||||
|
||||
Size = props.size,
|
||||
Position = props.position,
|
||||
AnchorPoint = props.anchorpoint,
|
||||
AutomaticSize = props.automaticsize or Enum.AutomaticSize.XY,
|
||||
AutoLocalize = false,
|
||||
|
||||
TextXAlignment = props.xalignment,
|
||||
TextYAlignment = props.yalignment,
|
||||
TextTruncate = props.truncate,
|
||||
|
||||
BackgroundTransparency = 1,
|
||||
|
||||
Text = props.text,
|
||||
|
||||
TextSize = props.textsize or function()
|
||||
return if read(props.header) then theme.header else theme.body
|
||||
end,
|
||||
TextWrapped = props.wrapped,
|
||||
FontFace = function()
|
||||
return if read(props.header) then
|
||||
Font.new(font().Family, Enum.FontWeight.Bold)
|
||||
else font()
|
||||
end,
|
||||
TextColor3 = anim(fg),
|
||||
|
||||
Visible = props.visible,
|
||||
|
||||
unpack(props)
|
||||
}
|
||||
|
||||
end
|
||||
229
modules/Jabby/ui/components/display/widget/borders.luau
Normal file
229
modules/Jabby/ui/components/display/widget/borders.luau
Normal file
|
|
@ -0,0 +1,229 @@
|
|||
local GuiService = game:GetService("GuiService")
|
||||
local RunService = game:GetService("RunService")
|
||||
local UserInputService = game:GetService("UserInputService")
|
||||
|
||||
local vide = require(script.Parent.Parent.Parent.Parent.Parent.vide)
|
||||
local theme = require(script.Parent.Parent.Parent.Parent.util.theme)
|
||||
local container = require(script.Parent.Parent.Parent.util.container)
|
||||
|
||||
local create = vide.create
|
||||
local source = vide.source
|
||||
local spring = vide.spring
|
||||
local changed = vide.changed
|
||||
local cleanup = vide.cleanup
|
||||
|
||||
type Source<T> = vide.Source<T>
|
||||
type props = {
|
||||
|
||||
resize_range: number,
|
||||
min_size: Vector2,
|
||||
|
||||
can_resize_left: Source<boolean>,
|
||||
can_resize_right: Source<boolean>,
|
||||
can_resize_bottom: Source<boolean>,
|
||||
can_resize_top: Source<boolean>,
|
||||
|
||||
resizing: () -> boolean,
|
||||
}
|
||||
|
||||
local function xpos(s: () -> number)
|
||||
return function()
|
||||
return Vector2.new(s(), 0)
|
||||
end
|
||||
end
|
||||
|
||||
local function ypos(s: () -> number)
|
||||
return function()
|
||||
return Vector2.new(0, s())
|
||||
end
|
||||
end
|
||||
|
||||
return function(props: props)
|
||||
local RESIZE_RANGE = props.resize_range
|
||||
local MIN_SIZE = props.min_size
|
||||
|
||||
local can_resize_left = props.can_resize_left
|
||||
local can_resize_right = props.can_resize_right
|
||||
local can_resize_bottom = props.can_resize_bottom
|
||||
local can_resize_top = props.can_resize_top
|
||||
local resizing = props.resizing
|
||||
|
||||
local absolute_size = source(Vector2.new(1, 1))
|
||||
local absolute_position = source(Vector2.zero)
|
||||
|
||||
local thickness = 4
|
||||
local border_selected = theme.acc[8]
|
||||
local gradient = NumberSequence.new({
|
||||
NumberSequenceKeypoint.new(0, 1),
|
||||
NumberSequenceKeypoint.new(0.25, 1),
|
||||
NumberSequenceKeypoint.new(0.5, 0),
|
||||
NumberSequenceKeypoint.new(0.75, 1),
|
||||
NumberSequenceKeypoint.new(1, 1),
|
||||
})
|
||||
|
||||
local x = source(0)
|
||||
local y = source(0)
|
||||
|
||||
cleanup(RunService.Heartbeat:Connect(function()
|
||||
local mposition = UserInputService:GetMouseLocation()
|
||||
local top_inset, bottom_inset = GuiService:GetGuiInset()
|
||||
mposition += - top_inset - bottom_inset
|
||||
|
||||
if MIN_SIZE.X ~= absolute_size().X or not resizing() then x(mposition.X) end
|
||||
if MIN_SIZE.Y ~= absolute_size().Y or not resizing() then y(mposition.Y) end
|
||||
end))
|
||||
|
||||
cleanup(RunService.RenderStepped:Connect(function()
|
||||
local mposition = UserInputService:GetMouseLocation()
|
||||
local top_inset, bottom_inset = GuiService:GetGuiInset()
|
||||
mposition += - top_inset - bottom_inset
|
||||
local x, y = mposition.X, mposition.Y
|
||||
|
||||
if resizing() then return end
|
||||
|
||||
local left = absolute_position().X
|
||||
local top = absolute_position().Y
|
||||
local right = left + absolute_size().X
|
||||
local bottom = top + absolute_size().Y
|
||||
|
||||
local topleft = absolute_position() - Vector2.new(RESIZE_RANGE, RESIZE_RANGE)
|
||||
local bottomright = absolute_position() + absolute_size() + Vector2.new(RESIZE_RANGE, RESIZE_RANGE)
|
||||
|
||||
-- perform AABB to check if the cursor is in range
|
||||
local within_bounds = x > topleft.X and y > topleft.Y and x < bottomright.X and y < bottomright.Y
|
||||
|
||||
can_resize_top(y > top - RESIZE_RANGE and y < top and within_bounds)
|
||||
can_resize_left(x < left + RESIZE_RANGE and x > left - RESIZE_RANGE and within_bounds)
|
||||
can_resize_bottom(y < bottom + RESIZE_RANGE and y > bottom - RESIZE_RANGE and within_bounds)
|
||||
can_resize_right(x < right + RESIZE_RANGE and x > right - RESIZE_RANGE and within_bounds)
|
||||
end))
|
||||
|
||||
return {
|
||||
|
||||
changed("AbsoluteSize", function(value: Vector2)
|
||||
if value.Magnitude == 0 then return end
|
||||
absolute_size(value)
|
||||
end),
|
||||
changed("AbsolutePosition", absolute_position),
|
||||
|
||||
container {
|
||||
|
||||
Name = "Left",
|
||||
|
||||
Position = UDim2.fromScale(0, 0.5),
|
||||
Size = UDim2.new(0, thickness, 1, thickness * 2),
|
||||
AnchorPoint = Vector2.new(1, 0.5),
|
||||
|
||||
BackgroundColor3 = border_selected,
|
||||
|
||||
BackgroundTransparency = spring(function()
|
||||
return can_resize_left() and 0 or 1
|
||||
end, 0.2),
|
||||
|
||||
ZIndex = 1000,
|
||||
|
||||
create "UIGradient" {
|
||||
|
||||
Rotation = 90,
|
||||
|
||||
Transparency = gradient,
|
||||
Offset = ypos(spring(function()
|
||||
return (y() - absolute_position().Y - absolute_size().Y / 2) / absolute_size().Y
|
||||
end, 0.1)),
|
||||
},
|
||||
|
||||
create "UICorner" {
|
||||
CornerRadius = UDim.new(0, 4)
|
||||
},
|
||||
},
|
||||
|
||||
container {
|
||||
|
||||
Name = "Right",
|
||||
|
||||
Position = UDim2.fromScale(1, 0.5),
|
||||
Size = UDim2.new(0, thickness, 1, thickness * 2),
|
||||
AnchorPoint = Vector2.new(0, 0.5),
|
||||
|
||||
BackgroundColor3 = border_selected,
|
||||
|
||||
BackgroundTransparency = spring(function()
|
||||
return can_resize_right() and 0 or 1
|
||||
end, 0.2),
|
||||
|
||||
ZIndex = 1000,
|
||||
|
||||
create "UIGradient" {
|
||||
|
||||
Rotation = 90,
|
||||
|
||||
Transparency = gradient,
|
||||
Offset = ypos(spring(function()
|
||||
return (y() - absolute_position().Y - absolute_size().Y / 2) / absolute_size().Y
|
||||
end, 0.1)),
|
||||
},
|
||||
|
||||
create "UICorner" {
|
||||
CornerRadius = UDim.new(0, 4)
|
||||
},
|
||||
},
|
||||
|
||||
container {
|
||||
|
||||
Name = "Bottom",
|
||||
|
||||
Position = UDim2.fromScale(0.5, 1),
|
||||
Size = UDim2.new(1, thickness * 2, 0, thickness),
|
||||
AnchorPoint = Vector2.new(0.5, 0),
|
||||
|
||||
BackgroundColor3 = border_selected,
|
||||
|
||||
BackgroundTransparency = spring(function()
|
||||
return can_resize_bottom() and 0 or 1
|
||||
end, 0.2),
|
||||
|
||||
ZIndex = 1000,
|
||||
|
||||
create "UIGradient" {
|
||||
|
||||
Transparency = gradient,
|
||||
Offset = xpos(spring(function()
|
||||
return (x() - absolute_position().X - absolute_size().X / 2) / absolute_size().X
|
||||
end, 0.1)),
|
||||
},
|
||||
|
||||
create "UICorner" {
|
||||
CornerRadius = UDim.new(0, 4)
|
||||
},
|
||||
},
|
||||
|
||||
container {
|
||||
|
||||
Name = "Top",
|
||||
|
||||
Position = UDim2.fromScale(0.5, 0),
|
||||
Size = UDim2.new(1, thickness * 2, 0, thickness),
|
||||
AnchorPoint = Vector2.new(0.5, 1),
|
||||
|
||||
BackgroundColor3 = border_selected,
|
||||
|
||||
BackgroundTransparency = spring(function()
|
||||
return can_resize_top() and 0 or 1
|
||||
end, 0.2),
|
||||
|
||||
ZIndex = 1000,
|
||||
|
||||
create "UIGradient" {
|
||||
|
||||
Transparency = gradient,
|
||||
Offset = xpos(spring(function()
|
||||
return (x() - absolute_position().X - absolute_size().X / 2) / absolute_size().X
|
||||
end, 0.1)),
|
||||
},
|
||||
|
||||
create "UICorner" {
|
||||
CornerRadius = UDim.new(0, 4)
|
||||
},
|
||||
},
|
||||
} :: { any }
|
||||
end
|
||||
299
modules/Jabby/ui/components/display/widget/init.luau
Normal file
299
modules/Jabby/ui/components/display/widget/init.luau
Normal file
|
|
@ -0,0 +1,299 @@
|
|||
local GuiService = game:GetService("GuiService")
|
||||
local Players = game:GetService("Players")
|
||||
local RunService = game:GetService("RunService")
|
||||
local UserInputService = game:GetService("UserInputService")
|
||||
|
||||
local vide = require(script.Parent.Parent.Parent.Parent.vide)
|
||||
local theme = require(script.Parent.Parent.Parent.util.theme)
|
||||
local container = require(script.Parent.Parent.util.container)
|
||||
local padding = require(script.Parent.Parent.util.padding)
|
||||
local shadow = require(script.Parent.Parent.util.shadow)
|
||||
local divider = require(script.Parent.divider)
|
||||
local snapping = require(script.Parent.snapping)
|
||||
local borders = require(script.borders)
|
||||
local topbar = require(script.topbar)
|
||||
|
||||
local create = vide.create
|
||||
local source = vide.source
|
||||
local cleanup = vide.cleanup
|
||||
local changed = vide.changed
|
||||
local reference = vide.action
|
||||
local spring = vide.spring
|
||||
|
||||
type can<T> = T | () -> T
|
||||
type props = {
|
||||
|
||||
title: can<string>,
|
||||
subtitle: can<string>?,
|
||||
min_size: Vector2?,
|
||||
position: Vector2?,
|
||||
size: Vector2?,
|
||||
|
||||
bind_to_close: (() -> ())?,
|
||||
|
||||
[number]: any
|
||||
|
||||
}
|
||||
|
||||
local HIGHEST_DISPLAY_ORDER = 100000
|
||||
local RESIZE_RANGE = 6
|
||||
|
||||
local docks
|
||||
|
||||
vide.mount(function()
|
||||
|
||||
docks = snapping()
|
||||
|
||||
return create "ScreenGui" {
|
||||
Name = "docks",
|
||||
AutoLocalize = false,
|
||||
|
||||
-- create "Frame" {
|
||||
-- Size = UDim2.new(1, 0, 0, 200),
|
||||
|
||||
-- BackgroundTransparency = 1,
|
||||
|
||||
-- docks.snap_area {}
|
||||
-- },
|
||||
|
||||
-- create "Frame" {
|
||||
-- Size = UDim2.new(1, 0, 0, 200),
|
||||
-- Position = UDim2.fromScale(0, 1),
|
||||
-- AnchorPoint = Vector2.new(0, 1),
|
||||
-- BackgroundTransparency = 1,
|
||||
|
||||
-- docks.snap_area {}
|
||||
-- },
|
||||
|
||||
create "Frame" {
|
||||
Size = UDim2.new(0, 16, 1, 0),
|
||||
BackgroundTransparency = 1,
|
||||
AutoLocalize = false,
|
||||
|
||||
docks.snap_area {}
|
||||
},
|
||||
}
|
||||
|
||||
end, Players.LocalPlayer.PlayerGui)
|
||||
|
||||
return function(props: props)
|
||||
local min_size = Vector2.new(100, 100):Max(props.min_size or Vector2.zero)
|
||||
local position = props.position or Vector2.new(32, 32)
|
||||
local base_size = props.size or min_size * 1.5
|
||||
|
||||
local x_size = source(math.max(min_size.X, base_size.X))
|
||||
local y_size = source(math.max(min_size.Y, base_size.Y))
|
||||
local x_position = source(position.X)
|
||||
local y_position = source(position.Y)
|
||||
|
||||
local offset = source(Vector2.zero)
|
||||
local dragging = source(false)
|
||||
local absolute_position = source(Vector2.zero)
|
||||
local absolute_size = source(Vector2.zero)
|
||||
|
||||
local can_resize_top = source(false)
|
||||
local can_resize_bottom = source(false)
|
||||
local can_resize_right = source(false)
|
||||
local can_resize_left = source(false)
|
||||
local resizing = source(false)
|
||||
local ref = source()
|
||||
local display_order = source(HIGHEST_DISPLAY_ORDER + 1)
|
||||
HIGHEST_DISPLAY_ORDER += 1
|
||||
|
||||
local mouse_inside = source(false)
|
||||
|
||||
local top: Vector2
|
||||
local bottom: Vector2
|
||||
|
||||
cleanup(UserInputService.InputEnded:Connect(function(input)
|
||||
if
|
||||
input.UserInputType ~= Enum.UserInputType.MouseButton1
|
||||
and input.UserInputType ~= Enum.UserInputType.Touch
|
||||
then
|
||||
return
|
||||
end
|
||||
resizing(false)
|
||||
dragging(false)
|
||||
end))
|
||||
|
||||
cleanup(UserInputService.InputChanged:Connect(function(input: InputObject)
|
||||
if input.UserInputType ~= Enum.UserInputType.MouseMovement then return end
|
||||
if not resizing() then return end
|
||||
|
||||
local mposition = UserInputService:GetMouseLocation()
|
||||
local top_inset, bottom_inset = GuiService:GetGuiInset()
|
||||
mposition += - top_inset - bottom_inset
|
||||
local x, y = mposition.X, mposition.Y
|
||||
|
||||
if can_resize_bottom() then y_size(math.max(y - top.Y, min_size.Y)) end
|
||||
if can_resize_right() then x_size(math.max(x - top.X, min_size.X)) end
|
||||
if can_resize_top() then
|
||||
y_size(math.max(bottom.Y - y, min_size.Y))
|
||||
y_position(math.min(y, bottom.Y - min_size.Y))
|
||||
end
|
||||
if can_resize_left() then
|
||||
x_size(math.max(bottom.X - x, min_size.X))
|
||||
x_position(math.min(x, bottom.X - min_size.X))
|
||||
end
|
||||
end))
|
||||
|
||||
cleanup(UserInputService.InputBegan:Connect(function(input)
|
||||
if input.UserInputType ~= Enum.UserInputType.MouseButton1 then return end
|
||||
if not dragging() then resizing(true) end
|
||||
|
||||
top = absolute_position()
|
||||
bottom = absolute_position() + absolute_size()
|
||||
|
||||
local player_gui
|
||||
if Players.LocalPlayer and RunService:IsRunning() then
|
||||
player_gui = Players.LocalPlayer:WaitForChild("PlayerGui") :: PlayerGui
|
||||
elseif RunService:IsStudio() and RunService:IsRunning() then
|
||||
player_gui = game:GetService("CoreGui") :: any
|
||||
else
|
||||
return
|
||||
end
|
||||
|
||||
local objects = player_gui:GetGuiObjectsAtPosition(input.Position.X, input.Position.Y)
|
||||
if #objects == 0 then return end
|
||||
if not objects[1]:IsDescendantOf(ref()) then return end
|
||||
|
||||
display_order(HIGHEST_DISPLAY_ORDER + 1)
|
||||
HIGHEST_DISPLAY_ORDER += 1
|
||||
end))
|
||||
|
||||
cleanup(UserInputService.InputChanged:Connect(function(input: InputObject)
|
||||
if dragging() == false then return end
|
||||
if not UserInputService:IsMouseButtonPressed(Enum.UserInputType.MouseButton1) then
|
||||
dragging(false)
|
||||
return
|
||||
end
|
||||
|
||||
local position = UserInputService:GetMouseLocation()
|
||||
-- local top_inset, bottom_inset = GuiService:GetGuiInset()
|
||||
-- position += - top_inset - bottom_inset
|
||||
x_position(position.X + offset().X)
|
||||
y_position(position.Y + offset().Y)
|
||||
end))
|
||||
|
||||
local snapped = source(false)
|
||||
local snap_size = source(UDim2.new())
|
||||
local snap_pos = source(UDim2.new())
|
||||
|
||||
local function radius()
|
||||
return if snapped() then UDim.new() else UDim.new(0, 6)
|
||||
end
|
||||
|
||||
return create "ScreenGui" {
|
||||
Name = props.title,
|
||||
AutoLocalize = false,
|
||||
DisplayOrder = display_order,
|
||||
reference(ref),
|
||||
|
||||
create "Frame" {
|
||||
AutoLocalize = false,
|
||||
Position = function()
|
||||
return if snapped() then snap_pos() else UDim2.fromOffset(x_position(), y_position())
|
||||
end,
|
||||
|
||||
Size = function()
|
||||
return if snapped() then UDim2.fromOffset(x_size() + 6, snap_size().Y.Offset) else UDim2.fromOffset(x_size() + 6, y_size() + 6)
|
||||
end,
|
||||
|
||||
Active = true,
|
||||
|
||||
BackgroundColor3 = theme.bg[0],
|
||||
|
||||
MouseMoved = function()
|
||||
if resizing() then return end
|
||||
|
||||
local mposition = UserInputService:GetMouseLocation()
|
||||
local top_inset, bottom_inset = GuiService:GetGuiInset()
|
||||
position += - top_inset - bottom_inset
|
||||
local x, y = mposition.X, mposition.Y
|
||||
x -= absolute_position().X
|
||||
y -= absolute_position().Y
|
||||
|
||||
can_resize_top(y < RESIZE_RANGE)
|
||||
can_resize_left(x < RESIZE_RANGE)
|
||||
can_resize_bottom(y > (absolute_size().Y - RESIZE_RANGE))
|
||||
can_resize_right(x > (absolute_size().X - RESIZE_RANGE))
|
||||
end,
|
||||
|
||||
MouseEnter = function()
|
||||
mouse_inside(true)
|
||||
end,
|
||||
|
||||
MouseLeave = function()
|
||||
if resizing() then return end
|
||||
if RunService:IsRunning() == false then return end
|
||||
|
||||
mouse_inside(false)
|
||||
end,
|
||||
|
||||
changed("AbsolutePosition", absolute_position),
|
||||
changed("AbsoluteSize", absolute_size),
|
||||
|
||||
create "UICorner" {
|
||||
CornerRadius = radius
|
||||
},
|
||||
|
||||
shadow {},
|
||||
|
||||
borders {
|
||||
|
||||
resize_range = RESIZE_RANGE,
|
||||
min_size = min_size,
|
||||
|
||||
can_resize_top = can_resize_top,
|
||||
can_resize_bottom = can_resize_bottom,
|
||||
can_resize_left = can_resize_left,
|
||||
can_resize_right = can_resize_right,
|
||||
|
||||
resizing = resizing,
|
||||
},
|
||||
|
||||
container {
|
||||
Size = UDim2.fromScale(1, 1),
|
||||
|
||||
create "UIListLayout" {},
|
||||
|
||||
topbar {
|
||||
title = props.title,
|
||||
subtitle = props.subtitle,
|
||||
dragging = dragging,
|
||||
offset = offset,
|
||||
bind_to_close = props.bind_to_close,
|
||||
radius = radius
|
||||
},
|
||||
|
||||
divider {},
|
||||
|
||||
container {
|
||||
Size = UDim2.fromScale(1, 0),
|
||||
|
||||
padding {
|
||||
x = UDim.new(0, 8),
|
||||
y = UDim.new(0, 8)
|
||||
},
|
||||
|
||||
unpack(props),
|
||||
|
||||
create "UIFlexItem" {
|
||||
FlexMode = Enum.UIFlexMode.Grow
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
docks.snappable {
|
||||
dragging = dragging,
|
||||
|
||||
snapped = snapped,
|
||||
position = snap_pos,
|
||||
size = snap_size
|
||||
}
|
||||
|
||||
},
|
||||
|
||||
}
|
||||
|
||||
end
|
||||
150
modules/Jabby/ui/components/display/widget/topbar.luau
Normal file
150
modules/Jabby/ui/components/display/widget/topbar.luau
Normal file
|
|
@ -0,0 +1,150 @@
|
|||
|
||||
local vide = require(script.Parent.Parent.Parent.Parent.Parent.vide)
|
||||
local theme = require(script.Parent.Parent.Parent.Parent.util.theme)
|
||||
local list = require(script.Parent.Parent.Parent.util.list)
|
||||
local padding = require(script.Parent.Parent.Parent.util.padding)
|
||||
local rounded_frame = require(script.Parent.Parent.Parent.util.rounded_frame)
|
||||
local typography = require(script.Parent.Parent.typography)
|
||||
|
||||
local create = vide.create
|
||||
local source = vide.source
|
||||
local changed = vide.changed
|
||||
local spring = vide.spring
|
||||
local show = vide.show
|
||||
|
||||
type Source<T> = vide.Source<T>
|
||||
type props = {
|
||||
|
||||
title: (string | () -> string)?,
|
||||
subtitle: (string | () -> string)?,
|
||||
bind_to_close: (() -> ())?,
|
||||
|
||||
radius: () -> UDim,
|
||||
|
||||
dragging: Source<boolean>,
|
||||
offset: (new: Vector2) -> (),
|
||||
|
||||
[any]: any,
|
||||
}
|
||||
|
||||
return function(props: props)
|
||||
local bind_to_close = props.bind_to_close
|
||||
local dragging = props.dragging
|
||||
local offset = props.offset
|
||||
|
||||
local closeable = not not bind_to_close
|
||||
|
||||
local absolute_position = source(Vector2.zero)
|
||||
|
||||
local gui_state = source(Enum.GuiState.Idle)
|
||||
|
||||
return rounded_frame {
|
||||
|
||||
name = "Topbar",
|
||||
|
||||
size = UDim2.new(1, 0, 0, 48),
|
||||
color = theme.bg[3],
|
||||
|
||||
topleft = props.radius,
|
||||
topright = props.radius,
|
||||
|
||||
create "ImageButton" {
|
||||
Size = UDim2.fromScale(1, 1),
|
||||
AutoLocalize = false,
|
||||
BackgroundTransparency = 1,
|
||||
ZIndex = 1000,
|
||||
|
||||
changed("AbsolutePosition", absolute_position),
|
||||
|
||||
MouseButton1Down = function(x, y)
|
||||
offset(absolute_position() - Vector2.new(x, y))
|
||||
dragging(true)
|
||||
end,
|
||||
create "UIListLayout" {
|
||||
FillDirection = Enum.FillDirection.Horizontal,
|
||||
VerticalAlignment = Enum.VerticalAlignment.Center,
|
||||
HorizontalFlex = Enum.UIFlexAlignment.Fill,
|
||||
},
|
||||
|
||||
padding {
|
||||
x = UDim.new(0, 16)
|
||||
},
|
||||
|
||||
list {
|
||||
spacing = UDim.new(),
|
||||
|
||||
typography {
|
||||
size = UDim2.fromScale(1, 0),
|
||||
text = props.title,
|
||||
|
||||
xalignment = Enum.TextXAlignment.Left,
|
||||
truncate = Enum.TextTruncate.SplitWord,
|
||||
textsize = 20,
|
||||
header = true,
|
||||
},
|
||||
|
||||
show(function()
|
||||
return props.subtitle ~= nil
|
||||
end, function()
|
||||
return typography {
|
||||
size = UDim2.fromScale(1, 0),
|
||||
text = props.subtitle,
|
||||
bold = true,
|
||||
|
||||
xalignment = Enum.TextXAlignment.Left,
|
||||
truncate = Enum.TextTruncate.SplitWord,
|
||||
|
||||
textsize = 16,
|
||||
}
|
||||
end)
|
||||
|
||||
},
|
||||
|
||||
show(source(closeable), function()
|
||||
return create "ImageButton" {
|
||||
|
||||
Size = UDim2.fromOffset(32, 32),
|
||||
|
||||
BackgroundColor3 = spring(function()
|
||||
return if gui_state() == Enum.GuiState.Hover then
|
||||
theme.bg[5]()
|
||||
elseif gui_state() == Enum.GuiState.Press then
|
||||
theme.bg[0]()
|
||||
else
|
||||
theme.bg[3]()
|
||||
end, 0.1),
|
||||
|
||||
changed("GuiState", gui_state),
|
||||
|
||||
Activated = props.bind_to_close,
|
||||
|
||||
create "UICorner" {
|
||||
CornerRadius = UDim.new(1, 0)
|
||||
},
|
||||
|
||||
create "ImageLabel" {
|
||||
Size = UDim2.fromOffset(24, 24),
|
||||
Position = UDim2.fromScale(0.5, 0.5),
|
||||
AnchorPoint = Vector2.new(0.5, 0.5),
|
||||
AutoLocalize = false,
|
||||
|
||||
BackgroundTransparency = 1,
|
||||
|
||||
ImageColor3 = theme.fg_on_bg_high[3],
|
||||
|
||||
Image = "rbxassetid://10747384394",
|
||||
|
||||
},
|
||||
|
||||
create "UIFlexItem" {
|
||||
FlexMode = Enum.UIFlexMode.Custom,
|
||||
ShrinkRatio = 0
|
||||
}
|
||||
|
||||
}
|
||||
end)
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
end
|
||||
77
modules/Jabby/ui/components/graph/bargraph.luau
Normal file
77
modules/Jabby/ui/components/graph/bargraph.luau
Normal file
|
|
@ -0,0 +1,77 @@
|
|||
local vide = require(script.Parent.Parent.Parent.Parent.vide)
|
||||
local theme = require(script.Parent.Parent.Parent.util.theme)
|
||||
local container = require(script.Parent.Parent.util.container)
|
||||
|
||||
local create = vide.create
|
||||
local indexes = vide.indexes
|
||||
local read = vide.read
|
||||
|
||||
type can<T> = T | () -> T
|
||||
type props = {
|
||||
|
||||
position: can<UDim2>?,
|
||||
size: can<UDim2>?,
|
||||
anchorpoint: can<UDim2>?,
|
||||
|
||||
values: () -> {number},
|
||||
max: can<number>?,
|
||||
min: can<number>?,
|
||||
|
||||
[number]: Instance
|
||||
|
||||
}
|
||||
|
||||
return function(props: props)
|
||||
|
||||
local max = props.max or function()
|
||||
return math.max(unpack(props.values()))
|
||||
end
|
||||
|
||||
local function total()
|
||||
return #props.values()
|
||||
end
|
||||
|
||||
return container {
|
||||
|
||||
Position = props.position,
|
||||
Size = props.size,
|
||||
AnchorPoint = props.anchorpoint,
|
||||
|
||||
ClipsDescendants = true,
|
||||
|
||||
indexes(props.values, function(value, index)
|
||||
|
||||
return create "Frame" {
|
||||
AutoLocalize = false,
|
||||
|
||||
Position = function()
|
||||
return UDim2.fromScale((index - 1) / total(), 1)
|
||||
end,
|
||||
Size = function()
|
||||
return UDim2.fromScale(
|
||||
1/total(),
|
||||
value() / read(max)
|
||||
)
|
||||
end,
|
||||
AnchorPoint = Vector2.new(0, 1),
|
||||
|
||||
create "UIGradient" {
|
||||
Color = function()
|
||||
return ColorSequence.new(
|
||||
theme.acc[10](),
|
||||
theme.acc[-3]()
|
||||
)
|
||||
end,
|
||||
|
||||
Rotation = 90
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
end),
|
||||
|
||||
unpack(props)
|
||||
|
||||
}
|
||||
|
||||
end
|
||||
53
modules/Jabby/ui/components/graph/graph.luau
Normal file
53
modules/Jabby/ui/components/graph/graph.luau
Normal file
|
|
@ -0,0 +1,53 @@
|
|||
local vide = require(script.Parent.Parent.Parent.Parent.vide)
|
||||
local theme = require(script.Parent.Parent.Parent.util.theme)
|
||||
local container = require(script.Parent.Parent.util.container)
|
||||
|
||||
local create = vide.create
|
||||
local source = vide.source
|
||||
local action = vide.action
|
||||
local effect = vide.effect
|
||||
|
||||
type can<T> = T | () -> T
|
||||
type props = {
|
||||
|
||||
position: can<UDim2>?,
|
||||
size: can<UDim2>?,
|
||||
anchorpoint: can<UDim2>?,
|
||||
|
||||
values: () -> {Path2DControlPoint},
|
||||
|
||||
[number]: Instance
|
||||
|
||||
}
|
||||
|
||||
return function(props: props)
|
||||
|
||||
local path2d: vide.Source<Path2D> = source()
|
||||
|
||||
effect(function()
|
||||
if not path2d() then return end
|
||||
|
||||
local path = path2d()
|
||||
path:SetControlPoints(props.values())
|
||||
end)
|
||||
|
||||
return container {
|
||||
|
||||
Position = props.position,
|
||||
Size = props.size,
|
||||
AnchorPoint = props.anchorpoint,
|
||||
|
||||
create "Path2D" {
|
||||
|
||||
Thickness = 2,
|
||||
Color3 = theme.acc[3],
|
||||
|
||||
action(path2d)
|
||||
|
||||
},
|
||||
|
||||
unpack(props)
|
||||
|
||||
}
|
||||
|
||||
end
|
||||
74
modules/Jabby/ui/components/graph/linegraph.luau
Normal file
74
modules/Jabby/ui/components/graph/linegraph.luau
Normal file
|
|
@ -0,0 +1,74 @@
|
|||
local vide = require(script.Parent.Parent.Parent.Parent.vide)
|
||||
local theme = require(script.Parent.Parent.Parent.util.theme)
|
||||
local container = require(script.Parent.Parent.util.container)
|
||||
|
||||
local create = vide.create
|
||||
local source = vide.source
|
||||
local action = vide.action
|
||||
local effect = vide.effect
|
||||
local read = vide.read
|
||||
|
||||
type can<T> = T | () -> T
|
||||
type props = {
|
||||
|
||||
position: can<UDim2>?,
|
||||
size: can<UDim2>?,
|
||||
anchorpoint: can<UDim2>?,
|
||||
|
||||
values: () -> {number},
|
||||
max: can<number>?,
|
||||
min: can<number>?,
|
||||
|
||||
[number]: Instance
|
||||
}
|
||||
|
||||
return function(props: props)
|
||||
|
||||
local path2d: vide.Source<Path2D> = source()
|
||||
|
||||
effect(function()
|
||||
if not path2d() then return end
|
||||
|
||||
local path = path2d()
|
||||
local points = table.create(50)
|
||||
local total = #props.values()
|
||||
local max = read(props.max) or 100
|
||||
local min = read(props.min) or 0
|
||||
local diff = math.abs(max - min)
|
||||
|
||||
for index, value in props.values() do
|
||||
table.insert(
|
||||
points,
|
||||
Path2DControlPoint.new(
|
||||
UDim2.fromScale(
|
||||
(index - 1) / (total - 1), 1 - (value - min) / diff
|
||||
)
|
||||
)
|
||||
)
|
||||
end
|
||||
|
||||
path:SetControlPoints(points)
|
||||
end)
|
||||
|
||||
return container {
|
||||
|
||||
Position = props.position,
|
||||
Size = props.size,
|
||||
AnchorPoint = props.anchorpoint,
|
||||
|
||||
ClipsDescendants = true,
|
||||
|
||||
create "Path2D" {
|
||||
|
||||
Thickness = 2,
|
||||
Color3 = theme.acc[3],
|
||||
|
||||
action(path2d)
|
||||
|
||||
},
|
||||
|
||||
unpack(props)
|
||||
|
||||
}
|
||||
|
||||
end
|
||||
160
modules/Jabby/ui/components/interactable/button.luau
Normal file
160
modules/Jabby/ui/components/interactable/button.luau
Normal file
|
|
@ -0,0 +1,160 @@
|
|||
local vide = require(script.Parent.Parent.Parent.Parent.vide)
|
||||
local anim = require(script.Parent.Parent.Parent.util.anim)
|
||||
local theme = require(script.Parent.Parent.Parent.util.theme)
|
||||
local typography = require(script.Parent.Parent.display.typography)
|
||||
local padding = require(script.Parent.Parent.util.padding)
|
||||
|
||||
local create = vide.create
|
||||
local source = vide.source
|
||||
local changed = vide.changed
|
||||
local show = vide.show
|
||||
local read = vide.read
|
||||
|
||||
type can<T> = T | () -> T
|
||||
type props = {
|
||||
|
||||
size: can<UDim2>?,
|
||||
position: can<UDim2>?,
|
||||
anchorpoint: can<Vector2>?,
|
||||
automaticsize: can<Enum.AutomaticSize>?,
|
||||
|
||||
text: can<string>?,
|
||||
disabled: can<boolean>?,
|
||||
|
||||
activated: () -> ()?,
|
||||
mouse2: () -> ()?,
|
||||
down: () -> ()?,
|
||||
up: () -> ()?,
|
||||
|
||||
--- enables the stroke (enabled by default)
|
||||
stroke: can<boolean>?,
|
||||
--- enables the corner (enabled by default)
|
||||
corner: can<boolean>?,
|
||||
accent: can<boolean>?,
|
||||
|
||||
xalignment: can<Enum.TextXAlignment>?,
|
||||
|
||||
code: can<boolean>?,
|
||||
|
||||
[number]: any
|
||||
|
||||
}
|
||||
|
||||
return function(props: props)
|
||||
|
||||
local guistate = source(Enum.GuiState.Idle)
|
||||
|
||||
local function bg()
|
||||
local accent = read(props.accent)
|
||||
local guistate = guistate()
|
||||
|
||||
return if accent then
|
||||
if guistate == Enum.GuiState.NonInteractable then theme.acc[-5]()
|
||||
elseif guistate == Enum.GuiState.Idle then theme.acc[0]()
|
||||
elseif guistate == Enum.GuiState.Hover then theme.acc[3]()
|
||||
elseif guistate == Enum.GuiState.Press then theme.acc[-8]()
|
||||
else theme.acc[0]()
|
||||
else
|
||||
if guistate == Enum.GuiState.NonInteractable then theme.bg[-2]()
|
||||
elseif guistate == Enum.GuiState.Idle then theme.bg[3]()
|
||||
elseif guistate == Enum.GuiState.Hover then theme.bg[6]()
|
||||
elseif guistate == Enum.GuiState.Press then theme.bg[0]()
|
||||
else theme.acc[0]()
|
||||
end
|
||||
|
||||
local function stroke()
|
||||
local accent = read(props.accent)
|
||||
local guistate = guistate()
|
||||
|
||||
return if accent then
|
||||
if guistate == Enum.GuiState.NonInteractable then theme.acc[-7]()
|
||||
else theme.acc[-7]()
|
||||
else
|
||||
if guistate == Enum.GuiState.NonInteractable then theme.bg[-3]()
|
||||
else theme.bg[-3]()
|
||||
end
|
||||
|
||||
return create "TextButton" {
|
||||
|
||||
Name = props.text,
|
||||
AutoLocalize = false,
|
||||
|
||||
Size = props.size or UDim2.fromOffset(100, 30),
|
||||
Position = props.position,
|
||||
AnchorPoint = props.anchorpoint,
|
||||
AutomaticSize = props.automaticsize,
|
||||
|
||||
Interactable = function()
|
||||
return not read(props.disabled)
|
||||
end,
|
||||
|
||||
BackgroundColor3 = anim(bg),
|
||||
|
||||
Activated = props.activated,
|
||||
MouseButton2Click = props.mouse2,
|
||||
MouseButton1Down = props.down,
|
||||
MouseButton1Up = props.up,
|
||||
|
||||
typography {
|
||||
|
||||
position = UDim2.fromScale(0.5, 0.5),
|
||||
anchorpoint = Vector2.new(0.5, 0.5),
|
||||
size = UDim2.fromScale(1, 1),
|
||||
automaticsize = Enum.AutomaticSize.Y,
|
||||
|
||||
text = props.text,
|
||||
truncate = Enum.TextTruncate.SplitWord,
|
||||
xalignment = props.xalignment,
|
||||
|
||||
accent = props.accent,
|
||||
disabled = props.disabled,
|
||||
|
||||
visible = function()
|
||||
return read(props.text) ~= ""
|
||||
end,
|
||||
|
||||
code = props.code,
|
||||
|
||||
create "UIFlexItem" {
|
||||
FlexMode = Enum.UIFlexMode.Fill
|
||||
}
|
||||
|
||||
},
|
||||
|
||||
show(
|
||||
function()
|
||||
return read(props.stroke) ~= false
|
||||
end,
|
||||
source(
|
||||
create "UIStroke" {
|
||||
ApplyStrokeMode = Enum.ApplyStrokeMode.Border,
|
||||
|
||||
Color = anim(stroke),
|
||||
Thickness = 1,
|
||||
Enabled = props.stroke
|
||||
}
|
||||
)
|
||||
),
|
||||
|
||||
show(
|
||||
function()
|
||||
return read(props.corner) ~= false
|
||||
end,
|
||||
source(
|
||||
create "UICorner" {
|
||||
CornerRadius = UDim.new(0,4)
|
||||
}
|
||||
)
|
||||
),
|
||||
|
||||
padding {
|
||||
x = UDim.new(0, 8),
|
||||
y = UDim.new(0, 2)
|
||||
},
|
||||
|
||||
changed("GuiState", guistate),
|
||||
|
||||
unpack(props),
|
||||
|
||||
}
|
||||
end
|
||||
166
modules/Jabby/ui/components/interactable/select.luau
Normal file
166
modules/Jabby/ui/components/interactable/select.luau
Normal file
|
|
@ -0,0 +1,166 @@
|
|||
local vide = require(script.Parent.Parent.Parent.Parent.vide)
|
||||
local theme = require(script.Parent.Parent.Parent.util.theme)
|
||||
local scroll_frame = require(script.Parent.Parent.display.scroll_frame)
|
||||
local container = require(script.Parent.Parent.util.container)
|
||||
local list = require(script.Parent.Parent.util.list)
|
||||
local padding = require(script.Parent.Parent.util.padding)
|
||||
local portal = require(script.Parent.Parent.util.portal)
|
||||
local button = require(script.Parent.button)
|
||||
|
||||
local create = vide.create
|
||||
local source = vide.source
|
||||
local changed = vide.changed
|
||||
local indexes = vide.indexes
|
||||
local spring = vide.spring
|
||||
local read = vide.read
|
||||
|
||||
local MAX_SIZE = 100
|
||||
|
||||
type can<T> = T | () -> T
|
||||
type dropdown = {
|
||||
size: can<UDim2>?,
|
||||
position: can<UDim2>?,
|
||||
anchorpoint: can<Vector2>?,
|
||||
|
||||
selected: can<number>,
|
||||
update_selected: (() -> number)?,
|
||||
|
||||
options: can<{string}>
|
||||
}
|
||||
|
||||
local function dropdown(props: dropdown)
|
||||
|
||||
local selected = props.selected
|
||||
local update_selected = props.update_selected or function() end
|
||||
local options = props.options
|
||||
|
||||
local enabled = source(false)
|
||||
local absolute_size = source(Vector2.zero)
|
||||
|
||||
local size = spring(function()
|
||||
if not enabled() then return UDim2.fromScale(1, 0) end
|
||||
return UDim2.new(1, 0, 0, math.min(MAX_SIZE, absolute_size().Y))
|
||||
end, 0.1)
|
||||
|
||||
return button {
|
||||
|
||||
size = props.size or UDim2.fromOffset(200, 32),
|
||||
position = props.position or UDim2.fromScale(0.5, 0.5),
|
||||
anchorpoint = props.anchorpoint or Vector2.new(0.5, 0.5),
|
||||
|
||||
xalignment = Enum.TextXAlignment.Left,
|
||||
|
||||
text = function()
|
||||
return read(options)[read(selected)]
|
||||
end,
|
||||
|
||||
activated = function()
|
||||
enabled(not enabled())
|
||||
end,
|
||||
|
||||
create "UIListLayout" {
|
||||
FillDirection = Enum.FillDirection.Horizontal,
|
||||
VerticalAlignment = Enum.VerticalAlignment.Center,
|
||||
Padding = UDim.new(0, 8),
|
||||
},
|
||||
|
||||
container {
|
||||
|
||||
AnchorPoint = Vector2.new(1, 0),
|
||||
Position = UDim2.fromScale(1, 0),
|
||||
Size = UDim2.new(0, 18, 0, 16),
|
||||
|
||||
LayoutOrder = -1,
|
||||
|
||||
create "ImageLabel" {
|
||||
|
||||
Name = "arrow",
|
||||
AutoLocalize = false,
|
||||
|
||||
Size = UDim2.new(0, 8, 0, 4),
|
||||
Position = UDim2.fromScale(0.5, 0.5),
|
||||
AnchorPoint = Vector2.new(0.5, 0.5),
|
||||
Rotation = spring(function()
|
||||
return if enabled() then -180 else 0
|
||||
end, 0.1),
|
||||
|
||||
BackgroundTransparency = 1,
|
||||
|
||||
BackgroundColor3 = theme.fg_on_bg_high[3],
|
||||
|
||||
Image = "rbxassetid://7260137654",
|
||||
ImageColor3 = theme.fg_on_bg_low[3],
|
||||
ScaleType = Enum.ScaleType.Stretch
|
||||
|
||||
}
|
||||
|
||||
},
|
||||
|
||||
portal {
|
||||
inherit_layout = true,
|
||||
|
||||
container {
|
||||
Position = UDim2.new(0, 1, 1, 4),
|
||||
Size = size,
|
||||
|
||||
BackgroundTransparency = 0,
|
||||
BackgroundColor3 = theme.bg[3],
|
||||
|
||||
ClipsDescendants = true,
|
||||
|
||||
Visible = function()
|
||||
return size().Y.Offset > 1
|
||||
end,
|
||||
|
||||
padding {
|
||||
padding = UDim.new(0, 2)
|
||||
},
|
||||
|
||||
scroll_frame {
|
||||
Size = UDim2.fromScale(1, 1),
|
||||
ScrollBarThickness = 4,
|
||||
AutomaticCanvasSize = Enum.AutomaticSize.Y,
|
||||
|
||||
list {
|
||||
spacing = UDim.new(0, 1),
|
||||
|
||||
changed("AbsoluteSize", absolute_size),
|
||||
|
||||
indexes(function()
|
||||
return read(options)
|
||||
end, function(value, key)
|
||||
return button {
|
||||
size = UDim2.new(1, 0, 0, 30),
|
||||
text = value,
|
||||
stroke = false,
|
||||
|
||||
activated = function()
|
||||
enabled(false)
|
||||
update_selected(key)
|
||||
end,
|
||||
|
||||
create "UIListLayout" {
|
||||
FillDirection = Enum.FillDirection.Horizontal,
|
||||
VerticalAlignment = Enum.VerticalAlignment.Center,
|
||||
Padding = UDim.new(0, 8),
|
||||
},
|
||||
}
|
||||
end)
|
||||
}
|
||||
},
|
||||
|
||||
create "UIStroke" {
|
||||
Color = theme.bg[-3]
|
||||
},
|
||||
|
||||
create "UICorner" {
|
||||
CornerRadius = UDim.new(0, 3)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
end
|
||||
|
||||
return dropdown
|
||||
175
modules/Jabby/ui/components/interactable/textfield.luau
Normal file
175
modules/Jabby/ui/components/interactable/textfield.luau
Normal file
|
|
@ -0,0 +1,175 @@
|
|||
local vide = require(script.Parent.Parent.Parent.Parent.vide)
|
||||
local anim = require(script.Parent.Parent.Parent.util.anim)
|
||||
local theme = require(script.Parent.Parent.Parent.util.theme)
|
||||
local typography = require(script.Parent.Parent.display.typography)
|
||||
local padding = require(script.Parent.Parent.util.padding)
|
||||
|
||||
local create = vide.create
|
||||
local source = vide.source
|
||||
local changed = vide.changed
|
||||
local effect = vide.effect
|
||||
local action = vide.action
|
||||
local read = vide.read
|
||||
|
||||
type can<T> = T | () -> T
|
||||
type props = {
|
||||
|
||||
size: can<UDim2>?,
|
||||
position: can<UDim2>?,
|
||||
anchorpoint: can<UDim2>?,
|
||||
|
||||
text: can<string>?,
|
||||
placeholder: can<string>?,
|
||||
|
||||
multiline: can<boolean>?,
|
||||
code: can<boolean>?,
|
||||
|
||||
disabled: can<boolean>?,
|
||||
|
||||
stroke: can<boolean>?,
|
||||
corner: can<boolean>?,
|
||||
|
||||
--- called whenever a character is added / removed
|
||||
oninput: ((new: string) -> ())?,
|
||||
--- called whenever focus is lost
|
||||
focuslost: ((text: string, enter: boolean?) -> ())?,
|
||||
--- called whenever focus is lost by pressing enter
|
||||
enter: ((text: string) -> ())?,
|
||||
|
||||
}
|
||||
|
||||
return function(props: props)
|
||||
|
||||
local guistate = source(Enum.GuiState.Idle)
|
||||
local focused = source(false)
|
||||
local textbox = source() :: vide.Source<TextBox>
|
||||
local text = source("")
|
||||
|
||||
effect(function()
|
||||
text(read(props.text) or "")
|
||||
end)
|
||||
|
||||
local function bg()
|
||||
local guistate = guistate()
|
||||
|
||||
return if guistate == Enum.GuiState.NonInteractable then theme.bg[0]()
|
||||
elseif focused() then theme.bg[-3]()
|
||||
else theme.bg[-2]()
|
||||
end
|
||||
|
||||
local function fg()
|
||||
local disabled = read(props.disabled)
|
||||
|
||||
return if disabled then theme.fg_on_bg_low[0]()
|
||||
else theme.fg_on_bg_high[0]()
|
||||
end
|
||||
|
||||
local function stroke()
|
||||
local guistate = guistate()
|
||||
|
||||
return if guistate == Enum.GuiState.NonInteractable then theme.bg[-3]()
|
||||
elseif focused() then theme.acc[5]()
|
||||
elseif guistate == Enum.GuiState.Idle then theme.bg[-3]()
|
||||
elseif guistate == Enum.GuiState.Hover then theme.bg[3]()
|
||||
else theme.bg[-3]()
|
||||
end
|
||||
|
||||
-- this effect will automatically focus the textbox if focused is true
|
||||
effect(function()
|
||||
if focused() == true and textbox() then
|
||||
textbox():CaptureFocus()
|
||||
end
|
||||
end)
|
||||
|
||||
return create "TextButton" {
|
||||
|
||||
Name = props.placeholder or "Textbox",
|
||||
AutoLocalize = false,
|
||||
|
||||
Size = props.size or UDim2.fromOffset(300, 30),
|
||||
Position = props.position,
|
||||
AnchorPoint = props.anchorpoint,
|
||||
|
||||
Activated = function()
|
||||
focused(true)
|
||||
end,
|
||||
|
||||
Interactable = function()
|
||||
return not props.disabled
|
||||
end,
|
||||
|
||||
BackgroundColor3 = anim(bg),
|
||||
|
||||
ClipsDescendants = true,
|
||||
|
||||
create "TextBox" {
|
||||
|
||||
Size = UDim2.fromScale(1, 1),
|
||||
AutoLocalize = false,
|
||||
|
||||
MultiLine = props.multiline,
|
||||
|
||||
BackgroundTransparency = 1,
|
||||
|
||||
Focused = function()
|
||||
focused(true)
|
||||
end,
|
||||
|
||||
FocusLost = function(enter)
|
||||
focused(false)
|
||||
if props.focuslost then
|
||||
props.focuslost(text(), enter)
|
||||
end
|
||||
|
||||
if props.enter then
|
||||
props.enter(text())
|
||||
end
|
||||
end,
|
||||
|
||||
TextSize = theme.body,
|
||||
FontFace = function()
|
||||
return if read(props.code) then theme.code else theme.font
|
||||
end,
|
||||
TextColor3 = anim(fg),
|
||||
PlaceholderColor3 = theme.fg_on_bg_low[0],
|
||||
|
||||
PlaceholderText = props.placeholder,
|
||||
Text = props.text,
|
||||
|
||||
ClipsDescendants = true,
|
||||
|
||||
TextXAlignment = Enum.TextXAlignment.Left,
|
||||
TextYAlignment = function()
|
||||
return if read(props.multiline) then Enum.TextYAlignment.Top else Enum.TextYAlignment.Center
|
||||
end,
|
||||
|
||||
action(textbox),
|
||||
changed("Text", text),
|
||||
if props.oninput then changed("Text", props.oninput) else nil
|
||||
|
||||
},
|
||||
|
||||
create "UIStroke" {
|
||||
ApplyStrokeMode = Enum.ApplyStrokeMode.Border,
|
||||
|
||||
Color = anim(stroke),
|
||||
Thickness = 1,
|
||||
Enabled = props.stroke
|
||||
},
|
||||
|
||||
create "UICorner" {
|
||||
CornerRadius = function()
|
||||
return if read(props.corner) == false then UDim.new() else UDim.new(0, 4)
|
||||
end
|
||||
},
|
||||
|
||||
padding {
|
||||
x = UDim.new(0, 8),
|
||||
y = UDim.new(0, 2)
|
||||
},
|
||||
|
||||
changed("GuiState", guistate)
|
||||
|
||||
}
|
||||
|
||||
end
|
||||
25
modules/Jabby/ui/components/util/container.luau
Normal file
25
modules/Jabby/ui/components/util/container.luau
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
--[[
|
||||
|
||||
container is a basic transparent frame that covers the entire frame.
|
||||
|
||||
]]
|
||||
|
||||
local vide = require(script.Parent.Parent.Parent.Parent.vide)
|
||||
|
||||
local create = vide.create
|
||||
|
||||
local function container(props: vide.vFrame)
|
||||
return create "Frame" {
|
||||
|
||||
Name = "Container",
|
||||
AutoLocalize = false,
|
||||
|
||||
Size = UDim2.fromScale(1, 1),
|
||||
|
||||
BackgroundTransparency = 1,
|
||||
|
||||
props,
|
||||
}
|
||||
end
|
||||
|
||||
return container
|
||||
31
modules/Jabby/ui/components/util/gap.luau
Normal file
31
modules/Jabby/ui/components/util/gap.luau
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
local vide = require(script.Parent.Parent.Parent.Parent.vide)
|
||||
local container = require(script.Parent.container)
|
||||
|
||||
local read = vide.read
|
||||
|
||||
type can<T> = T | () -> T
|
||||
type props = {
|
||||
|
||||
gap: can<number>,
|
||||
direction: can<"x" | "y">?,
|
||||
|
||||
}
|
||||
|
||||
return function(props: props)
|
||||
|
||||
local function direction()
|
||||
return read(props.direction) or "x"
|
||||
end
|
||||
|
||||
return container {
|
||||
|
||||
Size = function()
|
||||
return if direction() == "x" then
|
||||
UDim2.new(0, read(props.gap), 1, 0)
|
||||
else
|
||||
UDim2.new(1, 0, 0, read(props.gap))
|
||||
end
|
||||
|
||||
}
|
||||
|
||||
end
|
||||
55
modules/Jabby/ui/components/util/list.luau
Normal file
55
modules/Jabby/ui/components/util/list.luau
Normal file
|
|
@ -0,0 +1,55 @@
|
|||
--[[
|
||||
|
||||
Creates a container for a list of elements.
|
||||
|
||||
]]
|
||||
|
||||
local vide = require(script.Parent.Parent.Parent.Parent.vide)
|
||||
local container = require(script.Parent.container)
|
||||
|
||||
local create = vide.create
|
||||
local read = vide.read
|
||||
|
||||
type can<T> = (() -> T) | T
|
||||
type layout = {
|
||||
|
||||
justifycontent: can<Enum.UIFlexAlignment>?,
|
||||
alignitems: can<Enum.ItemLineAlignment>?,
|
||||
spacing: can<number | UDim>?,
|
||||
wraps: can<boolean>?,
|
||||
|
||||
[number]: Instance
|
||||
}
|
||||
|
||||
local function layout(props: layout)
|
||||
return container {
|
||||
|
||||
Size = UDim2.fromScale(1, 0),
|
||||
AutomaticSize = Enum.AutomaticSize.Y,
|
||||
|
||||
create "UIListLayout" {
|
||||
Padding = function()
|
||||
local spacing: number | UDim? = read(props.spacing)
|
||||
|
||||
return if typeof(spacing) == "number" then
|
||||
UDim.new(0, spacing)
|
||||
elseif typeof(spacing) == "UDim" then
|
||||
spacing
|
||||
elseif typeof(spacing) == "nil" then
|
||||
UDim.new(0, 8)
|
||||
else
|
||||
error("incorrect spacing type")
|
||||
end,
|
||||
|
||||
VerticalFlex = props.justifycontent,
|
||||
ItemLineAlignment = props.alignitems,
|
||||
Wraps = props.wraps
|
||||
|
||||
},
|
||||
|
||||
unpack(props)
|
||||
}
|
||||
|
||||
end
|
||||
|
||||
return layout
|
||||
41
modules/Jabby/ui/components/util/padding.luau
Normal file
41
modules/Jabby/ui/components/util/padding.luau
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
--[[
|
||||
|
||||
Fast and easy to use padding utility to make controlling padding quick and simple.
|
||||
|
||||
]]
|
||||
|
||||
local vide = require(script.Parent.Parent.Parent.Parent.vide)
|
||||
|
||||
local create = vide.create
|
||||
|
||||
type can<T> = T | () -> T
|
||||
type padding = {
|
||||
padding: can<UDim>?,
|
||||
x: can<UDim>?,
|
||||
y: can<UDim>?,
|
||||
left: can<UDim>?,
|
||||
right: can<UDim>?,
|
||||
top: can<UDim>?,
|
||||
bottom: can<UDim>?
|
||||
}
|
||||
|
||||
local function padding(props: padding)
|
||||
|
||||
local padding = props.padding or UDim.new(0, 8)
|
||||
local x = props.x or padding
|
||||
local y = props.y or padding
|
||||
local left = props.left or x
|
||||
local right = props.right or x
|
||||
local top = props.top or y
|
||||
local bottom = props.bottom or y
|
||||
|
||||
return create "UIPadding" {
|
||||
PaddingLeft = left,
|
||||
PaddingRight = right,
|
||||
PaddingTop = top,
|
||||
PaddingBottom = bottom,
|
||||
}
|
||||
|
||||
end
|
||||
|
||||
return padding
|
||||
102
modules/Jabby/ui/components/util/portal.luau
Normal file
102
modules/Jabby/ui/components/util/portal.luau
Normal file
|
|
@ -0,0 +1,102 @@
|
|||
--[[
|
||||
|
||||
portal is used to render a component over other components.
|
||||
it will find the nearest layer collector to parent itself and it's descendants
|
||||
onto, and if inherit_layout is enabled, inherits the nearest guibase2d's size and
|
||||
position properties.
|
||||
|
||||
]]
|
||||
local vide = require(script.Parent.Parent.Parent.Parent.vide)
|
||||
|
||||
local create = vide.create
|
||||
local source = vide.source
|
||||
local effect = vide.effect
|
||||
local cleanup = vide.cleanup
|
||||
local ref = vide.action
|
||||
local read = vide.read
|
||||
|
||||
type can<T> = T | () -> T
|
||||
type portal = {
|
||||
--- controls if the portal should inherit the layout of the frame it's under
|
||||
inherit_layout: can<boolean>?,
|
||||
|
||||
[number]: Instance,
|
||||
}
|
||||
|
||||
local layout = 100_000
|
||||
|
||||
local function portal(props: portal)
|
||||
|
||||
local inherit_layout = props.inherit_layout
|
||||
|
||||
local nearest_gui_base = source(nil :: GuiBase2d?)
|
||||
local nearest_layer_collector = source(nil :: LayerCollector?)
|
||||
|
||||
local size = source(UDim2.fromScale(1, 1))
|
||||
local position = source(UDim2.fromScale(0, 0))
|
||||
local reference = source(nil :: Configuration?)
|
||||
|
||||
-- this will create connections to update the size and position sources
|
||||
effect(function()
|
||||
local object = nearest_gui_base()
|
||||
if not object then return end
|
||||
|
||||
local function update()
|
||||
size(UDim2.fromOffset(object.AbsoluteSize.X, object.AbsoluteSize.Y))
|
||||
position(UDim2.fromOffset(object.AbsolutePosition.X, object.AbsolutePosition.Y))
|
||||
end
|
||||
|
||||
cleanup(object:GetPropertyChangedSignal("AbsoluteSize"):Connect(update))
|
||||
cleanup(object:GetPropertyChangedSignal("AbsolutePosition"):Connect(update))
|
||||
end)
|
||||
|
||||
-- creates a container that is mounted to somewhere.
|
||||
cleanup(vide.mount(function()
|
||||
cleanup(create "Frame" {
|
||||
|
||||
Name = `Portal:{layout}`,
|
||||
Parent = nearest_layer_collector,
|
||||
AutoLocalize = false,
|
||||
ZIndex = layout,
|
||||
|
||||
Size = function()
|
||||
return if read(inherit_layout) == true then size()
|
||||
else UDim2.fromScale(1, 1)
|
||||
end,
|
||||
Position = function()
|
||||
return if read(inherit_layout) == true then position()
|
||||
else UDim2.fromScale(0, 0)
|
||||
end,
|
||||
|
||||
BackgroundTransparency = 1,
|
||||
|
||||
unpack(props)
|
||||
|
||||
})
|
||||
end))
|
||||
|
||||
-- this is an anchor used to reference what gui base and layer collector to use.
|
||||
return create "Configuration" {
|
||||
Name = `PortalAnchor:{layout}`,
|
||||
|
||||
AncestryChanged = function()
|
||||
local reference = reference()
|
||||
if not reference then
|
||||
nearest_gui_base(nil)
|
||||
return
|
||||
end
|
||||
nearest_gui_base(reference:FindFirstAncestorWhichIsA("GuiBase2d"))
|
||||
nearest_layer_collector(reference:FindFirstAncestorWhichIsA("LayerCollector"))
|
||||
end,
|
||||
|
||||
ref(function(instance)
|
||||
layout += 1
|
||||
reference(instance)
|
||||
nearest_gui_base(instance:FindFirstAncestorWhichIsA("GuiBase2d"))
|
||||
nearest_layer_collector(instance:FindFirstAncestorWhichIsA("LayerCollector"))
|
||||
end)
|
||||
}
|
||||
|
||||
end
|
||||
|
||||
return portal
|
||||
201
modules/Jabby/ui/components/util/rounded_frame.luau
Normal file
201
modules/Jabby/ui/components/util/rounded_frame.luau
Normal file
|
|
@ -0,0 +1,201 @@
|
|||
--[[
|
||||
|
||||
rounded_frame is a special kind of frame with UICorner controls for every
|
||||
single corner.
|
||||
|
||||
]]
|
||||
|
||||
local vide = require(script.Parent.Parent.Parent.Parent.vide)
|
||||
local container = require(script.Parent.container)
|
||||
|
||||
local create = vide.create
|
||||
local read = vide.read
|
||||
|
||||
type can<T> = T | () -> T
|
||||
type rounded_frame = {
|
||||
name: can<string>?,
|
||||
size: can<UDim2>?,
|
||||
position: can<UDim2>?,
|
||||
anchor_point: can<Vector2>?,
|
||||
|
||||
topleft: can<UDim>?,
|
||||
topright: can<UDim>?,
|
||||
bottomleft: can<UDim>?,
|
||||
bottomright: can<UDim>?,
|
||||
|
||||
color: can<Color3>?,
|
||||
|
||||
layout: vide.vFrame?,
|
||||
|
||||
[number]: any,
|
||||
}
|
||||
|
||||
local function rounded_frame(props: rounded_frame)
|
||||
local topleft = props.topleft or UDim.new()
|
||||
local topright = props.topright or UDim.new()
|
||||
local bottomleft = props.bottomleft or UDim.new()
|
||||
local bottomright = props.bottomright or UDim.new()
|
||||
|
||||
local function corner(name: string, position: UDim2, anchor_point: Vector2, udim: can<UDim>)
|
||||
return create "Frame" {
|
||||
Name = name,
|
||||
AutoLocalize = false,
|
||||
|
||||
Size = function()
|
||||
return UDim2.new(read(udim), read(udim))
|
||||
end,
|
||||
Position = position,
|
||||
AnchorPoint = anchor_point,
|
||||
|
||||
BackgroundTransparency = 1,
|
||||
ClipsDescendants = true,
|
||||
|
||||
create "Frame" {
|
||||
Name = "TopLeft",
|
||||
AutoLocalize = false,
|
||||
|
||||
Size = UDim2.fromScale(2, 2),
|
||||
Position = UDim2.fromScale(-anchor_point.X, -anchor_point.Y),
|
||||
|
||||
BackgroundColor3 = props.color,
|
||||
ClipsDescendants = true,
|
||||
|
||||
create "UICorner" {
|
||||
CornerRadius = udim
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
end
|
||||
|
||||
return create "Frame" {
|
||||
|
||||
Name = props.name or "RoundedFrame",
|
||||
Size = props.size,
|
||||
Position = props.position,
|
||||
AnchorPoint = props.anchor_point,
|
||||
|
||||
BackgroundColor3 = props.color,
|
||||
|
||||
BackgroundTransparency = 1,
|
||||
|
||||
create "Folder" {
|
||||
Name = "Corner",
|
||||
|
||||
corner("TopLeft", UDim2.fromScale(0, 0), Vector2.new(0, 0), topleft),
|
||||
corner("TopRight", UDim2.fromScale(1, 0), Vector2.new(1, 0), topright),
|
||||
corner("BottomLeft", UDim2.fromScale(0, 1), Vector2.new(0, 1), bottomleft),
|
||||
corner("BottomRight", UDim2.fromScale(1, 1), Vector2.new(1, 1), bottomright),
|
||||
|
||||
create "Frame" {
|
||||
AutoLocalize = false,
|
||||
|
||||
Name = "FrameLeft",
|
||||
|
||||
Size = function()
|
||||
return UDim2.new(
|
||||
0.5,
|
||||
0,
|
||||
1 - read(topleft).Scale - read(bottomleft).Scale,
|
||||
- (read(topleft).Offset + read(bottomleft).Offset)
|
||||
)
|
||||
end,
|
||||
Position = function()
|
||||
return UDim2.new(
|
||||
0, 0,
|
||||
0.5 + read(topleft).Scale / 2 - read(bottomleft).Scale / 2,
|
||||
0 + read(topleft).Offset / 2 - read(bottomleft).Offset / 2
|
||||
)
|
||||
end,
|
||||
AnchorPoint = Vector2.new(0, 0.5),
|
||||
|
||||
BackgroundColor3 = props.color,
|
||||
},
|
||||
|
||||
create "Frame" {
|
||||
|
||||
Name = "FrameRight",
|
||||
AutoLocalize = false,
|
||||
|
||||
Size = function()
|
||||
return UDim2.new(
|
||||
0.5,
|
||||
0,
|
||||
1 - read(topright).Scale - read(bottomright).Scale,
|
||||
- (read(topright).Offset + read(bottomright).Offset)
|
||||
)
|
||||
end,
|
||||
Position = function()
|
||||
return UDim2.new(
|
||||
1, 0,
|
||||
0.5 + read(topright).Scale / 2 - read(bottomright).Scale / 2,
|
||||
0 + read(topright).Offset / 2 - read(bottomright).Offset / 2
|
||||
)
|
||||
end,
|
||||
AnchorPoint = Vector2.new(1, 0.5),
|
||||
|
||||
BackgroundColor3 = props.color,
|
||||
},
|
||||
|
||||
create "Frame" {
|
||||
|
||||
Name = "FrameTop",
|
||||
AutoLocalize = false,
|
||||
|
||||
Size = function()
|
||||
return UDim2.new(
|
||||
1 - read(topleft).Scale - read(topright).Scale,
|
||||
- (read(topleft).Offset + read(topright).Offset),
|
||||
0.5,
|
||||
0
|
||||
)
|
||||
end,
|
||||
Position = function()
|
||||
return UDim2.new(
|
||||
0.5 + read(topleft).Scale / 2 - read(topright).Scale / 2,
|
||||
0 + read(topleft).Offset / 2 - read(topright).Offset / 2,
|
||||
0, 0
|
||||
)
|
||||
end,
|
||||
AnchorPoint = Vector2.new(0.5, 0),
|
||||
|
||||
BackgroundColor3 = props.color,
|
||||
},
|
||||
|
||||
create "Frame" {
|
||||
|
||||
Name = "FrameBottom",
|
||||
AutoLocalize = false,
|
||||
|
||||
Size = function()
|
||||
return UDim2.new(
|
||||
1 - read(bottomleft).Scale - read(bottomright).Scale,
|
||||
- (read(bottomleft).Offset + read(bottomright).Offset),
|
||||
0.5,
|
||||
0
|
||||
)
|
||||
end,
|
||||
Position = function()
|
||||
return UDim2.new(
|
||||
0.5 + read(bottomleft).Scale / 2 - read(bottomright).Scale / 2,
|
||||
0 + read(bottomleft).Offset / 2 - read(bottomright).Offset / 2,
|
||||
1, 0
|
||||
)
|
||||
end,
|
||||
AnchorPoint = Vector2.new(0.5, 1),
|
||||
|
||||
BackgroundColor3 = props.color,
|
||||
},
|
||||
},
|
||||
|
||||
container {
|
||||
unpack(props)
|
||||
},
|
||||
|
||||
props.layout
|
||||
|
||||
}
|
||||
|
||||
end
|
||||
|
||||
return rounded_frame
|
||||
56
modules/Jabby/ui/components/util/row.luau
Normal file
56
modules/Jabby/ui/components/util/row.luau
Normal file
|
|
@ -0,0 +1,56 @@
|
|||
--[[
|
||||
|
||||
Creates a container for a list of elements.
|
||||
|
||||
]]
|
||||
|
||||
local vide = require(script.Parent.Parent.Parent.Parent.vide)
|
||||
local container = require(script.Parent.container)
|
||||
|
||||
local create = vide.create
|
||||
local read = vide.read
|
||||
|
||||
type can<T> = (() -> T) | T
|
||||
type layout = {
|
||||
|
||||
justifycontent: can<Enum.UIFlexAlignment>?,
|
||||
alignitems: can<Enum.ItemLineAlignment>?,
|
||||
spacing: can<number | UDim>?,
|
||||
wraps: can<boolean>?,
|
||||
|
||||
[number]: Instance
|
||||
}
|
||||
|
||||
local function layout(props: layout)
|
||||
return container {
|
||||
|
||||
Size = UDim2.fromScale(1, 0),
|
||||
AutomaticSize = Enum.AutomaticSize.Y,
|
||||
|
||||
create "UIListLayout" {
|
||||
Padding = function()
|
||||
local spacing: number | UDim? = read(props.spacing)
|
||||
|
||||
return if typeof(spacing) == "number" then
|
||||
UDim.new(0, spacing)
|
||||
elseif typeof(spacing) == "UDim" then
|
||||
spacing
|
||||
elseif typeof(spacing) == "nil" then
|
||||
UDim.new(0, 8)
|
||||
else
|
||||
error("incorrect spacing type")
|
||||
end,
|
||||
FillDirection = Enum.FillDirection.Horizontal,
|
||||
|
||||
HorizontalFlex = props.justifycontent,
|
||||
ItemLineAlignment = props.alignitems,
|
||||
Wraps = props.wraps
|
||||
|
||||
},
|
||||
|
||||
unpack(props)
|
||||
}
|
||||
|
||||
end
|
||||
|
||||
return layout
|
||||
20
modules/Jabby/ui/components/util/shadow.luau
Normal file
20
modules/Jabby/ui/components/util/shadow.luau
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
local vide = require(script.Parent.Parent.Parent.Parent.vide)
|
||||
|
||||
local create = vide.create
|
||||
|
||||
type can<T> = T | () -> T
|
||||
type props = {
|
||||
zindex: can<number>?,
|
||||
transparency: can<number>?
|
||||
}
|
||||
|
||||
return function(props: props)
|
||||
|
||||
return create "UIStroke" {
|
||||
|
||||
Thickness = 2,
|
||||
Color = Color3.new(0, 0, 0),
|
||||
Transparency = 0.8
|
||||
|
||||
}
|
||||
end
|
||||
189
modules/Jabby/ui/components/util/virtualscroller.luau
Normal file
189
modules/Jabby/ui/components/util/virtualscroller.luau
Normal file
|
|
@ -0,0 +1,189 @@
|
|||
local vide = require(script.Parent.Parent.Parent.Parent.vide)
|
||||
local scroll_frame = require(script.Parent.Parent.display.scroll_frame)
|
||||
local container = require(script.Parent.container)
|
||||
|
||||
local create = vide.create
|
||||
local source = vide.source
|
||||
local values = vide.values
|
||||
local changed = vide.changed
|
||||
local effect = vide.effect
|
||||
local untrack = vide.untrack
|
||||
local batch = vide.batch
|
||||
|
||||
type can<T> = T | () -> T
|
||||
type props = {
|
||||
|
||||
size: can<UDim2>?,
|
||||
position: can<UDim2>?,
|
||||
anchorpoint: can<UDim2>?,
|
||||
|
||||
--- streams in items. when index is -1, should expect to be unused
|
||||
item: (index: () -> number) -> Instance,
|
||||
--- streams in separators. when index is -1, should expect to be unused
|
||||
separator: ((index: () -> number) -> Instance)?,
|
||||
|
||||
item_size: number,
|
||||
separator_size: number?,
|
||||
|
||||
max_items: (() -> number)?,
|
||||
|
||||
[number]: any
|
||||
|
||||
}
|
||||
|
||||
return function(props: props)
|
||||
|
||||
local items = source({} :: {vide.Source<number>})
|
||||
|
||||
local absolute_size = source(Vector2.zero)
|
||||
local canvas_position = source(Vector2.zero)
|
||||
|
||||
local item_size = props.item_size
|
||||
local separator_size = props.separator_size or 0
|
||||
|
||||
local item = props.item
|
||||
local separator = props.separator
|
||||
|
||||
local OVERFLOW = 4
|
||||
|
||||
effect(function()
|
||||
local absolute_size = absolute_size()
|
||||
local canvas_position = canvas_position()
|
||||
|
||||
local child_size = item_size + separator_size
|
||||
local total_required = math.ceil(absolute_size.Y / child_size) + OVERFLOW
|
||||
local sources = untrack(items)
|
||||
|
||||
local min_index = math.floor(canvas_position.Y / child_size)
|
||||
local max_index = math.ceil((canvas_position.Y + absolute_size.Y) / child_size)
|
||||
|
||||
local max_items = math.huge
|
||||
if props.max_items then
|
||||
max_items = props.max_items()
|
||||
end
|
||||
|
||||
batch(function()
|
||||
untrack(function()
|
||||
-- mark any sources out of range as unused
|
||||
local unused = {}
|
||||
|
||||
for i, s in sources do
|
||||
local index = s()
|
||||
|
||||
if
|
||||
index >= math.max(min_index, 1)
|
||||
and index <= math.min(max_index, max_items)
|
||||
then continue end
|
||||
unused[i] = true
|
||||
s(-1)
|
||||
end
|
||||
|
||||
-- add sources necessary
|
||||
if #sources < total_required then
|
||||
for i = #sources + 1, total_required do
|
||||
sources[i] = source(-1)
|
||||
unused[i] = true
|
||||
end
|
||||
items(sources)
|
||||
end
|
||||
|
||||
-- update indexes of any sources that went unused
|
||||
local did_not_render = {}
|
||||
|
||||
for i = math.max(min_index, 1), math.min(max_index, max_items) do
|
||||
did_not_render[i] = true
|
||||
end
|
||||
|
||||
|
||||
for _, s in sources do
|
||||
did_not_render[s()] = nil
|
||||
end
|
||||
|
||||
for index in unused do
|
||||
local s = sources[index]
|
||||
local key = next(did_not_render)
|
||||
if not key then break end
|
||||
s(key)
|
||||
did_not_render[key] = nil
|
||||
unused[index] = nil
|
||||
end
|
||||
|
||||
-- remove unnecessary sources
|
||||
if #sources > total_required then
|
||||
for i = #sources, 1, -1 do
|
||||
if unused[i] then
|
||||
table.remove(sources, i)
|
||||
end
|
||||
unused[i] = nil
|
||||
if #sources < total_required then break end
|
||||
end
|
||||
items(sources)
|
||||
end
|
||||
|
||||
end)
|
||||
end)
|
||||
|
||||
end)
|
||||
|
||||
return scroll_frame {
|
||||
|
||||
unpack(props),
|
||||
|
||||
Size = props.size or UDim2.fromScale(1, 1),
|
||||
Position = props.position,
|
||||
AnchorPoint = props.anchorpoint,
|
||||
|
||||
BackgroundTransparency = 1,
|
||||
|
||||
CanvasSize = function()
|
||||
if props.max_items then
|
||||
return UDim2.fromOffset(0, props.max_items() * (item_size + separator_size))
|
||||
else
|
||||
local absolute_size = absolute_size()
|
||||
local canvas_position = canvas_position()
|
||||
local child_size = item_size + separator_size
|
||||
local max_index = math.ceil((canvas_position.Y + absolute_size.Y) / child_size) + OVERFLOW
|
||||
return UDim2.fromOffset(0, max_index * child_size)
|
||||
end
|
||||
end,
|
||||
|
||||
values(items, function(index)
|
||||
return create "Frame" {
|
||||
Name = index,
|
||||
AutoLocalize = false,
|
||||
|
||||
Position = function()
|
||||
if index() == -1 then UDim2.fromOffset(0, -1000) end
|
||||
return UDim2.fromOffset(
|
||||
0,
|
||||
(item_size + separator_size) * (index() - 1)
|
||||
)
|
||||
end,
|
||||
Size = UDim2.new(1, 0, 0, item_size + separator_size),
|
||||
|
||||
BackgroundTransparency = 1,
|
||||
|
||||
container {
|
||||
Name = "Item",
|
||||
|
||||
item(index),
|
||||
},
|
||||
|
||||
if separator then
|
||||
container {
|
||||
Name = "Separator",
|
||||
|
||||
separator(index)
|
||||
}
|
||||
else nil,
|
||||
|
||||
}
|
||||
end),
|
||||
|
||||
changed("AbsoluteSize", absolute_size),
|
||||
changed("CanvasPosition", canvas_position),
|
||||
|
||||
|
||||
}
|
||||
|
||||
end
|
||||
67
modules/Jabby/ui/init.luau
Normal file
67
modules/Jabby/ui/init.luau
Normal file
|
|
@ -0,0 +1,67 @@
|
|||
local accordion = require(script.components.display.accordion)
|
||||
local background = require(script.components.display.background)
|
||||
local checkbox = require(script.components.display.checkbox)
|
||||
local divider = require(script.components.display.divider)
|
||||
local pages = require(script.components.display.pages)
|
||||
local pane = require(script.components.display.pane)
|
||||
local snapping = require(script.components.display.snapping)
|
||||
local tablesheet = require(script.components.display.tablesheet)
|
||||
local typography = require(script.components.display.typography)
|
||||
local widget = require(script.components.display.widget)
|
||||
local bargraph = require(script.components.graph.bargraph)
|
||||
local graph = require(script.components.graph.graph)
|
||||
local linegraph = require(script.components.graph.linegraph)
|
||||
local button = require(script.components.interactable.button)
|
||||
local select = require(script.components.interactable.select)
|
||||
local textfield = require(script.components.interactable.textfield)
|
||||
local container = require(script.components.util.container)
|
||||
local gap = require(script.components.util.gap)
|
||||
local list = require(script.components.util.list)
|
||||
local padding = require(script.components.util.padding)
|
||||
local rounded_frame = require(script.components.util.rounded_frame)
|
||||
local row = require(script.components.util.row)
|
||||
local shadow = require(script.components.util.shadow)
|
||||
local virtualscroller = require(script.components.util.virtualscroller)
|
||||
local anim = require(script.util.anim)
|
||||
local oklch = require(script.util.oklch)
|
||||
local theme = require(script.util.theme)
|
||||
|
||||
return {
|
||||
|
||||
widget = widget,
|
||||
background = background,
|
||||
pane = pane,
|
||||
snapping = snapping,
|
||||
typography = typography,
|
||||
tablesheet = tablesheet,
|
||||
accordion = accordion,
|
||||
divider = divider,
|
||||
checkbox = checkbox,
|
||||
|
||||
bargraph = bargraph,
|
||||
graph = graph,
|
||||
linegraph = linegraph,
|
||||
|
||||
button = button,
|
||||
textfield = textfield,
|
||||
select = select,
|
||||
|
||||
pages = pages,
|
||||
|
||||
theme = theme,
|
||||
anim = anim,
|
||||
|
||||
container = container,
|
||||
rounded_frame = rounded_frame,
|
||||
list = list,
|
||||
row = row,
|
||||
shadow = shadow,
|
||||
|
||||
padding = padding,
|
||||
|
||||
virtualscroller = virtualscroller,
|
||||
gap = gap,
|
||||
|
||||
oklch = oklch,
|
||||
|
||||
}
|
||||
350
modules/Jabby/ui/libraries/apcaw3.luau
Normal file
350
modules/Jabby/ui/libraries/apcaw3.luau
Normal file
|
|
@ -0,0 +1,350 @@
|
|||
local SA98G = {
|
||||
mainTRC = 2.4,
|
||||
mainTRCencode = 1 / 2.4,
|
||||
sRco = 0.2126729,
|
||||
sGco = 0.7151522,
|
||||
sBco = 0.0721750,
|
||||
normBG = 0.56,
|
||||
normTXT = 0.57,
|
||||
revTXT = 0.62,
|
||||
revBG = 0.65,
|
||||
blkThrs = 0.022,
|
||||
blkClmp = 1.414,
|
||||
scaleBoW = 1.14,
|
||||
scaleWoB = 1.14,
|
||||
loBoWoffset = 0.027,
|
||||
loWoBoffset = 0.027,
|
||||
deltaYmin = 0.0005,
|
||||
loClip = 0.1,
|
||||
mFactor = 1.94685544331710,
|
||||
mFactInv = 1 / 1.94685544331710,
|
||||
mOffsetIn = 0.03873938165714010,
|
||||
mExpAdj = 0.2833433964208690,
|
||||
mExp = 0.2833433964208690 / 1.414,
|
||||
mOffsetOut = 0.3128657958707580,
|
||||
}
|
||||
|
||||
local function isNaN(n)
|
||||
return n ~= n
|
||||
end
|
||||
|
||||
local function reverseAPCA(contrast, knownY, knownType, returnAs)
|
||||
if contrast == nil then
|
||||
contrast = 0
|
||||
end
|
||||
if knownY == nil then
|
||||
knownY = 1.0
|
||||
end
|
||||
if knownType == nil then
|
||||
knownType = "bg"
|
||||
end
|
||||
if returnAs == nil then
|
||||
returnAs = "hex"
|
||||
end
|
||||
if math.abs(contrast) < 9 then
|
||||
return false
|
||||
end
|
||||
local unknownY = knownY
|
||||
local knownExp
|
||||
local unknownExp
|
||||
--/// APCA 0.0.98G - 4g - W3 Compatible Constants ////////////////////
|
||||
local scale = if contrast > 0 then SA98G.scaleBoW else SA98G.scaleWoB
|
||||
local offset = if contrast > 0 then SA98G.loBoWoffset else -SA98G.loWoBoffset
|
||||
contrast = (assert(tonumber(contrast)) * 0.01 + offset) / scale
|
||||
-- Soft clamps Y if it is near black.
|
||||
knownY = if (knownY > SA98G.blkThrs) then knownY else knownY + math.pow(SA98G.blkThrs - knownY, SA98G.blkClmp)
|
||||
-- set the known and unknown exponents
|
||||
if knownType == "bg" or knownType == "background" then
|
||||
knownExp = if contrast > 0 then SA98G.normBG else SA98G.revBG
|
||||
unknownExp = if contrast > 0 then SA98G.normTXT else SA98G.revTXT
|
||||
unknownY = math.pow(math.pow(knownY, knownExp) - contrast, 1 / unknownExp)
|
||||
if isNaN(unknownY) then
|
||||
return false
|
||||
end
|
||||
elseif knownType == "txt" or knownType == "text" then
|
||||
knownExp = if contrast > 0 then SA98G.normTXT else SA98G.revTXT
|
||||
unknownExp = if contrast > 0 then SA98G.normBG else SA98G.revBG
|
||||
unknownY = math.pow(contrast + math.pow(knownY, knownExp), 1 / unknownExp)
|
||||
if isNaN(unknownY) then
|
||||
return false
|
||||
end
|
||||
else
|
||||
return false
|
||||
end
|
||||
--return contrast +'----'+unknownY;
|
||||
if unknownY > 1.06 or unknownY < 0 then
|
||||
return false
|
||||
end
|
||||
-- if (unknownY < 0) { return false } // return false on underflow
|
||||
--unknownY = math.max(unknownY,0.0);
|
||||
-- unclamp
|
||||
unknownY = if (unknownY > SA98G.blkThrs) then unknownY else (math.pow(((unknownY + SA98G.mOffsetIn) * SA98G.mFactor), SA98G.mExp) * SA98G.mFactInv) - SA98G.mOffsetOut
|
||||
-- unknownY - 0.22 * math.pow(unknownY*0.5, 1/blkClmp);
|
||||
unknownY = math.max(math.min(unknownY, 1.0), 0.0)
|
||||
if returnAs == "color" then
|
||||
local colorB = math.round(math.pow(unknownY, SA98G.mainTRCencode) * 255)
|
||||
local retUse = if (knownType == "bg") then "txtColor" else "bgColor"
|
||||
return { colorB, colorB, colorB, 1, retUse }
|
||||
elseif returnAs == "Y" or returnAs == "y" then
|
||||
return math.max(0.0, unknownY)
|
||||
else
|
||||
return false
|
||||
end
|
||||
end
|
||||
|
||||
local function sRGBtoY(sRGBcolor: Color3)
|
||||
local r = sRGBcolor.R
|
||||
local g = sRGBcolor.G
|
||||
local b = sRGBcolor.B
|
||||
|
||||
local function simpleExp(chan)
|
||||
return math.pow(chan, SA98G.mainTRC)
|
||||
end
|
||||
|
||||
return SA98G.sRco * simpleExp(r) + SA98G.sGco * simpleExp(g) + SA98G.sBco * simpleExp(b)
|
||||
end
|
||||
|
||||
local function displayP3toY(rgb: Color3)
|
||||
local mainTRC = 2.4
|
||||
local sRco, sGco, sBco = 0.2289829594805780, 0.6917492625852380, 0.0792677779341829
|
||||
|
||||
local function simpleExp(chan)
|
||||
return math.pow(chan, mainTRC)
|
||||
end
|
||||
|
||||
return sRco * simpleExp(rgb.R) + sGco * simpleExp(rgb.G) + sBco * simpleExp(rgb.B)
|
||||
end
|
||||
|
||||
local function adobeRGBtoY(rgb: Color3)
|
||||
local mainTRC = 2.35
|
||||
|
||||
local sRco = 0.2973550227113810
|
||||
local sGco = 0.6273727497145280
|
||||
local sBco = 0.0752722275740913
|
||||
|
||||
local function simpleExp(chan)
|
||||
return math.pow(chan / 255.0, mainTRC)
|
||||
end
|
||||
|
||||
return sRco * simpleExp(rgb.R) + sGco * simpleExp(rgb.G) + sBco * simpleExp(rgb.B)
|
||||
end
|
||||
|
||||
|
||||
local function APCAcontrast(txtY, bgY, places)
|
||||
places = places or -1
|
||||
|
||||
local icp = { 0, 1.1 }
|
||||
|
||||
if math.min(txtY, bgY) < icp[1] or math.max(txtY, bgY) > icp[2] then
|
||||
return 0
|
||||
end
|
||||
|
||||
local SAPC = 0
|
||||
local outputContrast = 0
|
||||
local polCat = "BoW"
|
||||
|
||||
txtY = (txtY > SA98G.blkThrs) and txtY or txtY + math.pow(SA98G.blkThrs - txtY, SA98G.blkClmp)
|
||||
bgY = (bgY > SA98G.blkThrs) and bgY or bgY + math.pow(SA98G.blkThrs - bgY, SA98G.blkClmp)
|
||||
|
||||
if math.abs(bgY - txtY) < SA98G.deltaYmin then
|
||||
return 0
|
||||
end
|
||||
|
||||
if bgY > txtY then -- black text on white
|
||||
SAPC = (math.pow(bgY, SA98G.normBG) - math.pow(txtY, SA98G.normTXT)) * SA98G.scaleBoW
|
||||
|
||||
outputContrast = (SAPC < SA98G.loClip) and 0.0 or SAPC - SA98G.loBoWoffset
|
||||
else
|
||||
-- should always return negative
|
||||
polCat = "WoB" -- white on black
|
||||
|
||||
SAPC = (math.pow(bgY, SA98G.revBG) - math.pow(txtY, SA98G.revTXT)) * SA98G.scaleWoB
|
||||
|
||||
outputContrast = (SAPC > -SA98G.loClip) and 0.0 or SAPC + SA98G.loWoBoffset
|
||||
end
|
||||
|
||||
if places < 0 then
|
||||
return outputContrast * 100.0
|
||||
elseif places == 0 then
|
||||
return math.round(math.abs(outputContrast) * 100.0) --+ "<sub>" + polCat + "</sub>" -- why is there html
|
||||
elseif places // 1 == places then
|
||||
return (outputContrast * 100.0) * places // 1 / places
|
||||
else
|
||||
return 0.0
|
||||
end
|
||||
end
|
||||
|
||||
local function alphaBlend(rgbFG: Color3, aFG: number, rgbBG: Color3, round: boolean?)
|
||||
round = if round == nil then true else round
|
||||
aFG = aFG or 1
|
||||
local compBlend = 1 - aFG
|
||||
local rgbOut = {0, 0, 0}
|
||||
|
||||
rgbOut[1] = rgbBG.R * compBlend + rgbFG.R * aFG
|
||||
if round then rgbOut[1] = math.min(math.round(rgbOut[1]), 255) end
|
||||
rgbOut[2] = rgbBG.G * compBlend + rgbFG.G * aFG
|
||||
if round then rgbOut[2] = math.min(math.round(rgbOut[2]), 255) end
|
||||
rgbOut[3] = rgbBG.B * compBlend + rgbFG.B * aFG
|
||||
if round then rgbOut[3] = math.min(math.round(rgbOut[3]), 255) end
|
||||
|
||||
return Color3.new(rgbOut[1], rgbOut[2], rgbOut[3])
|
||||
|
||||
end
|
||||
|
||||
local function calcAPCA(textcolor: Color3, bgColor: Color3, textalpha: number?, places: number?, round: boolean?)
|
||||
places = -1
|
||||
|
||||
--todo: alpha blending
|
||||
if textalpha then textcolor = alphaBlend(textcolor, textalpha, bgColor, round) end
|
||||
|
||||
return APCAcontrast(sRGBtoY(textcolor), sRGBtoY(bgColor), places)
|
||||
end
|
||||
|
||||
local function fontLookupAPCA(contrast, places: number?)
|
||||
places = places or 2
|
||||
|
||||
-- Font size interpolations. Here the chart was re-ordered to put
|
||||
-- the main contrast levels each on one line, instead of font size per line.
|
||||
-- First column is LC value, then each following column is font size by weight
|
||||
|
||||
-- G G G G G G Public Beta 0.1.7 (G) • MAY 28 2022
|
||||
|
||||
-- Lc values under 70 should have Lc 15 ADDED if used for body text
|
||||
-- All font sizes are in px and reference font is Barlow
|
||||
|
||||
-- 999: prohibited - too low contrast
|
||||
-- 777: NON TEXT at this minimum weight stroke
|
||||
-- 666 - this is for spot text, not fluent-Things like copyright or placeholder.
|
||||
-- 5xx - minimum font at this weight for content, 5xx % 500 for font-size
|
||||
-- 4xx - minimum font at this weight for any purpose], 4xx % 400 for font-size
|
||||
|
||||
-- MAIN FONT SIZE LOOKUP
|
||||
|
||||
---- ASCENDING SORTED Public Beta 0.1.7 (G) • MAY 28 2022 ////
|
||||
|
||||
---- Lc 45 * 0.2 = 9 which is the index for the row for Lc 45
|
||||
|
||||
-- MAIN FONT LOOKUP May 28 2022 EXPANDED
|
||||
-- Sorted by Lc Value
|
||||
-- First row is standard weights 100-900
|
||||
-- First column is font size in px
|
||||
-- All other values are the Lc contrast
|
||||
-- 999 = too low. 777 = non-text and spot text only
|
||||
|
||||
local fontMatrixAscend = {
|
||||
{'Lc',100,200,300,400,500,600,700,800,900},
|
||||
{0,999,999,999,999,999,999,999,999,999},
|
||||
{10,999,999,999,999,999,999,999,999,999},
|
||||
{15,777,777,777,777,777,777,777,777,777},
|
||||
{20,777,777,777,777,777,777,777,777,777},
|
||||
{25,777,777,777,120,120,108,96,96,96},
|
||||
{30,777,777,120,108,108,96,72,72,72},
|
||||
{35,777,120,108,96,72,60,48,48,48},
|
||||
{40,120,108,96,60,48,42,32,32,32},
|
||||
{45,108,96,72,42,32,28,24,24,24},
|
||||
{50,96,72,60,32,28,24,21,21,21},
|
||||
{55,80,60,48,28,24,21,18,18,18},
|
||||
{60,72,48,42,24,21,18,16,16,18},
|
||||
{65,68,46,32,21.75,19,17,15,16,18},
|
||||
{70,64,44,28,19.5,18,16,14.5,16,18},
|
||||
{75,60,42,24,18,16,15,14,16,18},
|
||||
{80,56,38.25,23,17.25,15.81,14.81,14,16,18},
|
||||
{85,52,34.5,22,16.5,15.625,14.625,14,16,18},
|
||||
{90,48,32,21,16,15.5,14.5,14,16,18},
|
||||
{95,45,28,19.5,15.5,15,14,13.5,16,18},
|
||||
{100,42,26.5,18.5,15,14.5,13.5,13,16,18},
|
||||
{105,39,25,18,14.5,14,13,12,16,18},
|
||||
{110,36,24,18,14,13,12,11,16,18},
|
||||
{115,34.5,22.5,17.25,12.5,11.875,11.25,10.625,14.5,16.5},
|
||||
{120,33,21,16.5,11,10.75,10.5,10.25,13,15},
|
||||
{125,32,20,16,10,10,10,10,12,14},
|
||||
}
|
||||
|
||||
local fontDeltaAscend = {
|
||||
{'∆Lc',100,200,300,400,500,600,700,800,900},
|
||||
{0,0,0,0,0,0,0,0,0,0},
|
||||
{10,0,0,0,0,0,0,0,0,0},
|
||||
{15,0,0,0,0,0,0,0,0,0},
|
||||
{20,0,0,0,0,0,0,0,0,0},
|
||||
{25,0,0,0,12,12,12,24,24,24},
|
||||
{30,0,0,12,12,36,36,24,24,24},
|
||||
{35,0,12,12,36,24,18,16,16,16},
|
||||
{40,12,12,24,18,16,14,8,8,8},
|
||||
{45,12,24,12,10,4,4,3,3,3},
|
||||
{50,16,12,12,4,4,3,3,3,3},
|
||||
{55,8,12,6,4,3,3,2,2,0},
|
||||
{60,4,2,10,2.25,2,1,1,0,0},
|
||||
{65,4,2,4,2.25,1,1,0.5,0,0},
|
||||
{70,4,2,4,1.5,2,1,0.5,0,0},
|
||||
{75,4,3.75,1,0.75,0.188,0.188,0,0,0},
|
||||
{80,4,3.75,1,0.75,0.188,0.188,0,0,0},
|
||||
{85,4,2.5,1,0.5,0.125,0.125,0,0,0},
|
||||
{90,3,4,1.5,0.5,0.5,0.5,0.5,0,0},
|
||||
{95,3,1.5,1,0.5,0.5,0.5,0.5,0,0},
|
||||
{100,3,1.5,0.5,0.5,0.5,0.5,1,0,0},
|
||||
{105,3,1,0,0.5,1,1,1,0,0},
|
||||
{110,1.5,1.5,0.75,1.5,1.125,0.75,0.375,1.5,1.5},
|
||||
{115,1.5,1.5,0.75,1.5,1.125,0.75,0.375,1.5,1.5},
|
||||
{120,1,1,0.5,1,0.75,0.5,0.25,1,1},
|
||||
{125,0,0,0,0,0,0,0,0,0},
|
||||
};
|
||||
|
||||
local weightArray = {0, 100, 200, 300, 400, 500, 600, 700, 800, 900}
|
||||
local weightArrayLen = #weightArray
|
||||
|
||||
local returnArray = {tostring(contrast * places // 1 / places), 0, 0, 0, 0, 0, 0, 0, 0, 0}
|
||||
local returnArrayLen = #returnArray
|
||||
|
||||
local contrastArrayAscend = {'lc',0,10,15,20,25,30,35,40,45,50,55,60,65,70,75,80,85,90,95,100,105,110,115,120,125,}
|
||||
local contrastArrayLenAsc = #contrastArrayAscend
|
||||
|
||||
-- Lc 45 * 0.2 = 9 and 9 is the index for the row for lc 45
|
||||
|
||||
local tempFont = 777
|
||||
local contrast = math.abs(contrast)
|
||||
local factor = 0.2
|
||||
local index = contrast == 0 and 1 or bit32.bor(contrast * factor, 0)
|
||||
local w = 0
|
||||
local scoreAdj = (contrast - fontMatrixAscend[index + 1][w + 1]) * factor
|
||||
|
||||
w += 1
|
||||
|
||||
while w < weightArrayLen do
|
||||
w += 1
|
||||
tempFont = fontMatrixAscend[index+1][w+1]
|
||||
|
||||
if tempFont > 400 then
|
||||
returnArray[w + 1] = tempFont
|
||||
elseif contrast < 14.5 then
|
||||
returnArray[w + 1] = 999
|
||||
elseif contrast < 29.5 then
|
||||
returnArray[w + 1] = 777
|
||||
else
|
||||
--- interpolation of font size
|
||||
|
||||
if tempFont > 24 then
|
||||
returnArray[w + 1] = math.round(tempFont - (fontDeltaAscend[index + 1][w + 1] * scoreAdj))
|
||||
else
|
||||
returnArray[w + 1] = tempFont - ((2 * fontDeltaAscend[index + 1][w + 1] * scoreAdj // 1) * 0.5)
|
||||
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
return returnArray
|
||||
|
||||
end
|
||||
|
||||
return {
|
||||
|
||||
APCAcontrast = APCAcontrast,
|
||||
reverseAPCA = reverseAPCA,
|
||||
calcAPCA = calcAPCA,
|
||||
fontLookupAPCA = fontLookupAPCA,
|
||||
|
||||
sRGBtoY = sRGBtoY,
|
||||
displayP3toY = displayP3toY,
|
||||
adobeRGBtoY = adobeRGBtoY,
|
||||
alphaBlend = alphaBlend
|
||||
|
||||
}
|
||||
62
modules/Jabby/ui/libraries/cascade.luau
Normal file
62
modules/Jabby/ui/libraries/cascade.luau
Normal file
|
|
@ -0,0 +1,62 @@
|
|||
local vide = require(script.Parent.Parent.Parent.vide)
|
||||
|
||||
local action = vide.action
|
||||
local cleanup = vide.cleanup
|
||||
|
||||
--[[
|
||||
|
||||
Cascades are applied and passed through an instance.
|
||||
Use a context if you need to pass this through function scopes.
|
||||
|
||||
This is primarily useful for passing down theme information.
|
||||
|
||||
]]
|
||||
local function cascade<T>(default_value: T)
|
||||
|
||||
local self = {}
|
||||
local senders: {[Instance]: T} = {}
|
||||
|
||||
local function get_cascaded_value(from: Instance?): T
|
||||
while from ~= nil do
|
||||
local sender = senders[from]
|
||||
if sender ~= nil then
|
||||
return sender
|
||||
else
|
||||
from = from.Parent
|
||||
end
|
||||
end
|
||||
return default_value
|
||||
end
|
||||
|
||||
function self.send(value: T)
|
||||
return action(function(instance)
|
||||
cleanup(function()
|
||||
senders[instance] = nil
|
||||
end)
|
||||
|
||||
senders[instance] = value
|
||||
end)
|
||||
end
|
||||
|
||||
function self.receive(output_to: (T) -> any)
|
||||
return action(function(instance)
|
||||
local function recalculate()
|
||||
output_to(
|
||||
get_cascaded_value(instance.Parent)
|
||||
)
|
||||
end
|
||||
|
||||
recalculate()
|
||||
|
||||
cleanup(instance.AncestryChanged:Connect(recalculate))
|
||||
cleanup(function()
|
||||
output_to(default_value)
|
||||
end)
|
||||
end)
|
||||
end
|
||||
|
||||
return self
|
||||
|
||||
end
|
||||
|
||||
return cascade
|
||||
57
modules/Jabby/ui/libraries/context.luau
Normal file
57
modules/Jabby/ui/libraries/context.luau
Normal file
|
|
@ -0,0 +1,57 @@
|
|||
--[[
|
||||
|
||||
Implements a form of dependency injection to save the need from passing data
|
||||
as props through intermediate components.
|
||||
|
||||
]]
|
||||
|
||||
export type Context<T = nil> = {
|
||||
default_value: T,
|
||||
_values: {[thread]: T},
|
||||
|
||||
provide: <U>(Context<T>, callback: () -> U) -> (new: T) -> U,
|
||||
consume: (Context<T>) -> T
|
||||
}
|
||||
|
||||
type ContextNoDefault<T> = {
|
||||
_values: {[thread]: T},
|
||||
|
||||
provide: <U>(Context<T>, callback: () -> U) -> (new: T) -> U,
|
||||
consume: (Context<T>) -> T?
|
||||
}
|
||||
|
||||
local function provide<T, U>(context: Context<T>, callback: () -> U)
|
||||
return function(new: T): U
|
||||
local thread = coroutine.running()
|
||||
local old = context._values[thread]
|
||||
|
||||
context._values[thread] = new
|
||||
|
||||
local ok, value = pcall(callback)
|
||||
|
||||
context._values[thread] = old
|
||||
|
||||
if not ok then
|
||||
error(`provided callback errored with "{value}"`, 2)
|
||||
end
|
||||
|
||||
return value
|
||||
end :: (new: T) -> U
|
||||
end
|
||||
|
||||
local function consume<T>(context: Context<T>): T
|
||||
local thread = coroutine.running()
|
||||
return context._values[thread] or context.default_value
|
||||
end
|
||||
|
||||
local function create_context<T>(default_value: T?): Context<T>
|
||||
return {
|
||||
default_value = default_value,
|
||||
_values = {},
|
||||
|
||||
provide = provide :: any,
|
||||
consume = consume
|
||||
}
|
||||
end
|
||||
|
||||
return create_context :: (<T>(default_value: T) -> Context<T>) & (<T>() -> ContextNoDefault<T>)
|
||||
26
modules/Jabby/ui/libraries/delay.luau
Normal file
26
modules/Jabby/ui/libraries/delay.luau
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
local RunService = game:GetService("RunService")
|
||||
|
||||
local vide = require(script.Parent.Parent.Parent.vide)
|
||||
|
||||
local source = vide.source
|
||||
local effect = vide.effect
|
||||
local cleanup = vide.cleanup
|
||||
|
||||
return function<T>(delay: number, input: () -> T): () -> T
|
||||
local output = source(input())
|
||||
|
||||
effect(function()
|
||||
local v = input()
|
||||
local t = delay
|
||||
|
||||
cleanup(RunService.Heartbeat:Connect(function(dt)
|
||||
t -= dt
|
||||
if t > 0 then return end
|
||||
output(v)
|
||||
|
||||
end))
|
||||
|
||||
end)
|
||||
|
||||
return output
|
||||
end
|
||||
434
modules/Jabby/ui/libraries/oklab.luau
Normal file
434
modules/Jabby/ui/libraries/oklab.luau
Normal file
|
|
@ -0,0 +1,434 @@
|
|||
--!nolint
|
||||
--!strict
|
||||
-- Oklab C implementation provided by Björn Ottosson:
|
||||
-- https://bottosson.github.io/posts/gamutclipping/
|
||||
-- Luau port and Roblox/Lch extensions by Elttob:
|
||||
-- https://elttob.uk/
|
||||
-- Licensed under MIT
|
||||
|
||||
local TAU = 2 * math.pi
|
||||
|
||||
local function cbrt(x: number)
|
||||
return math.sign(x) * math.abs(x) ^ (1/3)
|
||||
end
|
||||
|
||||
local Oklab = {}
|
||||
|
||||
function Oklab.linear_srgb_to_oklab(
|
||||
c: Vector3
|
||||
): Vector3
|
||||
local l = 0.4122214708 * c.X + 0.5363325363 * c.Y + 0.0514459929 * c.Z
|
||||
local m = 0.2119034982 * c.X + 0.6806995451 * c.Y + 0.1073969566 * c.Z
|
||||
local s = 0.0883024619 * c.X + 0.2817188376 * c.Y + 0.6299787005 * c.Z
|
||||
|
||||
local l_ = cbrt(l)
|
||||
local m_ = cbrt(m)
|
||||
local s_ = cbrt(s)
|
||||
|
||||
return Vector3.new(
|
||||
0.2104542553 * l_ + 0.7936177850 * m_ - 0.0040720468 * s_,
|
||||
1.9779984951 * l_ - 2.4285922050 * m_ + 0.4505937099 * s_,
|
||||
0.0259040371 * l_ + 0.7827717662 * m_ - 0.8086757660 * s_
|
||||
)
|
||||
end
|
||||
|
||||
function Oklab.oklab_to_linear_srgb(
|
||||
c: Vector3
|
||||
): Vector3
|
||||
local l_ = c.X + 0.3963377774 * c.Y + 0.2158037573 * c.Z
|
||||
local m_ = c.X - 0.1055613458 * c.Y - 0.0638541728 * c.Z
|
||||
local s_ = c.X - 0.0894841775 * c.Y - 1.2914855480 * c.Z
|
||||
|
||||
local l = l_ * l_ * l_
|
||||
local m = m_ * m_ * m_
|
||||
local s = s_ * s_ * s_
|
||||
|
||||
return Vector3.new(
|
||||
4.0767416621 * l - 3.3077115913 * m + 0.2309699292 * s,
|
||||
-1.2684380046 * l + 2.6097574011 * m - 0.3413193965 * s,
|
||||
-0.0041960863 * l - 0.7034186147 * m + 1.7076147010 * s
|
||||
)
|
||||
end
|
||||
|
||||
-- Finds the maximum saturation possible for a given hue that fits in sRGB.
|
||||
-- Saturation here is defined as S = C/L
|
||||
-- a and b must be normalised so a^2 + b^2 == 1
|
||||
function Oklab.compute_max_saturation(
|
||||
a: number,
|
||||
b: number
|
||||
): number
|
||||
-- Max saturation will be when one of r, g or b goes below zero.
|
||||
|
||||
-- Select different coefficients depending on which component goes below zero first
|
||||
local k0, k1, k2, k3, k4, wl, wm, ws
|
||||
|
||||
if -1.88170328 * a - 0.80936493 * b > 1 then
|
||||
-- Red component
|
||||
k0, k1, k2, k3, k4 = 1.19086277, 1.76576728, 0.59662641, 0.75515197, 0.56771245
|
||||
wl, wm, ws = 4.0767416621, -3.3077115913, 0.2309699292
|
||||
elseif 1.81444104 * a - 1.19445276 * b > 1 then
|
||||
-- Green component
|
||||
k0, k1, k2, k3, k4 = 0.73956515, -0.45954404, 0.08285427, 0.12541070, 0.14503204
|
||||
wl, wm, ws = -1.2684380046, 2.6097574011, -0.3413193965
|
||||
else
|
||||
-- Blue component
|
||||
k0, k1, k2, k3, k4 = 1.35733652, -0.00915799, -1.15130210, -0.50559606, 0.00692167
|
||||
wl, wm, ws = -0.0041960863, -0.7034186147, 1.7076147010
|
||||
end
|
||||
|
||||
-- Approximate max saturation using a polynomial
|
||||
local S = k0 + k1 * a + k2 * b + k3 * a * a + k4 * a * b
|
||||
|
||||
-- Do one step Halley's method to get closer
|
||||
-- this gives an error less than 10e6, except for some blue hues where the dS/dh is close to infinite
|
||||
-- this should be sufficient for most applications, otherwise do two/three steps
|
||||
local k_l = 0.3963377774 * a + 0.2158037573 * b
|
||||
local k_m = -0.1055613458 * a - 0.0638541728 * b
|
||||
local k_s = -0.0894841775 * a - 1.2914855480 * b
|
||||
|
||||
do
|
||||
local l_ = 1 + S * k_l
|
||||
local m_ = 1 + S * k_m
|
||||
local s_ = 1 + S * k_s
|
||||
|
||||
local l = l_ * l_ * l_
|
||||
local m = m_ * m_ * m_
|
||||
local s = s_ * s_ * s_
|
||||
|
||||
local l_dS = 3 * k_l * l_ * l_
|
||||
local m_dS = 3 * k_m * m_ * m_
|
||||
local s_dS = 3 * k_s * s_ * s_
|
||||
|
||||
local l_dS2 = 6 * k_l * k_l * l_
|
||||
local m_dS2 = 6 * k_m * k_m * m_
|
||||
local s_dS2 = 6 * k_s * k_s * s_
|
||||
|
||||
local f = wl * l + wm * m + ws * s
|
||||
local f1 = wl * l_dS + wm * m_dS + ws * s_dS
|
||||
local f2 = wl * l_dS2 + wm * m_dS2 + ws * s_dS2
|
||||
|
||||
S = S - f * f1 / (f1*f1 - 0.5 * f * f2)
|
||||
end
|
||||
|
||||
return S
|
||||
end
|
||||
|
||||
-- finds L_cusp and C_cusp for a given hue
|
||||
-- a and b must be normalised so a^2 + b^2 == 1
|
||||
function Oklab.find_cusp(
|
||||
a: number,
|
||||
b: number
|
||||
): (number, number)
|
||||
-- First, find the maximum saturation (saturation S = C/L)
|
||||
local S_cusp = Oklab.compute_max_saturation(a, b)
|
||||
|
||||
-- Convert to linear sRGB to find the first point where at least one of r,g or b >= 1:
|
||||
local rgb_at_max = Oklab.oklab_to_linear_srgb(Vector3.new(1, S_cusp * a, S_cusp * b))
|
||||
local L_cusp = cbrt(1 / math.max(rgb_at_max.X, rgb_at_max.Y, rgb_at_max.Z))
|
||||
local C_cusp = L_cusp * S_cusp
|
||||
|
||||
return L_cusp, C_cusp
|
||||
end
|
||||
|
||||
-- Finds intersection of the line defined by
|
||||
-- L = L0 * (1 - t) + t * L1;
|
||||
-- C = t * C1;
|
||||
-- a and b must be normalized so a^2 + b^2 == 1
|
||||
function Oklab.find_gamut_intersection(
|
||||
a: number,
|
||||
b: number,
|
||||
L1: number,
|
||||
C1: number,
|
||||
L0: number
|
||||
): number
|
||||
-- Find the cusp of the gamut triangle
|
||||
local L_cusp, C_cusp = Oklab.find_cusp(a, b)
|
||||
|
||||
-- Find the intersection for upper and lower half seprately
|
||||
local t
|
||||
if ((L1 - L0) * C_cusp - (L_cusp - L0) * C1) <= 0 then
|
||||
-- Lower half
|
||||
t = C_cusp * L0 / (C1 * L_cusp + C_cusp * (L0 - L1))
|
||||
else
|
||||
-- Upper half
|
||||
-- First intersect with triangle
|
||||
t = C_cusp * (L0 - 1) / (C1 * (L_cusp - 1) + C_cusp * (L0 - L1))
|
||||
|
||||
-- Then one step Halley's method
|
||||
do
|
||||
local dL = L1 - L0
|
||||
local dC = C1
|
||||
|
||||
local k_l = 0.3963377774 * a + 0.2158037573 * b
|
||||
local k_m = -0.1055613458 * a - 0.0638541728 * b
|
||||
local k_s = -0.0894841775 * a - 1.2914855480 * b
|
||||
|
||||
local l_dt = dL + dC * k_l
|
||||
local m_dt = dL + dC * k_m
|
||||
local s_dt = dL + dC * k_s
|
||||
|
||||
-- If higher accuracy is required, 2 or 3 iterations of the following block can be used:
|
||||
do
|
||||
local L = L0 * (1 - t) + t * L1
|
||||
local C = t * C1
|
||||
|
||||
local l_ = L + C * k_l
|
||||
local m_ = L + C * k_m
|
||||
local s_ = L + C * k_s
|
||||
|
||||
local l = l_ * l_ * l_
|
||||
local m = m_ * m_ * m_
|
||||
local s = s_ * s_ * s_
|
||||
|
||||
local ldt = 3 * l_dt * l_ * l_
|
||||
local mdt = 3 * m_dt * m_ * m_
|
||||
local sdt = 3 * s_dt * s_ * s_
|
||||
|
||||
local ldt2 = 6 * l_dt * l_dt * l_
|
||||
local mdt2 = 6 * m_dt * m_dt * m_
|
||||
local sdt2 = 6 * s_dt * s_dt * s_
|
||||
|
||||
local r = 4.0767416621 * l - 3.3077115913 * m + 0.2309699292 * s - 1
|
||||
local r1 = 4.0767416621 * ldt - 3.3077115913 * mdt + 0.2309699292 * sdt
|
||||
local r2 = 4.0767416621 * ldt2 - 3.3077115913 * mdt2 + 0.2309699292 * sdt2
|
||||
|
||||
local u_r = r1 / (r1 * r1 - 0.5 * r * r2)
|
||||
local t_r = -r * u_r
|
||||
|
||||
local g = -1.2684380046 * l + 2.6097574011 * m - 0.3413193965 * s - 1
|
||||
local g1 = -1.2684380046 * ldt + 2.6097574011 * mdt - 0.3413193965 * sdt
|
||||
local g2 = -1.2684380046 * ldt2 + 2.6097574011 * mdt2 - 0.3413193965 * sdt2
|
||||
|
||||
local u_g = g1 / (g1 * g1 - 0.5 * g * g2)
|
||||
local t_g = -g * u_g
|
||||
|
||||
local b = -0.0041960863 * l - 0.7034186147 * m + 1.7076147010 * s - 1
|
||||
local b1 = -0.0041960863 * ldt - 0.7034186147 * mdt + 1.7076147010 * sdt
|
||||
local b2 = -0.0041960863 * ldt2 - 0.7034186147 * mdt2 + 1.7076147010 * sdt2
|
||||
|
||||
local u_b = b1 / (b1 * b1 - 0.5 * b * b2)
|
||||
local t_b = -b * u_b
|
||||
|
||||
t_r = if u_r >= 0 then t_r else math.huge
|
||||
t_g = if u_g >= 0 then t_g else math.huge
|
||||
t_b = if u_b >= 0 then t_b else math.huge
|
||||
|
||||
t += math.min(t_r, t_g, t_b)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
return t
|
||||
end
|
||||
|
||||
function Oklab.gamut_clip_preserve_chroma(
|
||||
rgb: Vector3
|
||||
): Vector3
|
||||
if rgb.X <= 1 and rgb.Y <= 1 and rgb.Z <= 1 and rgb.X >= 0 and rgb.Y >= 0 and rgb.Z >= 0 then
|
||||
return rgb
|
||||
end
|
||||
|
||||
local lab = Oklab.linear_srgb_to_oklab(rgb)
|
||||
|
||||
local L = lab.X
|
||||
local eps = 0.00001
|
||||
local C = math.max(eps, math.sqrt(lab.Y * lab.Y + lab.Z * lab.Z))
|
||||
local a_ = if C == 0 then 0 else lab.Y / C
|
||||
local b_ = if C == 0 then 0 else lab.Z / C
|
||||
local L0 = math.clamp(L, 0, 1)
|
||||
|
||||
local t = Oklab.find_gamut_intersection(a_, b_, L, C, L0)
|
||||
local L_clipped = L0 * (1 - t) + t * L
|
||||
local C_clipped = t * C
|
||||
|
||||
return Oklab.oklab_to_linear_srgb(Vector3.new(L_clipped, C_clipped * a_, C_clipped * b_))
|
||||
end
|
||||
|
||||
function Oklab.gamut_clip_project_to_0_5(
|
||||
rgb: Vector3
|
||||
): Vector3
|
||||
if rgb.X <= 1 and rgb.Y <= 1 and rgb.Z <= 1 and rgb.X >= 0 and rgb.Y >= 0 and rgb.Z >= 0 then
|
||||
return rgb
|
||||
end
|
||||
|
||||
local lab = Oklab.linear_srgb_to_oklab(rgb)
|
||||
|
||||
local L = lab.X
|
||||
local eps = 0.00001
|
||||
local C = math.max(eps, math.sqrt(lab.Y * lab.Y + lab.Z * lab.Z))
|
||||
local a_ = lab.Y / C
|
||||
local b_ = lab.Z / C
|
||||
|
||||
local L0 = 0.5
|
||||
|
||||
local t = Oklab.find_gamut_intersection(a_, b_, L, C, L0)
|
||||
local L_clipped = L0 * (1 - t) + t * L
|
||||
local C_clipped = t * C
|
||||
|
||||
return Oklab.oklab_to_linear_srgb(Vector3.new(L_clipped, C_clipped * a_, C_clipped * b_))
|
||||
end
|
||||
|
||||
function Oklab.gamut_clip_project_to_L_cusp(
|
||||
rgb: Vector3
|
||||
): Vector3
|
||||
if rgb.X <= 1 and rgb.Y <= 1 and rgb.Z <= 1 and rgb.X >= 0 and rgb.Y >= 0 and rgb.Z >= 0 then
|
||||
return rgb
|
||||
end
|
||||
|
||||
local lab = Oklab.linear_srgb_to_oklab(rgb)
|
||||
|
||||
local L = lab.X
|
||||
local eps = 0.00001
|
||||
local C = math.max(eps, math.sqrt(lab.Y * lab.Y + lab.Z * lab.Z))
|
||||
local a_ = lab.Y / C
|
||||
local b_ = lab.Z / C
|
||||
|
||||
-- The cusp is computed here and in find_gamut_intersection, an optimised solution would only compute it once.
|
||||
local L_cusp, C_cusp = Oklab.find_cusp(a_, b_)
|
||||
|
||||
local L0 = L_cusp
|
||||
|
||||
local t = Oklab.find_gamut_intersection(a_, b_, L, C, L0)
|
||||
|
||||
local L_clipped = L0 * (1 - t) + t * L
|
||||
local C_clipped = t * C
|
||||
|
||||
return Oklab.oklab_to_linear_srgb(Vector3.new(L_clipped, C_clipped * a_, C_clipped * b_))
|
||||
end
|
||||
|
||||
function Oklab.gamut_clip_adaptive_L0_0_5(
|
||||
rgb: Vector3,
|
||||
alpha: number?
|
||||
): Vector3
|
||||
if rgb.X <= 1 and rgb.Y <= 1 and rgb.Z <= 1 and rgb.X >= 0 and rgb.Y >= 0 and rgb.Z >= 0 then
|
||||
return rgb
|
||||
end
|
||||
local alpha = alpha or 0.05
|
||||
|
||||
local lab = Oklab.linear_srgb_to_oklab(rgb)
|
||||
|
||||
local L = lab.X
|
||||
local eps = 0.00001
|
||||
local C = math.max(eps, math.sqrt(lab.Y * lab.Y + lab.Z * lab.Z))
|
||||
local a_ = lab.Y / C
|
||||
local b_ = lab.Z / C
|
||||
|
||||
local Ld = L - 0.5
|
||||
local e1 = 0.5 + math.abs(Ld) + alpha * C
|
||||
local L0 = 0.5 * (1 + math.sign(Ld) * (e1 - math.sqrt(e1*e1 - 2 * math.abs(Ld))))
|
||||
|
||||
local t = Oklab.find_gamut_intersection(a_, b_, L, C, L0)
|
||||
local L_clipped = L0 * (1 - t) + t * L
|
||||
local C_clipped = t * C
|
||||
|
||||
return Oklab.oklab_to_linear_srgb(Vector3.new(L_clipped, C_clipped * a_, C_clipped * b_))
|
||||
end
|
||||
|
||||
function Oklab.gamut_clip_adaptive_L0_L_cusp(
|
||||
rgb: Vector3,
|
||||
alpha: number?
|
||||
): Vector3
|
||||
if rgb.X < 1 and rgb.Y < 1 and rgb.Z < 1 and rgb.X > 0 and rgb.Y > 0 and rgb.Z > 0 then
|
||||
return rgb
|
||||
end
|
||||
local alpha = alpha or 0.05
|
||||
|
||||
local lab = Oklab.linear_srgb_to_oklab(rgb)
|
||||
|
||||
local L = lab.X
|
||||
local eps = 0.00001
|
||||
local C = math.max(eps, math.sqrt(lab.Y * lab.Y + lab.Z * lab.Z))
|
||||
local a_ = lab.Y / C
|
||||
local b_ = lab.Z / C
|
||||
|
||||
-- The cusp is computed here and in find_gamut_intersection, an optimized solution would only compute it once.
|
||||
local L_cusp, C_cusp = Oklab.find_cusp(a_, b_)
|
||||
|
||||
local Ld = L - L_cusp
|
||||
local k = 2 * (if Ld > 0 then 1 - L_cusp else L_cusp)
|
||||
|
||||
local e1 = 0.5*k + math.abs(Ld) + alpha * C/k
|
||||
local L0 = L_cusp + 0.5 * (math.sign(Ld) * (e1 - math.sqrt(e1 * e1 - 2 * k * math.abs(Ld))))
|
||||
|
||||
local t = Oklab.find_gamut_intersection(a_, b_, L, C, L0)
|
||||
local L_clipped = L0 * (1 - t) + t * L
|
||||
local C_clipped = t * C
|
||||
|
||||
return Oklab.oklab_to_linear_srgb(Vector3.new(L_clipped, C_clipped * a_, C_clipped * b_))
|
||||
end
|
||||
|
||||
--[[
|
||||
ROBLOX EXTENSIONS
|
||||
]]
|
||||
|
||||
Oklab.default_gamut_clip = Oklab.gamut_clip_adaptive_L0_0_5
|
||||
|
||||
local function component_to_gamma(x: number): number
|
||||
if x >= 0.0031308 then
|
||||
return (1.055) * x^(1.0/2.4) - 0.055
|
||||
else
|
||||
return 12.92 * x
|
||||
end
|
||||
end
|
||||
|
||||
local function component_to_linear(x: number): number
|
||||
if x >= 0.04045 then
|
||||
return ((x + 0.055)/(1 + 0.055))^2.4
|
||||
else
|
||||
return x / 12.92
|
||||
end
|
||||
end
|
||||
|
||||
function Oklab.color3_to_linear_srgb(
|
||||
c: Color3
|
||||
): Vector3
|
||||
return Vector3.new(
|
||||
component_to_linear(c.R),
|
||||
component_to_linear(c.G),
|
||||
component_to_linear(c.B)
|
||||
)
|
||||
end
|
||||
|
||||
function Oklab.linear_srgb_to_color3(
|
||||
c: Vector3,
|
||||
use_default_gamut_clip: boolean?
|
||||
): Color3
|
||||
if use_default_gamut_clip == false then
|
||||
return Color3.new(
|
||||
component_to_gamma(c.X),
|
||||
component_to_gamma(c.Y),
|
||||
component_to_gamma(c.Z)
|
||||
)
|
||||
else
|
||||
local c = Oklab.default_gamut_clip(c)
|
||||
return Color3.new(
|
||||
math.clamp(component_to_gamma(c.X), 0, 1),
|
||||
math.clamp(component_to_gamma(c.Y), 0, 1),
|
||||
math.clamp(component_to_gamma(c.Z), 0, 1)
|
||||
)
|
||||
end
|
||||
|
||||
|
||||
end
|
||||
|
||||
--[[
|
||||
LCH EXTENSIONS
|
||||
]]
|
||||
|
||||
function Oklab.oklch_to_oklab(oklch: Vector3): Vector3
|
||||
return Vector3.new(
|
||||
oklch.X,
|
||||
oklch.Y * math.cos(oklch.Z * TAU),
|
||||
oklch.Y * math.sin(oklch.Z * TAU)
|
||||
)
|
||||
end
|
||||
|
||||
function Oklab.oklab_to_oklch(oklab: Vector3): Vector3
|
||||
return Vector3.new(
|
||||
oklab.X,
|
||||
math.sqrt(oklab.Y^2 + oklab.Z^2),
|
||||
(math.atan2(oklab.Z, oklab.Y) / TAU) % 1
|
||||
)
|
||||
end
|
||||
|
||||
return Oklab
|
||||
101
modules/Jabby/ui/libraries/store.luau
Normal file
101
modules/Jabby/ui/libraries/store.luau
Normal file
|
|
@ -0,0 +1,101 @@
|
|||
--------------------------------------------------------------------------------
|
||||
-- videx/store.luau
|
||||
--------------------------------------------------------------------------------
|
||||
|
||||
local vide = require(script.Parent.Parent.Parent.vide)
|
||||
local source = vide.source
|
||||
|
||||
local NULL = newproxy()
|
||||
|
||||
local Store = {}
|
||||
|
||||
--[=[
|
||||
Creates a new store object that receives some initial state and then returns
|
||||
a table with the same structure, but all keys of the given table will be reactive.
|
||||
|
||||
When accessed inside a reactive scope, the reactive scope will update whenever
|
||||
the key that is accessed is changed.
|
||||
|
||||
@param initial_state `T : {[string]: any}` The initial state the store will start in.
|
||||
@param mutations `() -> {[string]: (T, ...any) -> ...any}?` A list of functions that mutate the data.
|
||||
@return `T & U` A resulting table that
|
||||
]=]
|
||||
function Store.new<T, U>(
|
||||
initial_state: T & {},
|
||||
mutations: (T & U) -> U
|
||||
): T & U
|
||||
local sources = {}
|
||||
|
||||
for i, v in initial_state :: any do
|
||||
local src = source(v ~= NULL and v or nil)
|
||||
sources[i] = src
|
||||
end
|
||||
|
||||
local internal_proxy = {}
|
||||
|
||||
setmetatable(internal_proxy, {
|
||||
__index = function(_, index)
|
||||
return sources[index]()
|
||||
end,
|
||||
__newindex = function(_, index, value)
|
||||
sources[index](value)
|
||||
end
|
||||
})
|
||||
|
||||
local external_proxy = {}
|
||||
|
||||
setmetatable(external_proxy :: any, {
|
||||
__index = function(_, index)
|
||||
local src = sources[index]
|
||||
if src == nil then error(`invalid index {index}`, 2) end
|
||||
return src()
|
||||
end,
|
||||
|
||||
__newindex = function(_, index, value)
|
||||
sources[index](value)
|
||||
end
|
||||
})
|
||||
|
||||
for i, v in next, mutations(internal_proxy :: any) :: any do
|
||||
if rawget(external_proxy, i) then
|
||||
error(`duplicate field "{i}"`, 2)
|
||||
end
|
||||
rawset(external_proxy, i, v)
|
||||
end
|
||||
|
||||
return external_proxy :: T & U & {}
|
||||
end
|
||||
|
||||
|
||||
--[=[
|
||||
Creates a new store object that receives some initial state and then returns
|
||||
a table with the same structure, but all keys of the given table will be reactive.
|
||||
|
||||
When accessed inside a reactive scope, the reactive scope will update whenever
|
||||
the key that is accessed is changed.
|
||||
|
||||
@param initial_state `T : {[string]: any}` The initial state the store will start in.
|
||||
@param mutations `() -> {[string]: (T, ...any) -> ...any}?` A list of functions that mutate the data.
|
||||
@return `T & U` A resulting table that
|
||||
]=]
|
||||
function Store.new_deep<T, U>(
|
||||
initial_state: T & {},
|
||||
mutations: (T & U) -> U
|
||||
): T & U
|
||||
|
||||
local main = Store.new(initial_state, mutations)
|
||||
|
||||
for key, value in initial_state :: any do
|
||||
if type(value) == "table" then
|
||||
main[key] = Store.new_deep(value, mutations :: any)
|
||||
end
|
||||
end
|
||||
|
||||
return main
|
||||
|
||||
end
|
||||
|
||||
--- A special symbol used to indicate that a value should be nil within a Store.
|
||||
Store.null = NULL :: nil
|
||||
|
||||
return Store
|
||||
27
modules/Jabby/ui/util/anim.luau
Normal file
27
modules/Jabby/ui/util/anim.luau
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
local vide = require(script.Parent.Parent.Parent.vide)
|
||||
local reduced_motion = require(script.Parent.reduced_motion)
|
||||
|
||||
local source = vide.source
|
||||
local spring = vide.spring
|
||||
local untrack = vide.untrack
|
||||
|
||||
type Animation = "move" | ""
|
||||
|
||||
return function<T>(s: () -> T)
|
||||
local type = typeof(untrack(s))
|
||||
local is_movement = false
|
||||
local reduce = reduced_motion:consume()
|
||||
|
||||
if type == "UDim" or type == "UDim2" or type == "Vector2" then
|
||||
is_movement = true
|
||||
end
|
||||
|
||||
local spr = spring(s, 0.1)
|
||||
return function()
|
||||
if is_movement and reduce then
|
||||
return s()
|
||||
else
|
||||
return spr()
|
||||
end
|
||||
end
|
||||
end
|
||||
0
modules/Jabby/ui/util/constants.luau
Normal file
0
modules/Jabby/ui/util/constants.luau
Normal file
10
modules/Jabby/ui/util/consume.luau
Normal file
10
modules/Jabby/ui/util/consume.luau
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
--[[
|
||||
|
||||
Consumes a property from a table and removes it
|
||||
|
||||
]]
|
||||
|
||||
return function(t: {}, key: string)
|
||||
local v = t[key]; t[key] = nil
|
||||
return v
|
||||
end
|
||||
155
modules/Jabby/ui/util/contrast.luau
Normal file
155
modules/Jabby/ui/util/contrast.luau
Normal file
|
|
@ -0,0 +1,155 @@
|
|||
local vide = require(script.Parent.Parent.Parent.vide)
|
||||
local apcaw3 = require(script.Parent.Parent.libraries.apcaw3)
|
||||
local oklch = require(script.Parent.oklch)
|
||||
|
||||
local derive = vide.derive
|
||||
local read = vide.read
|
||||
|
||||
local function min_contrast(options: {
|
||||
size: can<number>,
|
||||
weight: can<number | Font>?,
|
||||
body: can<boolean>?
|
||||
})
|
||||
|
||||
local size = options.size
|
||||
local weight = options.weight or 400
|
||||
local needs_body = options.body
|
||||
local NO = math.huge
|
||||
|
||||
if not size and not weight then return 0 end
|
||||
|
||||
local MIN_CONTRAST = {
|
||||
[12] = {NO, NO, NO, NO, NO, NO, NO, NO, NO},
|
||||
[14] = {NO, NO, NO, 100, 100, 90, 75, NO, NO},
|
||||
[15] = {NO, NO, NO, 100, 90, 75, 70, NO, NO},
|
||||
[16] = {NO, NO, NO, 90, 75, 70, 60, 60, NO},
|
||||
[18] = {NO, NO, 100, 75, 70, 60, 55, 55, 55},
|
||||
[21] = {NO, NO, 90, 70, 60, 55, 50, 50, 50},
|
||||
[24] = {NO, NO, 75, 60, 55, 50, 45, 45, 55},
|
||||
[28] = {NO, 100, 70, 55, 50, 45, 43, 43, 43},
|
||||
[32] = {NO, 90, 65, 50, 45, 43, 40, 40, 40},
|
||||
[36] = {NO, 75, 60, 45, 43, 40, 38, 38, 38},
|
||||
[42] = {100, 70, 55, 43, 40, 38, 35, 35, 35},
|
||||
[48] = {90, 60, 50, 40, 38, 35, 33, 33, 33},
|
||||
[60] = {75, 55, 45, 37, 35, 33, 30 ,30, 30},
|
||||
[72] = {60, 50, 40, 35, 33, 30, 30, 30, 30},
|
||||
[96] = {50, 45, 35, 33, 30, 30, 30, 30, 30}
|
||||
}
|
||||
local MIN_CONTRAST_BODY = {
|
||||
[12] = {NO, NO, NO, NO, NO, NO, NO, NO, NO},
|
||||
[14] = {NO, NO, NO, 100, 100, 90, 75, NO, NO},
|
||||
[15] = {NO, NO, NO, 100, 90, 90, 85, NO, NO},
|
||||
[16] = {NO, NO, NO, 90, 75, 85, 75, NO, NO},
|
||||
[18] = {NO, NO, 100, 75, 85, 75, 70, NO, NO},
|
||||
[21] = {NO, NO, 90, 70, 75, 70, 65, NO, NO},
|
||||
[24] = {NO, NO, 75, 75, 70, 65, 60, NO, NO},
|
||||
[28] = {NO, NO, 85, 70, 65, 60, 58, NO, NO},
|
||||
[32] = {NO, NO, 80, 65, 60, 58, 55, NO, NO},
|
||||
[36] = {NO, NO, 75, 60, 58, 55, 52, NO, NO},
|
||||
[42] = {NO, NO, NO, NO, NO, NO, NO, NO, NO},
|
||||
[48] = {NO, NO, NO, NO, NO, NO, NO, NO, NO},
|
||||
[60] = {NO, NO, NO, NO, NO, NO, NO, NO, NO},
|
||||
[72] = {NO, NO, NO, NO, NO, NO, NO, NO, NO},
|
||||
[96] = {NO, NO, NO, NO, NO, NO, NO, NO, NO}
|
||||
}
|
||||
|
||||
return derive(function()
|
||||
local matrix_to_use =
|
||||
if read(needs_body) then MIN_CONTRAST_BODY
|
||||
else MIN_CONTRAST
|
||||
|
||||
local row_to_use = matrix_to_use[read(size)]
|
||||
if not row_to_use then return NO end
|
||||
local weight: Font | number = read(weight)
|
||||
local font_weight: number = 400
|
||||
|
||||
if type(weight) == "number" then
|
||||
font_weight = weight
|
||||
elseif typeof(weight) == "Font" then
|
||||
font_weight = weight.Weight.Value
|
||||
end
|
||||
|
||||
return row_to_use[font_weight // 100] or NO
|
||||
end)
|
||||
|
||||
end
|
||||
|
||||
--- collapses a state to a single value
|
||||
type recursive<T> = (() -> recursive<T>) | T
|
||||
type can<T> = (() -> T) | T
|
||||
local function unwrap<T>(source: recursive<T>): T
|
||||
|
||||
local value: recursive<T>
|
||||
|
||||
while type(source) == "function" do
|
||||
source = source()
|
||||
end
|
||||
|
||||
value = source
|
||||
|
||||
return value :: T
|
||||
end
|
||||
|
||||
local function get_appropriate_color(options: {
|
||||
background: recursive<Color3>?,
|
||||
foreground: recursive<{{number}}>,
|
||||
elevation: recursive<number>?,
|
||||
min_contrast: recursive<number>,
|
||||
}): () -> Color3
|
||||
|
||||
return function()
|
||||
local min_contrast = unwrap(options.min_contrast)
|
||||
local elevation = unwrap(options.elevation) or 0
|
||||
local bg = unwrap(options.background)
|
||||
|
||||
if bg == nil then
|
||||
local foreground = unwrap(options.foreground)[1]
|
||||
local l, c, h = unpack(foreground, 1, 3)
|
||||
local l_a, l_c, l_h = unpack(foreground, 4, 6)
|
||||
local color = oklch(
|
||||
l + elevation * (l_a or 0),
|
||||
c + elevation * (l_c or 0),
|
||||
h + elevation * (l_h or 0)
|
||||
)
|
||||
return color
|
||||
end
|
||||
|
||||
if min_contrast == math.huge then
|
||||
warn("min contrast is invalid")
|
||||
end
|
||||
|
||||
local max = -1
|
||||
local furthest_away = Color3.new()
|
||||
|
||||
for _, foreground: {number} in unwrap(options.foreground) do
|
||||
local l, c, h = unpack(foreground, 1, 3)
|
||||
local l_a, l_c, l_h = unpack(foreground, 4, 6)
|
||||
local color = oklch(
|
||||
l + elevation * (l_a or 0),
|
||||
c + elevation * (l_c or 0),
|
||||
h + elevation * (l_h or 0)
|
||||
)
|
||||
local contrast = math.abs(apcaw3.calcAPCA(color, bg, nil, 1, true))
|
||||
|
||||
if max < contrast then
|
||||
max = contrast
|
||||
furthest_away = color
|
||||
end
|
||||
|
||||
if contrast >= min_contrast then
|
||||
return color
|
||||
end
|
||||
end
|
||||
|
||||
-- warn(`unable to find a color, max contrast found is {max // 1} but needs at least {min_contrast}`)
|
||||
return furthest_away
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
return {
|
||||
|
||||
min_contrast = min_contrast,
|
||||
get_appropriate_color = get_appropriate_color
|
||||
|
||||
}
|
||||
28
modules/Jabby/ui/util/oklch.luau
Normal file
28
modules/Jabby/ui/util/oklch.luau
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
local oklab = require(script.Parent.Parent.libraries.oklab)
|
||||
|
||||
--[=[
|
||||
|
||||
Converts OkLCh into a Color3.
|
||||
|
||||
lightness is a value between 0-1, determining how "light" a color is.
|
||||
|
||||
chroma is a value between 0 to infinity, determining how colorful something is.
|
||||
current displays can only display a chroma up to around 0.34, and srgb can only
|
||||
go up to 0.245.
|
||||
|
||||
hue is a hue circle from 0-360
|
||||
|
||||
|
||||
]=]
|
||||
local function oklch(lightness: number, chroma: number, hue: number)
|
||||
return oklab.linear_srgb_to_color3(
|
||||
oklab.oklab_to_linear_srgb(
|
||||
oklab.oklch_to_oklab(
|
||||
Vector3.new(math.clamp(lightness, 0, 1), chroma, hue)
|
||||
)
|
||||
),
|
||||
true
|
||||
)
|
||||
end
|
||||
|
||||
return oklch
|
||||
3
modules/Jabby/ui/util/reduced_motion.luau
Normal file
3
modules/Jabby/ui/util/reduced_motion.luau
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
local context = require(script.Parent.Parent.libraries.context)
|
||||
|
||||
return context(false)
|
||||
253
modules/Jabby/ui/util/theme.luau
Normal file
253
modules/Jabby/ui/util/theme.luau
Normal file
|
|
@ -0,0 +1,253 @@
|
|||
--[[
|
||||
|
||||
simple theme engine for computing colors
|
||||
]]
|
||||
|
||||
local vide = require(script.Parent.Parent.Parent.vide)
|
||||
local apcaw3 = require(script.Parent.Parent.libraries.apcaw3)
|
||||
local contrast = require(script.Parent.contrast)
|
||||
local oklch_to_color3 = require(script.Parent.oklch)
|
||||
|
||||
local source = vide.source
|
||||
|
||||
local function oklch(t: {number})
|
||||
return table.freeze {
|
||||
l = t[1],
|
||||
c = t[2],
|
||||
h = t[3],
|
||||
color = oklch_to_color3(unpack(t))
|
||||
}
|
||||
end
|
||||
|
||||
local background = source(oklch {0.2012, 0.01, 0.6}) -- note: for light mode change this to 0.9012 from 0.3012
|
||||
local accent = source(oklch {0.52, 0.21, 0.71}) -- note: for light mode change this to 0.62 from 0.52
|
||||
|
||||
local BODY_SIZE = 18
|
||||
local HEADER_SIZE = 24
|
||||
local FONT = Font.fromName(
|
||||
"BuilderSans",
|
||||
Enum.FontWeight.Regular,
|
||||
Enum.FontStyle.Normal
|
||||
)
|
||||
local FONT_CODE = Font.new(
|
||||
"rbxassetid://16658246179",
|
||||
Enum.FontWeight.Regular,
|
||||
Enum.FontStyle.Normal
|
||||
)
|
||||
|
||||
--- determines when it should be decreasing instead of increasing
|
||||
local function should_flip(lightness: number)
|
||||
return lightness > 0.65
|
||||
end
|
||||
|
||||
local CONTRAST_LOW = 0.4
|
||||
local CONTRAST_HIGH = 0.7
|
||||
|
||||
type Depth = number
|
||||
type ColorPalette = {
|
||||
|
||||
bg: {[Depth]: Color3},
|
||||
fg_on_bg_high: {[Depth]: Color3},
|
||||
fg_on_bg_low: {[Depth]: Color3},
|
||||
fg_on_accent: {[Depth]: Color3},
|
||||
accent: Color3,
|
||||
|
||||
}
|
||||
|
||||
local function compute_bg(depth: Depth)
|
||||
local function get()
|
||||
local bg = background()
|
||||
-- print(bg.l + 0.01 * depth, depth, bg.c, bg.h)
|
||||
|
||||
return oklch {
|
||||
math.clamp(bg.l + 0.02 * depth, 0, 1),
|
||||
bg.c,
|
||||
bg.h
|
||||
}
|
||||
-- if should_flip(bg.l) then
|
||||
-- return oklch {
|
||||
-- math.clamp(bg.l - 0.02 * depth, 0, 1),
|
||||
-- bg.c,
|
||||
-- bg.h
|
||||
-- }
|
||||
-- else
|
||||
-- return oklch {
|
||||
-- math.clamp(bg.l + 0.02 * depth, 0, 1),
|
||||
-- bg.c,
|
||||
-- bg.h
|
||||
-- }
|
||||
-- end
|
||||
end
|
||||
|
||||
return function(raw: boolean)
|
||||
local c = if raw then get() else get().color
|
||||
return c
|
||||
end :: ((true) -> typeof(oklch {}) ) & (false?) -> Color3
|
||||
end
|
||||
|
||||
local function compute_acc(depth: Depth)
|
||||
local function get()
|
||||
local bg = accent()
|
||||
-- print(bg.l + 0.01 * depth, depth, bg.c, bg.h)
|
||||
return oklch {
|
||||
math.clamp(bg.l + 0.02 * depth, 0, 1),
|
||||
bg.c,
|
||||
bg.h
|
||||
}
|
||||
-- if should_flip(bg.l) then
|
||||
-- return oklch {
|
||||
-- math.clamp(bg.l - 0.02 * depth, 0, 1),
|
||||
-- bg.c,
|
||||
-- bg.h
|
||||
-- }
|
||||
-- else
|
||||
-- return oklch {
|
||||
-- math.clamp(bg.l + 0.02 * depth, 0, 1),
|
||||
-- bg.c,
|
||||
-- bg.h
|
||||
-- }
|
||||
-- end
|
||||
end
|
||||
|
||||
return function(raw: boolean)
|
||||
local c = if raw then get() else get().color
|
||||
return c
|
||||
end :: ((true) -> typeof(oklch {}) ) & (false?) -> Color3
|
||||
end
|
||||
|
||||
local function compute_fg_on_bg_high(depth: Depth)
|
||||
local get_bg = compute_bg(depth)
|
||||
return function()
|
||||
local bg = get_bg(true)
|
||||
|
||||
if should_flip(bg.l) then
|
||||
return oklch {
|
||||
bg.l - CONTRAST_HIGH,
|
||||
bg.c, bg.h
|
||||
}.color
|
||||
else
|
||||
return oklch {
|
||||
bg.l + CONTRAST_HIGH,
|
||||
bg.c, bg.h
|
||||
}.color
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local function compute_fg_on_bg_low(depth: Depth)
|
||||
local get_bg = compute_bg(depth)
|
||||
return function()
|
||||
local bg = get_bg(true)
|
||||
|
||||
if should_flip(bg.l) then
|
||||
return oklch {
|
||||
bg.l - CONTRAST_LOW,
|
||||
bg.c, bg.h
|
||||
}.color
|
||||
else
|
||||
return oklch {
|
||||
bg.l + CONTRAST_LOW,
|
||||
bg.c, bg.h
|
||||
}.color
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local function compute_fg_on_acc_low(depth: Depth)
|
||||
local get_bg = compute_acc(depth)
|
||||
return function()
|
||||
local bg = get_bg(true)
|
||||
|
||||
if should_flip(bg.l) then
|
||||
return oklch {
|
||||
bg.l - CONTRAST_LOW,
|
||||
bg.c, bg.h
|
||||
}.color
|
||||
else
|
||||
return oklch {
|
||||
bg.l + CONTRAST_LOW,
|
||||
bg.c, bg.h
|
||||
}.color
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local function compute_fg_on_acc_high(depth: Depth)
|
||||
local get_bg = compute_acc(depth)
|
||||
return function()
|
||||
local bg = get_bg(true)
|
||||
|
||||
if should_flip(bg.l) then
|
||||
return oklch {
|
||||
bg.l - CONTRAST_HIGH,
|
||||
bg.c, bg.h
|
||||
}.color
|
||||
else
|
||||
return oklch {
|
||||
bg.l + CONTRAST_HIGH,
|
||||
bg.c, bg.h
|
||||
}.color
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
type Theme = {
|
||||
|
||||
bg: {[Depth]: number},
|
||||
accent: Color3,
|
||||
|
||||
fg_on_bg_high: {[Depth]: Color3},
|
||||
fg_on_bg_low: {[Depth]: Color3},
|
||||
fg_on_accent_high: {[Depth]: Color3},
|
||||
fg_on_accent_low: {[Depth]: Color3},
|
||||
|
||||
body: number,
|
||||
header: number,
|
||||
font: Font,
|
||||
font_code: Font,
|
||||
|
||||
}
|
||||
|
||||
local function compute_theme()
|
||||
|
||||
local MAX_DEPTH = 20
|
||||
local MIN_DEPTH = -20
|
||||
|
||||
local theme = {
|
||||
bg = {},
|
||||
acc = {},
|
||||
|
||||
fg_on_bg_high = {},
|
||||
fg_on_bg_low = {},
|
||||
fg_on_acc_high = {},
|
||||
fg_on_acc_low = {},
|
||||
|
||||
body = BODY_SIZE,
|
||||
header = HEADER_SIZE,
|
||||
font = FONT,
|
||||
code = FONT_CODE
|
||||
}
|
||||
|
||||
for i = MIN_DEPTH, MAX_DEPTH do
|
||||
theme.bg[i] = compute_bg(i)
|
||||
theme.acc[i] = compute_acc(i)
|
||||
theme.fg_on_acc_high[i] = compute_fg_on_acc_high(i)
|
||||
theme.fg_on_acc_low[i] = compute_fg_on_acc_low(i)
|
||||
theme.fg_on_bg_high[i] = compute_fg_on_bg_high(i)
|
||||
theme.fg_on_bg_low[i] = compute_fg_on_bg_low(i)
|
||||
end
|
||||
|
||||
return theme
|
||||
|
||||
end
|
||||
|
||||
return setmetatable({
|
||||
|
||||
settings = {
|
||||
background = background,
|
||||
accent = accent
|
||||
},
|
||||
|
||||
}, {
|
||||
__index = compute_theme()
|
||||
})
|
||||
25
modules/Jabby/vide/action.luau
Normal file
25
modules/Jabby/vide/action.luau
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
type Action = {
|
||||
priority: number,
|
||||
callback: (Instance) -> ()
|
||||
}
|
||||
|
||||
local ActionMT = table.freeze {}
|
||||
|
||||
local function is_action(v: any)
|
||||
return getmetatable(v) == ActionMT
|
||||
end
|
||||
|
||||
local function action(callback: (Instance) -> (), priority: number?): Action
|
||||
local a = {
|
||||
priority = priority or 1,
|
||||
callback = callback
|
||||
}
|
||||
|
||||
setmetatable(a :: any, ActionMT)
|
||||
|
||||
return table.freeze(a)
|
||||
end
|
||||
|
||||
return function()
|
||||
return action, is_action
|
||||
end
|
||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue