From 54da7d9b701bbd280406ab0f816647ce0a31f0af Mon Sep 17 00:00:00 2001 From: znotfireman Date: Tue, 4 Feb 2025 21:43:47 +0700 Subject: [PATCH 1/6] Tag --- aftman.toml | 3 ++- src/Instances/Tag.luau | 59 ++++++++++++++++++++++++++++++++++++++++++ src/Types.luau | 1 + src/init.luau | 7 ++--- 4 files changed, 66 insertions(+), 4 deletions(-) create mode 100644 src/Instances/Tag.luau diff --git a/aftman.toml b/aftman.toml index bd0ab91bc..6d365c63b 100644 --- a/aftman.toml +++ b/aftman.toml @@ -4,4 +4,5 @@ # To add a new tool, add an entry to this table. [tools] rojo = "rojo-rbx/rojo@7.4.1" -selene = "Kampfkarren/selene@0.26.1" \ No newline at end of file +selene = "Kampfkarren/selene@0.26.1" +luau-lsp = "JohnnyMorganz/luau-lsp@1.38.1" diff --git a/src/Instances/Tag.luau b/src/Instances/Tag.luau new file mode 100644 index 000000000..1fc4b29ca --- /dev/null +++ b/src/Instances/Tag.luau @@ -0,0 +1,59 @@ +--!strict +--!nolint LocalUnused +--!nolint LocalShadow +local task = nil -- Disable usage of Roblox's task scheduler + +--[[ + A special key for property tables, which allows users to apply custom + attributes to instances +]] + +local Package = script.Parent.Parent +local Types = require(Package.Types) +-- Memory +local checkLifetime = require(Package.Memory.checkLifetime) +-- Graph +local Observer = require(Package.Graph.Observer) +-- State +local castToState = require(Package.State.castToState) +local peek = require(Package.State.peek) + +local keyCache: { [string]: Types.SpecialKey } = {} + +-- TODO: should this accept tagName: UsedAs? +local function Tag(tagName: string): Types.SpecialKey + local key = keyCache[tagName] + if key == nil then + key = { + type = "SpecialKey", + kind = "Tag", + stage = "self", + apply = function(self: Types.SpecialKey, scope: Types.Scope, value: unknown, applyTo: Instance) + if castToState(value) then + local value = value :: Types.StateObject + checkLifetime.bOutlivesA( + scope, + applyTo, + value.scope, + value.oldestTask, + checkLifetime.formatters.boundAttribute, + tagName + ) + Observer(scope, value :: any):onBind(function() + if peek(value) and not applyTo:HasTag(tagName) then + applyTo:AddTag(tagName) + elseif applyTo:HasTag(tagName) then + applyTo:RemoveTag(tagName) + end + end) + else + applyTo:AddTag(tagName) + end + end, + } + keyCache[tagName] = key + end + return key +end + +return Tag diff --git a/src/Types.luau b/src/Types.luau index ddecd4092..4377acd0b 100644 --- a/src/Types.luau +++ b/src/Types.luau @@ -276,6 +276,7 @@ export type Fusion = { Attribute: (attributeName: string) -> SpecialKey, AttributeChange: (attributeName: string) -> SpecialKey, AttributeOut: (attributeName: string) -> SpecialKey, + Tag: (tagName: string) -> SpecialKey, } export type ExternalProvider = { diff --git a/src/init.luau b/src/init.luau index c3d87cf97..1ce6e3566 100644 --- a/src/init.luau +++ b/src/init.luau @@ -38,9 +38,9 @@ do External.setExternalProvider(RobloxExternal) end -local Fusion: Fusion = table.freeze { +local Fusion: Fusion = table.freeze({ -- General - version = {major = 0, minor = 4, isRelease = false}, + version = { major = 0, minor = 4, isRelease = false }, Contextual = require(script.Utility.Contextual), Safe = require(script.Utility.Safe), @@ -73,10 +73,11 @@ local Fusion: Fusion = table.freeze { OnChange = require(script.Instances.OnChange), OnEvent = require(script.Instances.OnEvent), Out = require(script.Instances.Out), + Tag = require(script.Instances.Tag), -- Animation Tween = require(script.Animation.Tween), Spring = require(script.Animation.Spring), -} +}) return Fusion From 35cffc3a489e3638184981eb51e7f2d93152fc9f Mon Sep 17 00:00:00 2001 From: znotfireman Date: Tue, 4 Feb 2025 21:47:53 +0700 Subject: [PATCH 2/6] update file comment --- src/Instances/Tag.luau | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Instances/Tag.luau b/src/Instances/Tag.luau index 1fc4b29ca..f3e9f03f6 100644 --- a/src/Instances/Tag.luau +++ b/src/Instances/Tag.luau @@ -5,7 +5,7 @@ local task = nil -- Disable usage of Roblox's task scheduler --[[ A special key for property tables, which allows users to apply custom - attributes to instances + CollectionService tags to instances ]] local Package = script.Parent.Parent From 938da361f016cb56ddbda030ae9a60e424cfa88a Mon Sep 17 00:00:00 2001 From: znotfireman Date: Tue, 4 Feb 2025 21:51:04 +0700 Subject: [PATCH 3/6] checkLifetime.formatters.boundTag --- src/Instances/Tag.luau | 2 +- src/Memory/checkLifetime.luau | 10 ++++++++++ 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/src/Instances/Tag.luau b/src/Instances/Tag.luau index f3e9f03f6..51041cbe7 100644 --- a/src/Instances/Tag.luau +++ b/src/Instances/Tag.luau @@ -36,7 +36,7 @@ local function Tag(tagName: string): Types.SpecialKey applyTo, value.scope, value.oldestTask, - checkLifetime.formatters.boundAttribute, + checkLifetime.formatters.boundTag, tagName ) Observer(scope, value :: any):onBind(function() diff --git a/src/Memory/checkLifetime.luau b/src/Memory/checkLifetime.luau index 3801a9d70..0e2fd0b2f 100644 --- a/src/Memory/checkLifetime.luau +++ b/src/Memory/checkLifetime.luau @@ -47,6 +47,16 @@ function checkLifetime.formatters.boundAttribute( return `The {boundName} (bound to the {attribute} attribute)`, `the {selfName} instance` end +function checkLifetime.formatters.boundTag( + instance: Instance, + bound: unknown, + tag: string +): (string, string) + local selfName = instance.Name + local boundName = nameOf(bound, "value") + return `The {boundName} (bound to the {tag} CollectionService tag)`, `the {selfName} instance` +end + function checkLifetime.formatters.propertyOutputsTo( instance: Instance, bound: unknown, From b4a27f3f9c4435458031b67d8c0885183fbb1561 Mon Sep 17 00:00:00 2001 From: znotfireman Date: Wed, 5 Feb 2025 08:07:28 +0700 Subject: [PATCH 4/6] minor fixes --- src/Instances/Tag.luau | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/Instances/Tag.luau b/src/Instances/Tag.luau index 51041cbe7..6d29f0ae8 100644 --- a/src/Instances/Tag.luau +++ b/src/Instances/Tag.luau @@ -40,14 +40,16 @@ local function Tag(tagName: string): Types.SpecialKey tagName ) Observer(scope, value :: any):onBind(function() - if peek(value) and not applyTo:HasTag(tagName) then + if peek(value) then applyTo:AddTag(tagName) elseif applyTo:HasTag(tagName) then applyTo:RemoveTag(tagName) end end) else - applyTo:AddTag(tagName) + if value then + applyTo:AddTag(tagName) + end end end, } From 51f98eacb9ab5583ba985c3630014bb99e36ce93 Mon Sep 17 00:00:00 2001 From: znotfireman Date: Fri, 7 Feb 2025 22:53:25 +0700 Subject: [PATCH 5/6] check for true not truthiness --- src/Instances/Tag.luau | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Instances/Tag.luau b/src/Instances/Tag.luau index 6d29f0ae8..e4c555f2c 100644 --- a/src/Instances/Tag.luau +++ b/src/Instances/Tag.luau @@ -40,14 +40,14 @@ local function Tag(tagName: string): Types.SpecialKey tagName ) Observer(scope, value :: any):onBind(function() - if peek(value) then + if peek(value) == true then applyTo:AddTag(tagName) elseif applyTo:HasTag(tagName) then applyTo:RemoveTag(tagName) end end) else - if value then + if value == true then applyTo:AddTag(tagName) end end From cd6e90356993ae86059b61eca8bff2e00645506e Mon Sep 17 00:00:00 2001 From: Dog <104234930+dgxo@users.noreply.github.com> Date: Sun, 7 Dec 2025 13:33:59 +0000 Subject: [PATCH 6/6] Add Tag tests --- test/Spec/Instances/Tag.spec.luau | 51 +++++++++++++++++++++++++++++++ 1 file changed, 51 insertions(+) create mode 100644 test/Spec/Instances/Tag.spec.luau diff --git a/test/Spec/Instances/Tag.spec.luau b/test/Spec/Instances/Tag.spec.luau new file mode 100644 index 000000000..a511ad1fa --- /dev/null +++ b/test/Spec/Instances/Tag.spec.luau @@ -0,0 +1,51 @@ +--!strict +--!nolint LocalUnused +local task = nil -- Disable usage of Roblox's task scheduler + +local ReplicatedStorage = game:GetService("ReplicatedStorage") +local Fusion = ReplicatedStorage.Fusion + +local New = require(Fusion.Instances.New) +local Tag = require(Fusion.Instances.Tag) +local Value = require(Fusion.State.Value) +local doCleanup = require(Fusion.Memory.doCleanup) + +return function() + local it = getfenv().it + + it("adds tags (constant)", function() + local expect = getfenv().expect + + local scope = {} + local child = New(scope, "Folder") { + [Tag "Foo"] = true + } + expect(child:HasTag("Foo")).to.equal(true) + doCleanup(scope) + end) + + it("adds tags (state)", function() + local expect = getfenv().expect + + local scope = {} + local addTag = Value(scope, true) + local child = New(scope, "Folder") { + [Tag "Foo"] = addTag + } + expect(child:HasTag("Foo")).to.equal(true) + end) + + it("adds/removes tag when state objects are updated", function() + local expect = getfenv().expect + + local scope = {} + local tagExists = Value(scope, true) + local child = New(scope, "Folder") { + [Tag "Foo"] = tagExists + } + expect(child:HasTag("Foo")).to.equal(true) + tagExists:set(false) + expect(child:HasTag("Foo")).to.equal(false) + doCleanup(scope) + end) +end