Skip to content

Commit 1708be9

Browse files
committed
Broaden VX browser runtime surface
1 parent bf00c12 commit 1708be9

6 files changed

Lines changed: 1061 additions & 10 deletions

File tree

apps/smoke/fixtures/vx-runtime-browser.voyd

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,14 @@ obj Model {
1111

1212
enum Msg
1313
KeyDown { key: String, code: String, ctrl: bool }
14+
ClipboardRead { value: String }
15+
LocationChanged { href: String }
16+
WindowEvent { event: String }
17+
Frame { timestamp: f64 }
18+
Media { matches: bool }
19+
Storage { key: String }
20+
Broadcast { value: String }
21+
Changed
1422

1523
pub fn app() -> Program<Model, Msg>
1624
program({ init, step, view, subscriptions })
@@ -25,6 +33,8 @@ fn step(model: Model, msg: Msg) -> Program<Model, Msg>
2533
match(msg)
2634
Msg::KeyDown { key, code, ctrl }:
2735
next<Model, Msg>(Model { status: "key", key: key, code: code, ctrl: ctrl })
36+
else:
37+
next<Model, Msg>(model)
2838

2939
fn view(model: Model) -> Html<Msg>
3040
<div>
@@ -41,6 +51,80 @@ fn subscriptions(model: Model) -> Sub<Msg>
4151
Msg::KeyDown { key: event.key, code: event.code, ctrl: event.ctrl_key }
4252
)
4353

54+
pub fn standard_commands() -> Cmd<Msg>
55+
let editor = Ref<DomElement> { id: "editor" }
56+
Cmd::batch([
57+
Cmd<Msg>::copy_to_clipboard("Copied from Voyd"),
58+
Cmd<Msg>::read_clipboard((value: String) -> Msg => Msg::ClipboardRead { value: value }),
59+
Cmd<Msg>::set_document_title("VX"),
60+
Cmd<Msg>::push_url("/next"),
61+
Cmd<Msg>::replace_url("/next?replace=1"),
62+
Cmd<Msg>::set_hash("section"),
63+
Cmd<Msg>::navigate_back(),
64+
Cmd<Msg>::navigate_forward(),
65+
Cmd<Msg>::open_url("/external"),
66+
Cmd<Msg>::open_url(url: "/external", target: "_self"),
67+
Cmd<Msg>::scroll_window_to(x: 0.0, y: 10.0),
68+
Cmd<Msg>::scroll_window_by(x: 1.0, y: 2.0),
69+
Cmd<Msg>::local_storage_set(key: "draft", value: "yes"),
70+
Cmd<Msg>::local_storage_remove("draft"),
71+
Cmd<Msg>::local_storage_clear(),
72+
Cmd<Msg>::session_storage_set(key: "draft", value: "yes"),
73+
Cmd<Msg>::session_storage_remove("draft"),
74+
Cmd<Msg>::session_storage_clear(),
75+
Cmd<Msg>::focus(editor),
76+
Cmd<Msg>::scroll_into_view(editor),
77+
Cmd<Msg>::select_text(editor)
78+
])
79+
80+
pub fn standard_subscriptions() -> Sub<Msg>
81+
Sub::batch([
82+
keyboard_on_key_down(key: "s", value: Msg::Changed {}),
83+
keyboard_on_key_up(
84+
key: "s",
85+
handler: (event: KeyboardEvent) -> Msg =>
86+
Msg::KeyDown { key: event.key, code: event.code, ctrl: event.ctrl_key }
87+
),
88+
online_status(key: "network", value: Msg::Changed {}),
89+
online_status(
90+
key: "network-payload",
91+
handler: (_online: bool) -> Msg => Msg::Changed {}
92+
),
93+
window_on_resize(
94+
key: "viewport",
95+
handler: (_size: WindowSize) -> Msg => Msg::Changed {}
96+
),
97+
document_on_visibility_change(
98+
key: "visibility",
99+
handler: (_visibility: DocumentVisibility) -> Msg => Msg::Changed {}
100+
),
101+
location_on_change(
102+
key: "location",
103+
handler: (location: Location) -> Msg => Msg::LocationChanged { href: location.href }
104+
),
105+
window_on_focus(
106+
key: "focus",
107+
handler: (event: GenericEvent) -> Msg => Msg::WindowEvent { event: event.event }
108+
),
109+
window_on_blur(key: "blur", value: Msg::Changed {}),
110+
animation_frame(
111+
key: "frame",
112+
handler: (frame: AnimationFrame) -> Msg => Msg::Frame { timestamp: frame.timestamp }
113+
),
114+
media_query(
115+
query: "(prefers-reduced-motion: reduce)",
116+
handler: (query: MediaQuery) -> Msg => Msg::Media { matches: query.matches }
117+
),
118+
storage_on_change(
119+
key: "storage",
120+
handler: (_change: StorageChange) -> Msg => Msg::Changed {}
121+
),
122+
broadcast_channel<String, Msg>(
123+
name: "updates",
124+
handler: (value: String) -> Msg => Msg::Broadcast { value: value }
125+
)
126+
])
127+
44128
fn ctrl_label(value: bool) -> String
45129
if
46130
value: "ctrl"

apps/smoke/src/vx-dom.test.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -451,6 +451,12 @@ describe("smoke: compiled VX DOM rendering", () => {
451451
wasm: result.wasm,
452452
bufferSize: 256 * 1024,
453453
});
454+
const commands = await host.run<{ children?: unknown[] }>("standard_commands");
455+
const subscriptions = await host.run<{ children?: unknown[] }>("standard_subscriptions");
456+
457+
expect(commands.children).toHaveLength(21);
458+
expect(subscriptions.children).toHaveLength(13);
459+
454460
const app = createVoydVxAppRuntime({ host });
455461

456462
const container = document.createElement("div");

packages/reference/docs/vx.md

Lines changed: 101 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -695,6 +695,20 @@ Most application code should prefer `Cmd.task`.
695695
Cmd<Msg>::copy_to_clipboard("Saved URL")
696696
```
697697

698+
`Cmd.read_clipboard(handler:)` reads text from the browser clipboard and
699+
dispatches the handler result.
700+
701+
```voyd
702+
Cmd<Msg>::read_clipboard(
703+
(value: String) -> Msg => Msg::ClipboardRead { value: value }
704+
)
705+
```
706+
707+
Clipboard reads can fail because of browser permissions or unavailable APIs.
708+
Those failures are reported through the runtime command error path. Use
709+
`Cmd.task` when the response-producing work should be owned by Voyd task code
710+
instead of the browser runtime host.
711+
698712
`Cmd.focus(target)` focuses a DOM element found by `data-vx-ref`.
699713

700714
```voyd
@@ -708,8 +722,15 @@ Cmd<Msg>::focus(editor)
708722
Cmd<Msg>::scroll_into_view(editor)
709723
```
710724

725+
`Cmd.select_text(target)` selects text in an input-like DOM element found by
726+
`data-vx-ref`.
727+
728+
```voyd
729+
Cmd<Msg>::select_text(editor)
730+
```
731+
711732
`Cmd.set_document_title(value:)`, `Cmd.push_url(value:)`,
712-
`Cmd.replace_url(value:)`, `Cmd.navigate_back()`, and
733+
`Cmd.replace_url(value:)`, `Cmd.set_hash(value:)`, `Cmd.navigate_back()`, and
713734
`Cmd.navigate_forward()` cover common document and history effects.
714735

715736
```voyd
@@ -719,6 +740,32 @@ Cmd::batch([
719740
])
720741
```
721742

743+
`Cmd.scroll_window_to(x:, y:)` and `Cmd.scroll_window_by(x:, y:)` control the
744+
browser window scroll position.
745+
746+
```voyd
747+
Cmd<Msg>::scroll_window_to(x: 0.0, y: 240.0)
748+
Cmd<Msg>::scroll_window_by(x: 0.0, y: 80.0)
749+
```
750+
751+
`Cmd.local_storage_set(key:, value:)`, `Cmd.local_storage_remove(key:)`,
752+
`Cmd.local_storage_clear()`, `Cmd.session_storage_set(key:, value:)`,
753+
`Cmd.session_storage_remove(key:)`, and `Cmd.session_storage_clear()` cover
754+
string storage writes and removals.
755+
756+
```voyd
757+
Cmd<Msg>::local_storage_set(key: "draft", value: model.draft)
758+
Cmd<Msg>::session_storage_remove("wizard-step")
759+
```
760+
761+
`Cmd.open_url(value:)` opens a URL in a new browser context. Use the labeled
762+
form to choose the browser target.
763+
764+
```voyd
765+
Cmd<Msg>::open_url("https://voyd.dev")
766+
Cmd<Msg>::open_url(url: "/help", target: "_self")
767+
```
768+
722769
`Cmd.runtime(kind:)` creates a host command envelope for custom browser or
723770
application capabilities.
724771

@@ -959,11 +1006,57 @@ document_on_visibility_change(
9591006
handler: (visibility: DocumentVisibility) -> Msg =>
9601007
Msg::VisibilityChanged { hidden: visibility.hidden }
9611008
)
1009+
1010+
location_on_change(
1011+
key: "location",
1012+
handler: (location: Location) -> Msg =>
1013+
Msg::LocationChanged { href: location.href }
1014+
)
1015+
1016+
window_on_focus(key: "focus", value: Msg::WindowFocused {})
1017+
window_on_blur(key: "blur", value: Msg::WindowBlurred {})
9621018
```
9631019

9641020
Each helper also has a fixed-message form with `value:` when the app only needs
9651021
to know that the event happened.
9661022

1023+
For rendering loops and browser preferences, use animation frame and media query
1024+
subscriptions:
1025+
1026+
```voyd
1027+
animation_frame(
1028+
key: "render-loop",
1029+
handler: (frame: AnimationFrame) -> Msg =>
1030+
Msg::Frame { timestamp: frame.timestamp }
1031+
)
1032+
1033+
media_query(
1034+
query: "(prefers-reduced-motion: reduce)",
1035+
handler: (query: MediaQuery) -> Msg =>
1036+
Msg::ReducedMotionChanged { enabled: query.matches }
1037+
)
1038+
```
1039+
1040+
For cross-tab and cross-context coordination, use storage and broadcast channel
1041+
subscriptions:
1042+
1043+
```voyd
1044+
storage_on_change(
1045+
key: "storage",
1046+
handler: (change: StorageChange) -> Msg =>
1047+
Msg::StorageChanged { key: change.key }
1048+
)
1049+
1050+
broadcast_channel<String, Msg>(
1051+
name: "updates",
1052+
handler: (value: String) -> Msg =>
1053+
Msg::Broadcast { value: value }
1054+
)
1055+
```
1056+
1057+
`StorageChange.key`, `old_value`, and `new_value` are optional because browser
1058+
storage events use `null` for clears, missing old values, and removals.
1059+
9671060
### Runtime Subscriptions
9681061

9691062
`Sub.runtime_payload(kind:, key:, handler:)` creates a host subscription
@@ -1312,10 +1405,14 @@ reports asynchronous listener failures with `phase: "subscriptions"`.
13121405
The built-in browser runtime host already handles:
13131406

13141407
- Commands: `delay`, `task`, `copy_to_clipboard`, `focus`,
1315-
`scroll_into_view`, `set_document_title`, `push_url`, `replace_url`,
1316-
`navigate_back`, `navigate_forward`.
1408+
`read_clipboard`, `scroll_into_view`, `select_text`, `set_document_title`,
1409+
`push_url`, `replace_url`, `set_hash`, `navigate_back`,
1410+
`navigate_forward`, `open_url`, `scroll_window_to`, `scroll_window_by`,
1411+
`local_storage_set`, `local_storage_remove`, `local_storage_clear`,
1412+
`session_storage_set`, `session_storage_remove`, `session_storage_clear`.
13171413
- Subscriptions: `interval`, `keyboard`, `online_status`, `window_resize`,
1318-
`visibility_change`.
1414+
`visibility_change`, `location_change`, `window_focus`, `window_blur`,
1415+
`animation_frame`, `media_query`, `storage`, `broadcast_channel`.
13191416

13201417
Lower-level renderer APIs are available for integration work:
13211418

0 commit comments

Comments
 (0)