#Vision
What Scope:Capture returns. It holds a staged tree and one CapFunc per
declared value.
export type Vision = {
Mount: (self: Vision) -> (),
Open: (self: Vision) -> Instance,
Cleanup: (self: Vision) -> (),
Clone: (self: Vision) -> Vision,
} & { [string]: CapFunc<any> }
#Mount
Interface:Mount()
Creates the tree. In order:
- every instance is created, with properties, attributes, tags and connections applied
- children are parented, resolving any
mount("Name")targets - every value fires once, with the value it holds
- the root is parented to its mount target
readycallbacks run, deepest first
Calling Mount on a Vision that is already mounted does nothing, with one
exception: a Vision mounted through inject that found
no host retries its placement, so mounting again once the host is up parents
it. Mounting a Vision that was cleaned up rebuilds it - see Cleanup.
Mount is also what registers the Vision with its scope. Before the first mount,
and after every Cleanup, the scope holds no reference to it at all.
#MountAsync
Interface:MountAsync(Budget: number?)
Builds the tree across frames instead of in one go, yielding whenever the
frame budget is spent. Budget is seconds per frame, defaulting to 0.002.
It yields, so call it from a thread.
task.spawn(function()
Interface:MountAsync()
end)
Nothing appears until the whole tree is ready. The root is parented last,
exactly as in Mount, so a partially built tree is never on screen. You get
the hitch spread out, not a half drawn interface.
Use it when a tree is big enough to drop a frame. A hundred rows costs a few milliseconds and is fine synchronously; a thousand is not.
Calling it on a Vision that is already mounted does nothing, and Cleanup
during the build aborts it cleanly, destroying anything already created.
#Open
local Object = Interface:Open()
Mounts if needed and returns the root instance. Idempotent - calling it twice returns the same instance and does not rebuild anything.
Warning
Open mounts synchronously. During a MountAsync that has not finished, the
root instance does not exist yet and Open returns nil.
#Cleanup
Interface:Cleanup()
Runs cleanup callbacks deepest first, cancels any links driving this Vision
or its instances, disconnects every connection, and destroys the root.
Cleaning up twice does nothing.
Cleanup does not destroy the Vision. It puts it to sleep. The declaration and every value survive, and mounting again rebuilds the tree as it was:
Interface.Count(7)
Interface:Mount()
Interface:Cleanup()
print(Interface.Count()) --> 7
Interface:Mount() -- rebuilt, callbacks fire once with 7
A sleeping Vision behaves exactly like one that has never been mounted - writes store and nothing runs, so it can keep receiving updates while its instances are gone, and comes back correct rather than stale.
The scope forgets a sleeping Vision, so Scope:Release will not touch it.
Mounting adopts it again.
#Declared cleanups repeat
A cleanup declared in the table runs on every teardown. A cleanup
registered from inside ready belongs to that mount only, and is registered
again the next time ready runs.
#Clone
Interface:Clone() -> Vision
Copies a Vision without building anything. The copy is captured into the same scope from the same declaration, so it mounts to the same place, and starts with the values the original holds right now.
local Row = Card:Clone()
Row.Title("Second")
Row:Mount()
The two share nothing after that - separate instances, separate values.
Scope:Release cleans up clones along with everything else.
A Vision that takes its instance with
fromInstance cannot be cloned - there is only
one of that instance and two Visions cannot both own driving it. Clone says
so and points you at fromClone, which copies the
template and gives every clone its own.
#CapFunc
Every name passed to event becomes a field on the Vision:
export type CapFunc<v> = (i: v?) -> v
Call it with nothing to read, or with a value to write. A write returns the new value, so it reads naturally in an expression.
Interface.Count() -- read
Interface.Count(5) -- write, returns 5
#Writes before mount
While staged, a write only stores. Nothing runs. At Mount the callback
fires once with whatever the value holds by then.
Interface.Count(1)
Interface.Count(2)
Interface.Count(3)
-- no callbacks yet
Interface:Mount()
-- callback runs once, with 3
#Writes after mount
Every bound callback runs immediately, each with its own instance.
#Unchanged writes are skipped
Writing the value it already holds does nothing - no callbacks, no property assignments.
Interface.Mode("active") -- callbacks run
Interface.Mode("active") -- skipped entirely
Tables are compared by identity, and a table that is not frozen is always treated as changed, so mutating a table in place and writing it back still fires.
Warning
This makes writes idempotent. If you were relying on writing the same value to re-trigger a callback, it will no longer fire.
#Notes
An event name that collides with Mount, Open or Cleanup raises at
capture time rather than silently shadowing the method.