@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
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Trio Forge Studio
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,112 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @trioforge/anvil
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Headless UI primitives for Roblox games, built on [Vide](https://centau.github.io/vide/). Anvil owns behaviour: who gets gamepad selection, which modal is on top, how a press, a hold or a repeat fires, and when the Close button may close a screen. Your game owns every pixel and every sound.
|
|
4
|
+
|
|
5
|
+
Anvil started as the interface runtime of Fragile Delivery. Version 0.1 covers modals, selection and presses. Components such as sliders, tabs and dialogs come next.
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
```sh
|
|
10
|
+
pnpm add @trioforge/anvil @rbxts/vide @rbxts/charm @rbxts/vide-charm @rbxts/services
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
roblox-ts only looks for packages in the scopes your project lists, so add the `@trioforge` scope in two places.
|
|
14
|
+
|
|
15
|
+
1. In `tsconfig.json`, add it to `typeRoots`:
|
|
16
|
+
|
|
17
|
+
```json
|
|
18
|
+
"typeRoots": ["node_modules/@rbxts", "node_modules/@trioforge"]
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
2. In your Rojo project, map it beside `@rbxts` under `rbxts_include/node_modules`:
|
|
22
|
+
|
|
23
|
+
```json
|
|
24
|
+
"node_modules": {
|
|
25
|
+
"$className": "Folder",
|
|
26
|
+
"@rbxts": { "$path": "node_modules/@rbxts" },
|
|
27
|
+
"@trioforge": { "$path": "node_modules/@trioforge" }
|
|
28
|
+
}
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## Connect your game
|
|
32
|
+
|
|
33
|
+
Anvil reads input and gives feedback only through an adapter that you install once, on the client, before any Anvil UI renders. Then start the menu runtime.
|
|
34
|
+
|
|
35
|
+
```ts
|
|
36
|
+
import { atom } from "@rbxts/charm";
|
|
37
|
+
import { UserInputService } from "@rbxts/services";
|
|
38
|
+
import Vide from "@rbxts/vide";
|
|
39
|
+
import { installAnvil, Modal, startMenuRuntime, usePress, type AnvilAdapter, type InputDevice } from "@trioforge/anvil";
|
|
40
|
+
|
|
41
|
+
const device = atom<InputDevice>("Keyboard");
|
|
42
|
+
UserInputService.LastInputTypeChanged.Connect((input) => {
|
|
43
|
+
if (input === Enum.UserInputType.Touch) device("Touch");
|
|
44
|
+
else if (input === Enum.UserInputType.Gamepad1) device("Gamepad");
|
|
45
|
+
else if (input === Enum.UserInputType.Keyboard || input === Enum.UserInputType.MouseMovement) device("Keyboard");
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
function onButtonB(signal: RBXScriptSignal<(input: InputObject) => void>, listener: () => void) {
|
|
49
|
+
const connection = signal.Connect((input) => {
|
|
50
|
+
if (input.KeyCode === Enum.KeyCode.ButtonB) listener();
|
|
51
|
+
});
|
|
52
|
+
return () => connection.Disconnect();
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const sounds = new Map<string, Sound>();
|
|
56
|
+
|
|
57
|
+
const adapter: AnvilAdapter = {
|
|
58
|
+
device,
|
|
59
|
+
close: {
|
|
60
|
+
held: () =>
|
|
61
|
+
UserInputService.GetConnectedGamepads().some((gamepad) =>
|
|
62
|
+
UserInputService.IsGamepadButtonDown(gamepad, Enum.KeyCode.ButtonB),
|
|
63
|
+
),
|
|
64
|
+
onPressed: (listener) => onButtonB(UserInputService.InputBegan, listener),
|
|
65
|
+
onReleased: (listener) => onButtonB(UserInputService.InputEnded, listener),
|
|
66
|
+
},
|
|
67
|
+
claimMenuInput: () => () => {},
|
|
68
|
+
feedback: (kind) => sounds.get(kind)?.Play(),
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
installAnvil(adapter);
|
|
72
|
+
startMenuRuntime();
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
`installAnvil` accepts the same adapter again and does nothing. It refuses a different adapter. Anvil reads the adapter when the first control renders or the runtime starts, and raises an error if you never installed one.
|
|
76
|
+
|
|
77
|
+
| Member | What Anvil does with it |
|
|
78
|
+
| --- | --- |
|
|
79
|
+
| `device` | Reads the current input device. Selection only moves while it is `"Gamepad"`, and highlights ignore `"Touch"`. It must be a Charm atom or computed, because Anvil reads it inside Charm effects. |
|
|
80
|
+
| `close.held` | Asks whether the close input is down. A screen closes only on a fresh press, so a press that started before the screen opened does nothing. |
|
|
81
|
+
| `close.onPressed` | Subscribes to close presses. The top native modal's `onClose` runs on each armed press. |
|
|
82
|
+
| `close.onReleased` | Subscribes to anything that may have lifted the close input. Anvil reads `close.held` again each time, so extra calls are harmless. |
|
|
83
|
+
| `claimMenuInput` | Runs while a native modal is on top and no text box has focus. Return a function that releases the claim, for example to stop gameplay actions from reading ButtonB. |
|
|
84
|
+
| `feedback` | Receives `"Focus"`, `"Press"` or `"Refused"` for each press that is not `silent`. Play your sounds here. |
|
|
85
|
+
|
|
86
|
+
## Build a modal
|
|
87
|
+
|
|
88
|
+
```tsx
|
|
89
|
+
function BuyButton(props: { onBuy: () => void }) {
|
|
90
|
+
const press = usePress({ onPress: props.onBuy, selectFirst: true });
|
|
91
|
+
return (
|
|
92
|
+
<textbutton
|
|
93
|
+
action={press.action}
|
|
94
|
+
Text="Buy"
|
|
95
|
+
Size={() => (press.pressed() ? UDim2.fromOffset(112, 38) : UDim2.fromOffset(120, 40))}
|
|
96
|
+
BackgroundColor3={() => (press.highlighted() ? Color3.fromRGB(255, 214, 102) : Color3.fromRGB(240, 240, 240))}
|
|
97
|
+
/>
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export function Shop(props: { onBuy: () => void; onClose: () => void }) {
|
|
102
|
+
return <Modal name="Shop" onClose={props.onClose}>{() => <BuyButton onBuy={props.onBuy} />}</Modal>;
|
|
103
|
+
}
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
- `usePress` returns `highlighted`, `pressed` and `progress` getters, and an `action` that you attach to your own `GuiButton`. Pass `hold` for a hold-to-confirm button, or `repeating` for a button that repeats while held.
|
|
107
|
+
- `Modal` takes its children as a function, so the controls inside know which modal owns them.
|
|
108
|
+
- The topmost modal, ranked by its `ScreenGui`'s `DisplayOrder`, owns gamepad selection and blocks presses underneath. Use `modalOpen()` to pause gameplay while any modal is open.
|
|
109
|
+
|
|
110
|
+
## License
|
|
111
|
+
|
|
112
|
+
MIT
|
package/out/adapter.d.ts
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { Getter } from "@rbxts/charm";
|
|
2
|
+
export type InputDevice = "Gamepad" | "Touch" | "Keyboard";
|
|
3
|
+
export type PressFeedback = "Focus" | "Press" | "Refused";
|
|
4
|
+
export interface CloseInput {
|
|
5
|
+
readonly held: () => boolean;
|
|
6
|
+
readonly onPressed: (listener: () => void) => () => void;
|
|
7
|
+
readonly onReleased: (listener: () => void) => () => void;
|
|
8
|
+
}
|
|
9
|
+
export interface AnvilAdapter {
|
|
10
|
+
readonly device: Getter<InputDevice>;
|
|
11
|
+
readonly close: CloseInput;
|
|
12
|
+
readonly claimMenuInput: () => () => void;
|
|
13
|
+
readonly feedback: (kind: PressFeedback) => void;
|
|
14
|
+
}
|
|
15
|
+
export declare function installAnvil(adapter: AnvilAdapter): void;
|
|
16
|
+
export declare function adapter(): AnvilAdapter;
|
package/out/adapter.luau
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
-- Compiled with roblox-ts v3.0.0
|
|
2
|
+
local installed
|
|
3
|
+
local function installAnvil(adapter)
|
|
4
|
+
if installed == adapter then
|
|
5
|
+
return nil
|
|
6
|
+
end
|
|
7
|
+
local _arg0 = installed == nil
|
|
8
|
+
assert(_arg0, "Anvil already has a different adapter")
|
|
9
|
+
installed = adapter
|
|
10
|
+
end
|
|
11
|
+
local function adapter()
|
|
12
|
+
local _installed = installed
|
|
13
|
+
assert(_installed, "Anvil has no adapter. Call installAnvil before rendering Anvil UI.")
|
|
14
|
+
return installed
|
|
15
|
+
end
|
|
16
|
+
return {
|
|
17
|
+
installAnvil = installAnvil,
|
|
18
|
+
adapter = adapter,
|
|
19
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
-- Compiled with roblox-ts v3.0.0
|
|
2
|
+
local TS = _G[script]
|
|
3
|
+
local RunService = TS.import(script, TS.getModule(script, "@rbxts", "services")).RunService
|
|
4
|
+
local function loop(signal)
|
|
5
|
+
local listeners = {}
|
|
6
|
+
local order
|
|
7
|
+
local connection
|
|
8
|
+
return function(listener)
|
|
9
|
+
local _listener = listener
|
|
10
|
+
listeners[_listener] = true
|
|
11
|
+
order = nil
|
|
12
|
+
if connection == nil then
|
|
13
|
+
connection = signal:Connect(function(delta)
|
|
14
|
+
if order == nil then
|
|
15
|
+
local _array = {}
|
|
16
|
+
local _length = #_array
|
|
17
|
+
for _v in listeners do
|
|
18
|
+
_length += 1
|
|
19
|
+
_array[_length] = _v
|
|
20
|
+
end
|
|
21
|
+
order = _array
|
|
22
|
+
end
|
|
23
|
+
local now = os.clock()
|
|
24
|
+
for _, callback in order do
|
|
25
|
+
if listeners[callback] ~= nil then
|
|
26
|
+
callback(now, delta)
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end)
|
|
30
|
+
end
|
|
31
|
+
return function()
|
|
32
|
+
local _listener_1 = listener
|
|
33
|
+
-- ▼ Set.delete ▼
|
|
34
|
+
local _valueExisted = listeners[_listener_1] ~= nil
|
|
35
|
+
listeners[_listener_1] = nil
|
|
36
|
+
-- ▲ Set.delete ▲
|
|
37
|
+
if not _valueExisted then
|
|
38
|
+
return nil
|
|
39
|
+
end
|
|
40
|
+
order = nil
|
|
41
|
+
if next(listeners) == nil then
|
|
42
|
+
local _result = connection
|
|
43
|
+
if _result ~= nil then
|
|
44
|
+
_result:Disconnect()
|
|
45
|
+
end
|
|
46
|
+
connection = nil
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
local heartbeat = loop(RunService.Heartbeat)
|
|
52
|
+
local preRender = loop(RunService.PreRender)
|
|
53
|
+
return {
|
|
54
|
+
heartbeat = heartbeat,
|
|
55
|
+
preRender = preRender,
|
|
56
|
+
}
|
package/out/index.d.ts
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export { installAnvil, type AnvilAdapter, type CloseInput, type InputDevice, type PressFeedback } from "./adapter";
|
|
2
|
+
export { Modal, modalOpen, useModalActive, useTextEditing, type ModalProps } from "./modal";
|
|
3
|
+
export { closeFallbackContext, startMenuRuntime, useSelection } from "./runtime";
|
|
4
|
+
export { usePress, type PressOptions, type PressTiming } from "./use-press";
|
|
5
|
+
export { heartbeat, preRender, type FrameListener } from "./frame-loop";
|
|
6
|
+
export type { Children, ChildFactory, Getter, Placement, Value } from "./props";
|
package/out/init.luau
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
-- Compiled with roblox-ts v3.0.0
|
|
2
|
+
local TS = _G[script]
|
|
3
|
+
local exports = {}
|
|
4
|
+
exports.installAnvil = TS.import(script, script, "adapter").installAnvil
|
|
5
|
+
local _modal = TS.import(script, script, "modal")
|
|
6
|
+
exports.Modal = _modal.Modal
|
|
7
|
+
exports.modalOpen = _modal.modalOpen
|
|
8
|
+
exports.useModalActive = _modal.useModalActive
|
|
9
|
+
exports.useTextEditing = _modal.useTextEditing
|
|
10
|
+
local _runtime = TS.import(script, script, "runtime")
|
|
11
|
+
exports.closeFallbackContext = _runtime.closeFallbackContext
|
|
12
|
+
exports.startMenuRuntime = _runtime.startMenuRuntime
|
|
13
|
+
exports.useSelection = _runtime.useSelection
|
|
14
|
+
exports.usePress = TS.import(script, script, "use-press").usePress
|
|
15
|
+
local _frame_loop = TS.import(script, script, "frame-loop")
|
|
16
|
+
exports.heartbeat = _frame_loop.heartbeat
|
|
17
|
+
exports.preRender = _frame_loop.preRender
|
|
18
|
+
return exports
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export interface ModalEntry<Id> {
|
|
2
|
+
readonly id: Id;
|
|
3
|
+
readonly order: number | undefined;
|
|
4
|
+
readonly serial: number;
|
|
5
|
+
}
|
|
6
|
+
export interface ModalState<Id> {
|
|
7
|
+
readonly entries: readonly ModalEntry<Id>[];
|
|
8
|
+
}
|
|
9
|
+
export declare function enter<Id>(state: ModalState<Id>, entry: ModalEntry<Id>): ModalState<Id>;
|
|
10
|
+
export declare function leave<Id>(state: ModalState<Id>, id: Id): ModalState<Id>;
|
|
11
|
+
export declare function top<Id>(state: ModalState<Id>): ModalEntry<Id> | undefined;
|
|
12
|
+
export declare function hasModal<Id>(state: ModalState<Id>): boolean;
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
-- Compiled with roblox-ts v3.0.0
|
|
2
|
+
local function enter(state, entry)
|
|
3
|
+
local _exp = state.entries
|
|
4
|
+
-- ▼ ReadonlyArray.find ▼
|
|
5
|
+
local _callback = function(value)
|
|
6
|
+
return value.id == entry.id
|
|
7
|
+
end
|
|
8
|
+
local _result
|
|
9
|
+
for _i, _v in _exp do
|
|
10
|
+
if _callback(_v, _i - 1, _exp) == true then
|
|
11
|
+
_result = _v
|
|
12
|
+
break
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
-- ▲ ReadonlyArray.find ▲
|
|
16
|
+
local existing = _result
|
|
17
|
+
local _result_1 = existing
|
|
18
|
+
if _result_1 ~= nil then
|
|
19
|
+
_result_1 = _result_1.order
|
|
20
|
+
end
|
|
21
|
+
local _condition = _result_1 == entry.order
|
|
22
|
+
if _condition then
|
|
23
|
+
_condition = existing ~= nil
|
|
24
|
+
end
|
|
25
|
+
if _condition then
|
|
26
|
+
return state
|
|
27
|
+
end
|
|
28
|
+
local _object = {}
|
|
29
|
+
local _left = "entries"
|
|
30
|
+
local _result_2
|
|
31
|
+
if existing then
|
|
32
|
+
local _exp_1 = state.entries
|
|
33
|
+
-- ▼ ReadonlyArray.map ▼
|
|
34
|
+
local _newValue = table.create(#_exp_1)
|
|
35
|
+
local _callback_1 = function(value)
|
|
36
|
+
local _result_3
|
|
37
|
+
if value.id == entry.id then
|
|
38
|
+
local _object_1 = table.clone(value)
|
|
39
|
+
setmetatable(_object_1, nil)
|
|
40
|
+
_object_1.order = entry.order
|
|
41
|
+
_result_3 = _object_1
|
|
42
|
+
else
|
|
43
|
+
_result_3 = value
|
|
44
|
+
end
|
|
45
|
+
return _result_3
|
|
46
|
+
end
|
|
47
|
+
for _k, _v in _exp_1 do
|
|
48
|
+
_newValue[_k] = _callback_1(_v, _k - 1, _exp_1)
|
|
49
|
+
end
|
|
50
|
+
-- ▲ ReadonlyArray.map ▲
|
|
51
|
+
_result_2 = _newValue
|
|
52
|
+
else
|
|
53
|
+
local _array = {}
|
|
54
|
+
local _length = #_array
|
|
55
|
+
local _array_1 = state.entries
|
|
56
|
+
local _Length = #_array_1
|
|
57
|
+
table.move(_array_1, 1, _Length, _length + 1, _array)
|
|
58
|
+
_length += _Length
|
|
59
|
+
_array[_length + 1] = entry
|
|
60
|
+
_result_2 = _array
|
|
61
|
+
end
|
|
62
|
+
_object[_left] = _result_2
|
|
63
|
+
return _object
|
|
64
|
+
end
|
|
65
|
+
local function leave(state, id)
|
|
66
|
+
local _exp = state.entries
|
|
67
|
+
-- ▼ ReadonlyArray.some ▼
|
|
68
|
+
local _result = false
|
|
69
|
+
local _callback = function(value)
|
|
70
|
+
return value.id == id
|
|
71
|
+
end
|
|
72
|
+
for _k, _v in _exp do
|
|
73
|
+
if _callback(_v, _k - 1, _exp) then
|
|
74
|
+
_result = true
|
|
75
|
+
break
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
-- ▲ ReadonlyArray.some ▲
|
|
79
|
+
if not _result then
|
|
80
|
+
return state
|
|
81
|
+
end
|
|
82
|
+
local _object = {}
|
|
83
|
+
local _left = "entries"
|
|
84
|
+
local _exp_1 = state.entries
|
|
85
|
+
-- ▼ ReadonlyArray.filter ▼
|
|
86
|
+
local _newValue = {}
|
|
87
|
+
local _callback_1 = function(value)
|
|
88
|
+
return value.id ~= id
|
|
89
|
+
end
|
|
90
|
+
local _length = 0
|
|
91
|
+
for _k, _v in _exp_1 do
|
|
92
|
+
if _callback_1(_v, _k - 1, _exp_1) == true then
|
|
93
|
+
_length += 1
|
|
94
|
+
_newValue[_length] = _v
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
-- ▲ ReadonlyArray.filter ▲
|
|
98
|
+
_object[_left] = _newValue
|
|
99
|
+
return _object
|
|
100
|
+
end
|
|
101
|
+
local function top(state)
|
|
102
|
+
local result
|
|
103
|
+
for _, entry in state.entries do
|
|
104
|
+
if entry.order == nil then
|
|
105
|
+
return nil
|
|
106
|
+
end
|
|
107
|
+
local _condition = not result
|
|
108
|
+
if not _condition then
|
|
109
|
+
local _exp = entry.order
|
|
110
|
+
local _condition_1 = result.order
|
|
111
|
+
if _condition_1 == nil then
|
|
112
|
+
_condition_1 = 0
|
|
113
|
+
end
|
|
114
|
+
_condition = _exp > _condition_1
|
|
115
|
+
if not _condition then
|
|
116
|
+
_condition = (entry.order == result.order and entry.serial > result.serial)
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
if _condition then
|
|
120
|
+
result = entry
|
|
121
|
+
end
|
|
122
|
+
end
|
|
123
|
+
return result
|
|
124
|
+
end
|
|
125
|
+
local function hasModal(state)
|
|
126
|
+
return not (#state.entries == 0)
|
|
127
|
+
end
|
|
128
|
+
return {
|
|
129
|
+
enter = enter,
|
|
130
|
+
leave = leave,
|
|
131
|
+
top = top,
|
|
132
|
+
hasModal = hasModal,
|
|
133
|
+
}
|
package/out/modal.d.ts
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import Vide from "@rbxts/vide";
|
|
2
|
+
import type { ChildFactory, Placement, Value } from "./props";
|
|
3
|
+
import { modalOpen } from "./runtime";
|
|
4
|
+
export { modalOpen };
|
|
5
|
+
export interface ModalProps extends Placement {
|
|
6
|
+
selection?: "Native" | "Pointer";
|
|
7
|
+
onClose?: () => void;
|
|
8
|
+
size?: Value<UDim2>;
|
|
9
|
+
children: ChildFactory;
|
|
10
|
+
}
|
|
11
|
+
export declare function Modal(props: ModalProps): Vide.Node;
|
|
12
|
+
export declare function useModalActive(): () => boolean;
|
|
13
|
+
export declare function useTextEditing(): () => boolean;
|
package/out/modal.luau
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
-- Compiled with roblox-ts v3.0.0
|
|
2
|
+
local TS = _G[script]
|
|
3
|
+
local _vide = TS.import(script, TS.getModule(script, "@rbxts", "vide").src)
|
|
4
|
+
local Vide = _vide
|
|
5
|
+
local cleanup = _vide.cleanup
|
|
6
|
+
local useAtom = TS.import(script, TS.getModule(script, "@rbxts", "vide-charm").src).useAtom
|
|
7
|
+
local _runtime = TS.import(script, script.Parent, "runtime")
|
|
8
|
+
local active = _runtime.active
|
|
9
|
+
local attachRoot = _runtime.attachRoot
|
|
10
|
+
local closeRoot = _runtime.closeRoot
|
|
11
|
+
local modalOpen = _runtime.modalOpen
|
|
12
|
+
local openRoot = _runtime.openRoot
|
|
13
|
+
local ownerContext = _runtime.ownerContext
|
|
14
|
+
local textEditing = _runtime.textEditing
|
|
15
|
+
local function Modal(props)
|
|
16
|
+
local id = openRoot(props.selection or "Native", props.onClose)
|
|
17
|
+
cleanup(function()
|
|
18
|
+
return closeRoot(id)
|
|
19
|
+
end)
|
|
20
|
+
return ownerContext(id, function()
|
|
21
|
+
local _attributes = {}
|
|
22
|
+
local _condition = props.name
|
|
23
|
+
if _condition == nil then
|
|
24
|
+
_condition = "Modal"
|
|
25
|
+
end
|
|
26
|
+
_attributes.Name = _condition
|
|
27
|
+
_attributes.Size = props.size or UDim2.fromScale(1, 1)
|
|
28
|
+
_attributes.Position = props.position
|
|
29
|
+
_attributes.AnchorPoint = props.anchorPoint
|
|
30
|
+
_attributes.LayoutOrder = props.layoutOrder
|
|
31
|
+
_attributes.ZIndex = props.zIndex
|
|
32
|
+
_attributes.BackgroundTransparency = 1
|
|
33
|
+
_attributes.SelectionGroup = true
|
|
34
|
+
_attributes.SelectionBehaviorUp = Enum.SelectionBehavior.Stop
|
|
35
|
+
_attributes.SelectionBehaviorDown = Enum.SelectionBehavior.Stop
|
|
36
|
+
_attributes.SelectionBehaviorLeft = Enum.SelectionBehavior.Stop
|
|
37
|
+
_attributes.SelectionBehaviorRight = Enum.SelectionBehavior.Stop
|
|
38
|
+
_attributes.action = function(root)
|
|
39
|
+
return cleanup(attachRoot(id, root))
|
|
40
|
+
end
|
|
41
|
+
return Vide.jsx("frame", _attributes, props.children())
|
|
42
|
+
end)
|
|
43
|
+
end
|
|
44
|
+
local function useModalActive()
|
|
45
|
+
local owner = ownerContext()
|
|
46
|
+
return useAtom(function()
|
|
47
|
+
return active(owner)
|
|
48
|
+
end)
|
|
49
|
+
end
|
|
50
|
+
local function useTextEditing()
|
|
51
|
+
return useAtom(textEditing)
|
|
52
|
+
end
|
|
53
|
+
return {
|
|
54
|
+
Modal = Modal,
|
|
55
|
+
useModalActive = useModalActive,
|
|
56
|
+
useTextEditing = useTextEditing,
|
|
57
|
+
modalOpen = modalOpen,
|
|
58
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
export type PressMode = {
|
|
2
|
+
readonly kind: "Activate";
|
|
3
|
+
} | {
|
|
4
|
+
readonly kind: "Hold";
|
|
5
|
+
readonly seconds: number;
|
|
6
|
+
} | {
|
|
7
|
+
readonly kind: "Repeat";
|
|
8
|
+
};
|
|
9
|
+
export interface PressState<Source> {
|
|
10
|
+
readonly source?: Source;
|
|
11
|
+
readonly elapsed: number;
|
|
12
|
+
readonly progress: number;
|
|
13
|
+
readonly fired: boolean;
|
|
14
|
+
readonly nextRepeatAt: number;
|
|
15
|
+
readonly repeatInterval: number;
|
|
16
|
+
}
|
|
17
|
+
export type PressEvent<Source> = {
|
|
18
|
+
readonly kind: "Begin";
|
|
19
|
+
readonly source: Source;
|
|
20
|
+
} | {
|
|
21
|
+
readonly kind: "Tick";
|
|
22
|
+
readonly delta: number;
|
|
23
|
+
} | {
|
|
24
|
+
readonly kind: "End";
|
|
25
|
+
readonly source: Source;
|
|
26
|
+
} | {
|
|
27
|
+
readonly kind: "Cancel";
|
|
28
|
+
};
|
|
29
|
+
export interface PressTransition<Source> {
|
|
30
|
+
readonly state: PressState<Source>;
|
|
31
|
+
readonly fire: boolean;
|
|
32
|
+
readonly ended: boolean;
|
|
33
|
+
}
|
|
34
|
+
export declare function idlePress<Source>(): PressState<Source>;
|
|
35
|
+
export declare function transition<Source>(state: PressState<Source>, mode: PressMode, event: PressEvent<Source>): PressTransition<Source>;
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
-- Compiled with roblox-ts v3.0.0
|
|
2
|
+
local function idlePress()
|
|
3
|
+
return {
|
|
4
|
+
elapsed = 0,
|
|
5
|
+
progress = 0,
|
|
6
|
+
fired = false,
|
|
7
|
+
nextRepeatAt = 0.4,
|
|
8
|
+
repeatInterval = 0.15,
|
|
9
|
+
}
|
|
10
|
+
end
|
|
11
|
+
local function transition(state, mode, event)
|
|
12
|
+
if event.kind == "Begin" then
|
|
13
|
+
local _result
|
|
14
|
+
if state.source ~= nil then
|
|
15
|
+
_result = {
|
|
16
|
+
state = state,
|
|
17
|
+
fire = false,
|
|
18
|
+
ended = false,
|
|
19
|
+
}
|
|
20
|
+
else
|
|
21
|
+
local _object = {}
|
|
22
|
+
local _left = "state"
|
|
23
|
+
local _object_1 = table.clone(idlePress())
|
|
24
|
+
setmetatable(_object_1, nil)
|
|
25
|
+
_object_1.source = event.source
|
|
26
|
+
_object[_left] = _object_1
|
|
27
|
+
_object.fire = mode.kind == "Repeat"
|
|
28
|
+
_object.ended = false
|
|
29
|
+
_result = _object
|
|
30
|
+
end
|
|
31
|
+
return _result
|
|
32
|
+
end
|
|
33
|
+
if event.kind == "Cancel" or event.kind == "End" then
|
|
34
|
+
if event.kind == "End" and event.source ~= state.source then
|
|
35
|
+
return {
|
|
36
|
+
state = state,
|
|
37
|
+
fire = false,
|
|
38
|
+
ended = false,
|
|
39
|
+
}
|
|
40
|
+
end
|
|
41
|
+
local _object = {}
|
|
42
|
+
local _left = "state"
|
|
43
|
+
local _object_1 = table.clone(idlePress())
|
|
44
|
+
setmetatable(_object_1, nil)
|
|
45
|
+
_object_1.progress = state.progress
|
|
46
|
+
_object[_left] = _object_1
|
|
47
|
+
_object.fire = false
|
|
48
|
+
_object.ended = state.source ~= nil
|
|
49
|
+
return _object
|
|
50
|
+
end
|
|
51
|
+
local delta = math.max(0, event.delta)
|
|
52
|
+
if state.source == nil then
|
|
53
|
+
local _object = {}
|
|
54
|
+
local _left = "state"
|
|
55
|
+
local _object_1 = table.clone(state)
|
|
56
|
+
setmetatable(_object_1, nil)
|
|
57
|
+
_object_1.progress = math.max(0, state.progress - delta / 0.25)
|
|
58
|
+
_object[_left] = _object_1
|
|
59
|
+
_object.fire = false
|
|
60
|
+
_object.ended = false
|
|
61
|
+
return _object
|
|
62
|
+
end
|
|
63
|
+
local elapsed = state.elapsed + delta
|
|
64
|
+
if mode.kind == "Hold" then
|
|
65
|
+
local fire = not state.fired and elapsed >= mode.seconds
|
|
66
|
+
local _object = {}
|
|
67
|
+
local _left = "state"
|
|
68
|
+
local _object_1 = table.clone(state)
|
|
69
|
+
setmetatable(_object_1, nil)
|
|
70
|
+
_object_1.elapsed = elapsed
|
|
71
|
+
_object_1.fired = state.fired or fire
|
|
72
|
+
_object_1.progress = if state.fired or fire then 0 else math.min(1, elapsed / mode.seconds)
|
|
73
|
+
_object[_left] = _object_1
|
|
74
|
+
_object.fire = fire
|
|
75
|
+
_object.ended = false
|
|
76
|
+
return _object
|
|
77
|
+
end
|
|
78
|
+
if mode.kind == "Repeat" and elapsed >= state.nextRepeatAt then
|
|
79
|
+
local _object = {}
|
|
80
|
+
local _left = "state"
|
|
81
|
+
local _object_1 = table.clone(state)
|
|
82
|
+
setmetatable(_object_1, nil)
|
|
83
|
+
_object_1.elapsed = elapsed
|
|
84
|
+
_object_1.nextRepeatAt = elapsed + state.repeatInterval
|
|
85
|
+
_object_1.repeatInterval = math.max(0.04, state.repeatInterval * 0.85)
|
|
86
|
+
_object[_left] = _object_1
|
|
87
|
+
_object.fire = true
|
|
88
|
+
_object.ended = false
|
|
89
|
+
return _object
|
|
90
|
+
end
|
|
91
|
+
local _object = {}
|
|
92
|
+
local _left = "state"
|
|
93
|
+
local _object_1 = table.clone(state)
|
|
94
|
+
setmetatable(_object_1, nil)
|
|
95
|
+
_object_1.elapsed = elapsed
|
|
96
|
+
_object[_left] = _object_1
|
|
97
|
+
_object.fire = false
|
|
98
|
+
_object.ended = false
|
|
99
|
+
return _object
|
|
100
|
+
end
|
|
101
|
+
return {
|
|
102
|
+
idlePress = idlePress,
|
|
103
|
+
transition = transition,
|
|
104
|
+
}
|
package/out/props.d.ts
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type Vide from "@rbxts/vide";
|
|
2
|
+
export type Value<T> = Vide.Derivable<T>;
|
|
3
|
+
export type Getter<T> = () => T;
|
|
4
|
+
export type Children = Vide.Node;
|
|
5
|
+
export type ChildFactory = () => Vide.Node;
|
|
6
|
+
export interface Placement {
|
|
7
|
+
name?: string;
|
|
8
|
+
layoutOrder?: number;
|
|
9
|
+
anchorPoint?: Value<Vector2>;
|
|
10
|
+
position?: Value<UDim2>;
|
|
11
|
+
zIndex?: Value<number>;
|
|
12
|
+
}
|
package/out/props.luau
ADDED
package/out/runtime.d.ts
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { Value } from "./props";
|
|
2
|
+
export type Owner = number | "Base";
|
|
3
|
+
export declare const ownerContext: import("@rbxts/vide").Context<Owner>;
|
|
4
|
+
export declare const closeFallbackContext: import("@rbxts/vide").Context<boolean>;
|
|
5
|
+
export declare const modalOpen: import("@rbxts/charm").Getter<boolean>;
|
|
6
|
+
export declare const selectedObject: import("@rbxts/charm").Atom<GuiObject | undefined>;
|
|
7
|
+
export declare const textEditing: import("@rbxts/charm").Atom<boolean>;
|
|
8
|
+
export declare const menuOpen: import("@rbxts/charm").Atom<boolean>;
|
|
9
|
+
interface Control {
|
|
10
|
+
readonly owner: Owner;
|
|
11
|
+
readonly buttonADown?: (input: InputObject) => void;
|
|
12
|
+
readonly buttonAUp?: (input: InputObject) => void;
|
|
13
|
+
}
|
|
14
|
+
export declare function active(owner: Owner): boolean;
|
|
15
|
+
export declare function openRoot(mode: "Native" | "Pointer", onClose?: () => void): number;
|
|
16
|
+
export declare function attachRoot(id: number, root: GuiObject): () => void;
|
|
17
|
+
export declare function closeRoot(id: number): void;
|
|
18
|
+
export declare function registerControl(object: GuiObject, control: Control, fallback?: boolean): () => void;
|
|
19
|
+
export declare function markControl(owner: Owner, object: GuiObject, marked: boolean): void;
|
|
20
|
+
export declare function useSelection(options?: {
|
|
21
|
+
selectFirst?: Value<boolean>;
|
|
22
|
+
buttonADown?: (input: InputObject) => void;
|
|
23
|
+
buttonAUp?: (input: InputObject) => void;
|
|
24
|
+
}): {
|
|
25
|
+
allowed: () => boolean;
|
|
26
|
+
selected: () => boolean;
|
|
27
|
+
action: (instance: GuiObject) => void;
|
|
28
|
+
};
|
|
29
|
+
export declare function startMenuRuntime(): () => void;
|
|
30
|
+
export {};
|