@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.
- package/LICENSE +21 -0
- package/README.md +111 -2
- package/out/adapter.d.ts +16 -0
- package/out/adapter.luau +19 -0
- package/out/frame-loop.d.ts +3 -0
- package/out/frame-loop.luau +56 -0
- package/out/index.d.ts +6 -0
- package/out/init.luau +18 -0
- package/out/modal-state.d.ts +12 -0
- package/out/modal-state.luau +133 -0
- package/out/modal.d.ts +13 -0
- package/out/modal.luau +58 -0
- package/out/press-state.d.ts +35 -0
- package/out/press-state.luau +104 -0
- package/out/props.d.ts +12 -0
- package/out/props.luau +2 -0
- package/out/runtime.d.ts +30 -0
- package/out/runtime.luau +797 -0
- package/out/selection-policy.d.ts +33 -0
- package/out/selection-policy.luau +112 -0
- package/out/use-press.d.ts +22 -0
- package/out/use-press.luau +228 -0
- package/package.json +42 -5
|
@@ -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
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
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
|
+
}
|