@trioforge/anvil 0.0.0-stage → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,33 @@
1
+ export interface Selection<T extends defined> {
2
+ readonly marked: readonly T[];
3
+ readonly remembered?: T;
4
+ readonly paused: boolean;
5
+ }
6
+ export type SelectionEvent<T> = {
7
+ readonly kind: "Shown";
8
+ } | {
9
+ readonly kind: "Marked";
10
+ readonly object: T;
11
+ readonly replaces?: T;
12
+ } | {
13
+ readonly kind: "Unmarked";
14
+ readonly object: T;
15
+ } | {
16
+ readonly kind: "Selected";
17
+ readonly object: T;
18
+ } | {
19
+ readonly kind: "Lost";
20
+ readonly removed: boolean;
21
+ };
22
+ export interface PolicyContext<T extends defined> {
23
+ readonly current?: T;
24
+ readonly alive: (object: T) => boolean;
25
+ readonly canSelect: boolean;
26
+ readonly close?: T;
27
+ }
28
+ export interface Decision<T extends defined> {
29
+ readonly selection: Selection<T>;
30
+ readonly target?: T | "Root";
31
+ }
32
+ export declare function emptySelection<T extends defined>(): Selection<T>;
33
+ export declare function decide<T extends defined>(selection: Selection<T>, event: SelectionEvent<T>, context: PolicyContext<T>): Decision<T>;
@@ -0,0 +1,112 @@
1
+ -- Compiled with roblox-ts v3.0.0
2
+ local function emptySelection()
3
+ return {
4
+ marked = {},
5
+ paused = false,
6
+ }
7
+ end
8
+ local function fallback(selection, context)
9
+ for i = #selection.marked - 1, 0, -1 do
10
+ local object = selection.marked[i + 1]
11
+ if context.alive(object) then
12
+ return object
13
+ end
14
+ end
15
+ return if context.close ~= nil and context.alive(context.close) then context.close else "Root"
16
+ end
17
+ local function decide(selection, event, context)
18
+ local _exp = event.kind
19
+ repeat
20
+ if _exp == "Shown" then
21
+ return {
22
+ selection = selection,
23
+ target = if context.canSelect and not selection.paused then if selection.remembered ~= nil and context.alive(selection.remembered) then selection.remembered else fallback(selection, context) else nil,
24
+ }
25
+ end
26
+ if _exp == "Marked" then
27
+ local _marked = selection.marked
28
+ local _object = event.object
29
+ if table.find(_marked, _object) ~= nil then
30
+ return {
31
+ selection = selection,
32
+ }
33
+ end
34
+ local _object_1 = table.clone(selection)
35
+ setmetatable(_object_1, nil)
36
+ local _left = "marked"
37
+ local _array = {}
38
+ local _length = #_array
39
+ local _array_1 = selection.marked
40
+ local _Length = #_array_1
41
+ table.move(_array_1, 1, _Length, _length + 1, _array)
42
+ _length += _Length
43
+ _array[_length + 1] = event.object
44
+ _object_1[_left] = _array
45
+ local changed = _object_1
46
+ return {
47
+ selection = changed,
48
+ target = if context.canSelect and not selection.paused and (context.current == nil or context.current == event.replaces) then event.object else nil,
49
+ }
50
+ end
51
+ if _exp == "Unmarked" then
52
+ local _object = {}
53
+ local _left = "selection"
54
+ local _object_1 = table.clone(selection)
55
+ setmetatable(_object_1, nil)
56
+ local _left_1 = "marked"
57
+ local _exp_1 = selection.marked
58
+ -- ▼ ReadonlyArray.filter ▼
59
+ local _newValue = {}
60
+ local _callback = function(object)
61
+ return object ~= event.object
62
+ end
63
+ local _length = 0
64
+ for _k, _v in _exp_1 do
65
+ if _callback(_v, _k - 1, _exp_1) == true then
66
+ _length += 1
67
+ _newValue[_length] = _v
68
+ end
69
+ end
70
+ -- ▲ ReadonlyArray.filter ▲
71
+ _object_1[_left_1] = _newValue
72
+ _object[_left] = _object_1
73
+ return _object
74
+ end
75
+ if _exp == "Selected" then
76
+ local _object = {}
77
+ local _left = "selection"
78
+ local _object_1 = table.clone(selection)
79
+ setmetatable(_object_1, nil)
80
+ _object_1.remembered = event.object
81
+ _object_1.paused = false
82
+ _object[_left] = _object_1
83
+ return _object
84
+ end
85
+ if _exp == "Lost" then
86
+ local _result
87
+ if event.removed then
88
+ local _object = {}
89
+ local _left = "selection"
90
+ local _object_1 = table.clone(selection)
91
+ setmetatable(_object_1, nil)
92
+ _object_1.remembered = nil
93
+ _object[_left] = _object_1
94
+ _object.target = if context.canSelect and not selection.paused then fallback(selection, context) else nil
95
+ _result = _object
96
+ else
97
+ local _object = {}
98
+ local _left = "selection"
99
+ local _object_1 = table.clone(selection)
100
+ setmetatable(_object_1, nil)
101
+ _object_1.paused = true
102
+ _object[_left] = _object_1
103
+ _result = _object
104
+ end
105
+ return _result
106
+ end
107
+ until true
108
+ end
109
+ return {
110
+ emptySelection = emptySelection,
111
+ decide = decide,
112
+ }
@@ -0,0 +1,22 @@
1
+ import type { Value } from "./props";
2
+ export type PressTiming = {
3
+ hold?: Value<number | undefined>;
4
+ repeating?: never;
5
+ } | {
6
+ repeating: true;
7
+ hold?: never;
8
+ };
9
+ export type PressOptions = PressTiming & {
10
+ onPress: () => boolean | void;
11
+ onPressedChange?: (pressed: boolean) => void;
12
+ onHighlightChange?: (highlighted: boolean) => void;
13
+ disabled?: Value<boolean>;
14
+ selectFirst?: Value<boolean>;
15
+ silent?: boolean;
16
+ };
17
+ export declare function usePress(options: PressOptions): {
18
+ highlighted: () => boolean;
19
+ pressed: () => boolean;
20
+ progress: () => number;
21
+ action: (instance: GuiButton) => void;
22
+ };
@@ -0,0 +1,228 @@
1
+ -- Compiled with roblox-ts v3.0.0
2
+ local TS = _G[script]
3
+ local UserInputService = TS.import(script, TS.getModule(script, "@rbxts", "services")).UserInputService
4
+ local _vide = TS.import(script, TS.getModule(script, "@rbxts", "vide").src)
5
+ local cleanup = _vide.cleanup
6
+ local derive = _vide.derive
7
+ local effect = _vide.effect
8
+ local read = _vide.read
9
+ local source = _vide.source
10
+ local untrack = _vide.untrack
11
+ local useAtom = TS.import(script, TS.getModule(script, "@rbxts", "vide-charm").src).useAtom
12
+ local heartbeat = TS.import(script, script.Parent, "frame-loop").heartbeat
13
+ local adapter = TS.import(script, script.Parent, "adapter").adapter
14
+ local _runtime = TS.import(script, script.Parent, "runtime")
15
+ local menuOpen = _runtime.menuOpen
16
+ local textEditing = _runtime.textEditing
17
+ local useSelection = _runtime.useSelection
18
+ local _press_state = TS.import(script, script.Parent, "press-state")
19
+ local idlePress = _press_state.idlePress
20
+ local transition = _press_state.transition
21
+ local function usePress(options)
22
+ local host = adapter()
23
+ local device = useAtom(host.device)
24
+ local menu = useAtom(menuOpen)
25
+ local editing = useAtom(textEditing)
26
+ local hovered = source(false)
27
+ local state = source(idlePress())
28
+ local timedPress = false
29
+ local mode = function()
30
+ local hold = read(options.hold)
31
+ if hold ~= nil then
32
+ local _arg0 = hold > 0 and hold < math.huge
33
+ assert(_arg0, "Hold duration must be finite and positive")
34
+ return {
35
+ kind = "Hold",
36
+ seconds = hold,
37
+ }
38
+ end
39
+ return if options.repeating then {
40
+ kind = "Repeat",
41
+ } else {
42
+ kind = "Activate",
43
+ }
44
+ end
45
+ local selection
46
+ local fire = function()
47
+ return untrack(function()
48
+ if not selection.allowed() or menu() then
49
+ return nil
50
+ end
51
+ local _condition = options.disabled
52
+ if _condition == nil then
53
+ _condition = false
54
+ end
55
+ local _condition_1 = read(_condition)
56
+ if not _condition_1 then
57
+ _condition_1 = options.onPress() == false
58
+ end
59
+ local refused = _condition_1
60
+ if not options.silent then
61
+ host.feedback(if refused then "Refused" else "Press")
62
+ end
63
+ end)
64
+ end
65
+ local update = function(event)
66
+ return untrack(function()
67
+ local before = state()
68
+ local result = transition(before, mode(), event)
69
+ state(result.state)
70
+ if (before.source == nil) ~= (result.state.source == nil) then
71
+ local _result = options.onPressedChange
72
+ if _result ~= nil then
73
+ _result(result.state.source ~= nil)
74
+ end
75
+ end
76
+ if result.fire then
77
+ fire()
78
+ end
79
+ end)
80
+ end
81
+ local begin = function(input)
82
+ return untrack(function()
83
+ if not selection.allowed() or menu() or editing() then
84
+ return nil
85
+ end
86
+ if state().source == nil then
87
+ timedPress = mode().kind ~= "Activate"
88
+ end
89
+ local _condition = options.disabled
90
+ if _condition == nil then
91
+ _condition = false
92
+ end
93
+ if read(_condition) then
94
+ if not options.silent and mode().kind ~= "Activate" then
95
+ host.feedback("Refused")
96
+ end
97
+ return nil
98
+ end
99
+ update({
100
+ kind = "Begin",
101
+ source = input,
102
+ })
103
+ end)
104
+ end
105
+ selection = useSelection({
106
+ selectFirst = options.selectFirst,
107
+ buttonADown = if options.hold ~= nil or options.repeating then begin else nil,
108
+ buttonAUp = function(input)
109
+ return update({
110
+ kind = "End",
111
+ source = input,
112
+ })
113
+ end,
114
+ })
115
+ local highlighted = derive(function()
116
+ return device() ~= "Touch" and (hovered() or selection.selected())
117
+ end)
118
+ local pressed = derive(function()
119
+ return state().source ~= nil
120
+ end)
121
+ local previouslySelected = false
122
+ effect(function()
123
+ local selected = selection.selected()
124
+ local _condition = not selection.allowed()
125
+ if not _condition then
126
+ local _condition_1 = options.disabled
127
+ if _condition_1 == nil then
128
+ _condition_1 = false
129
+ end
130
+ _condition = read(_condition_1)
131
+ if not _condition then
132
+ _condition = menu() or editing() or (previouslySelected and not selected)
133
+ end
134
+ end
135
+ if _condition then
136
+ update({
137
+ kind = "Cancel",
138
+ })
139
+ end
140
+ previouslySelected = selected
141
+ end)
142
+ effect(function()
143
+ local on = highlighted()
144
+ if on and not options.silent then
145
+ host.feedback("Focus")
146
+ end
147
+ untrack(function()
148
+ local _result = options.onHighlightChange
149
+ if _result ~= nil then
150
+ _result = _result(on)
151
+ end
152
+ return _result
153
+ end)
154
+ cleanup(function()
155
+ if on then
156
+ untrack(function()
157
+ local _result = options.onHighlightChange
158
+ if _result ~= nil then
159
+ _result = _result(false)
160
+ end
161
+ return _result
162
+ end)
163
+ end
164
+ end)
165
+ end)
166
+ local ticking = derive(function()
167
+ return pressed() or state().progress > 0
168
+ end)
169
+ effect(function()
170
+ if not ticking() then
171
+ return nil
172
+ end
173
+ cleanup(heartbeat(function(_, delta)
174
+ return update({
175
+ kind = "Tick",
176
+ delta = delta,
177
+ })
178
+ end))
179
+ end)
180
+ cleanup(function()
181
+ return update({
182
+ kind = "Cancel",
183
+ })
184
+ end)
185
+ local pointer = function(input)
186
+ return input.UserInputType == Enum.UserInputType.MouseButton1 or input.UserInputType == Enum.UserInputType.Touch
187
+ end
188
+ return {
189
+ highlighted = highlighted,
190
+ pressed = pressed,
191
+ progress = function()
192
+ return state().progress
193
+ end,
194
+ action = function(instance)
195
+ selection.action(instance)
196
+ cleanup(instance.Activated:Connect(function()
197
+ if not timedPress and mode().kind == "Activate" then
198
+ fire()
199
+ end
200
+ end))
201
+ cleanup(instance.MouseEnter:Connect(function()
202
+ return hovered(true)
203
+ end))
204
+ cleanup(instance.MouseLeave:Connect(function()
205
+ hovered(false)
206
+ update({
207
+ kind = "Cancel",
208
+ })
209
+ end))
210
+ cleanup(instance.InputBegan:Connect(function(input)
211
+ if pointer(input) then
212
+ begin(input)
213
+ end
214
+ end))
215
+ cleanup(UserInputService.InputEnded:Connect(function(input)
216
+ if pointer(input) then
217
+ update({
218
+ kind = "End",
219
+ source = input,
220
+ })
221
+ end
222
+ end))
223
+ end,
224
+ }
225
+ end
226
+ return {
227
+ usePress = usePress,
228
+ }
package/package.json CHANGED
@@ -1,6 +1,43 @@
1
1
  {
2
- "name": "@trioforge/anvil",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
2
+ "name": "@trioforge/anvil",
3
+ "version": "0.1.1",
4
+ "description": "Headless UI primitives for Roblox games, built on Vide",
5
+ "keywords": ["roblox", "roblox-ts", "vide", "ui", "headless", "gamepad"],
6
+ "bugs": "https://github.com/Trio-Forge-Studio/anvil/issues",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "git+https://github.com/Trio-Forge-Studio/anvil.git"
10
+ },
11
+ "license": "MIT",
12
+ "main": "out/init.luau",
13
+ "types": "out/index.d.ts",
14
+ "files": ["out", "!**/*.tsbuildinfo", "README.md", "LICENSE"],
15
+ "packageManager": "pnpm@12.15.0",
16
+ "publishConfig": {
17
+ "access": "public"
18
+ },
19
+ "scripts": {
20
+ "build": "rbxtsc --type package",
21
+ "test": "rbxtsc --type package && lune run test",
22
+ "fmt": "oxfmt src",
23
+ "check": "oxfmt --check src && rbxtsc --type package && stylua --check test .lune && lune run test",
24
+ "prepack": "rbxtsc --type package"
25
+ },
26
+ "peerDependencies": {
27
+ "@rbxts/charm": "^0.11.1",
28
+ "@rbxts/services": "^1.6.0",
29
+ "@rbxts/vide": "^0.6.1",
30
+ "@rbxts/vide-charm": "^0.4.1"
31
+ },
32
+ "devDependencies": {
33
+ "@rbxts/charm": "^0.11.1",
34
+ "@rbxts/compiler-types": "3.0.0-types.0",
35
+ "@rbxts/services": "^1.6.0",
36
+ "@rbxts/types": "^1.0.958",
37
+ "@rbxts/vide": "^0.6.1",
38
+ "@rbxts/vide-charm": "^0.4.1",
39
+ "oxfmt": "^0.74.0",
40
+ "roblox-ts": "^3.0.0",
41
+ "typescript": "5.5.3"
42
+ }
43
+ }