Skip to content

Commit fd0fa42

Browse files
authored
Support callbacks in React (#10518)
1 parent cf1b1b2 commit fd0fa42

5 files changed

Lines changed: 68 additions & 12 deletions

File tree

crates/viewer/re_viewer/src/event.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
// NOTE: Any changes to the type definitions in this file must be replicated in:
77
// - rerun_js/web-viewer/index.ts (ViewerEvent)
88
// - rerun_py/rerun_sdk/rerun/event.py (ViewerEvent)
9+
// Important: The event names defined here are transformed to `snake_case` on the JS side.
910

1011
use std::rc::Rc;
1112

rerun_js/web-viewer-react/index.js

Lines changed: 36 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import * as rerun from "@rerun-io/web-viewer";
1515
* @typedef {(
1616
* Omit<import("@rerun-io/web-viewer").WebViewerOptions, "allow_fullscreen" | "enable_history">
1717
* & BaseProps
18+
* & import("./types.d.ts").ViewerEvents
1819
* )} Props
1920
*/
2021

@@ -43,7 +44,7 @@ export default class WebViewer extends React.Component {
4344
startViewer(
4445
this.#handle,
4546
/** @type {HTMLDivElement} */ (this.#parent.current),
46-
this.props,
47+
() => this.props,
4748
);
4849
}
4950

@@ -65,7 +66,7 @@ export default class WebViewer extends React.Component {
6566
startViewer(
6667
this.#handle,
6768
/** @type {HTMLDivElement} */ (this.#parent.current),
68-
this.props,
69+
() => this.props,
6970
);
7071
} else {
7172
// We only need to diff the recordings.
@@ -92,12 +93,33 @@ export default class WebViewer extends React.Component {
9293
}
9394
}
9495

96+
/** @param {string} str */
97+
function pascalToSnake(str) {
98+
let out = "";
99+
for (let i = 0; i < str.length; i++) {
100+
const code = str.charCodeAt(i);
101+
102+
// A–Z ?
103+
if (code >= 65 && code <= 90) {
104+
// if not first char, prepend underscore
105+
if (i > 0) out += "_";
106+
// convert to lowercase by adding 32
107+
out += String.fromCharCode(code + 32);
108+
} else {
109+
// everything else (a–z, 0–9, etc.) goes straight through
110+
out += String.fromCharCode(code);
111+
}
112+
}
113+
return out;
114+
}
115+
95116
/**
96117
* @param {rerun.WebViewer} handle
97118
* @param {HTMLElement} parent
98-
* @param {Props} props
119+
* @param {() => Props} getProps
99120
*/
100-
function startViewer(handle, parent, props) {
121+
function startViewer(handle, parent, getProps) {
122+
const props = getProps();
101123
handle.start(toArray(props.rrd), parent, {
102124
manifest_url: props.manifest_url,
103125
render_backend: props.render_backend,
@@ -108,6 +130,16 @@ function startViewer(handle, parent, props) {
108130
width: "100%",
109131
height: "100%",
110132
});
133+
134+
for (const key of Object.keys(props)) {
135+
if (key.startsWith("on")) {
136+
/** @type {any} */
137+
const event = pascalToSnake(key.slice(2));
138+
/** @type {any} */
139+
const callback = /** @type {any} */ (getProps())[key];
140+
handle.on(event, callback);
141+
}
142+
}
111143
}
112144

113145
/**

rerun_js/web-viewer-react/package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@
3737
"index.d.ts",
3838
"index.d.ts.map",
3939
"index.js",
40+
"types.d.ts",
4041
"package.json",
4142
"tsconfig.json"
4243
],
@@ -51,4 +52,4 @@
5152
"typedoc-github-theme": "^0.3.0",
5253
"typescript": "^5.2.2"
5354
}
54-
}
55+
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
2+
import type { WebViewerEvents } from "@rerun-io/web-viewer";
3+
4+
type PascalCase<S extends string> = S extends `${infer P1}_${infer P2}`
5+
? `${Capitalize<P1>}${PascalCase<P2>}`
6+
: Capitalize<S>;
7+
8+
type WithValue = {
9+
[K in keyof WebViewerEvents as WebViewerEvents[K] extends void ? never : `on${PascalCase<K>}`]?: (event: WebViewerEvents[K]) => void;
10+
11+
}
12+
13+
type WithoutValue = {
14+
[K in keyof WebViewerEvents as WebViewerEvents[K] extends void ? `on${PascalCase<K>}` : never]?: () => void;
15+
16+
}
17+
18+
export type ViewerEvents = WithValue & WithoutValue;
19+
20+
21+

rerun_js/web-viewer/index.ts

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,8 @@ export interface AppOptions extends WebViewerOptions {
126126
}
127127

128128
// Types are based on `crates/viewer/re_viewer/src/event.rs`.
129+
// Important: The event names defined here are `snake_case` versions
130+
// of their `PascalCase` counterparts on the Rust side.
129131
/** An event produced in the Viewer. */
130132
export type ViewerEvent =
131133
| PlayEvent
@@ -351,7 +353,7 @@ export class WebViewer {
351353
// for notebooks/gradio, we can avoid a whole layer
352354
// of serde by sending over the raw json directly,
353355
// which will be deserialized in Python instead
354-
this.#_dispatch_raw_event(event_json);
356+
this.#dispatch_raw_event(event_json);
355357

356358
// for JS users, we dispatch the parsed event
357359
let event: ViewerEvent = JSON.parse(event_json);
@@ -384,10 +386,9 @@ export class WebViewer {
384386
return;
385387
}
386388

387-
#_raw_events: Set<(event_json: string) => void> = new Set();
388-
389-
#_dispatch_raw_event(event_json: string) {
390-
for (const callback of this.#_raw_events) {
389+
#raw_events: Set<(event_json: string) => void> = new Set();
390+
#dispatch_raw_event(event_json: string) {
391+
for (const callback of this.#raw_events) {
391392
callback(event_json);
392393
}
393394
}
@@ -399,8 +400,8 @@ export class WebViewer {
399400
//
400401
// Do not change this without searching for grepping for usage!
401402
private _on_raw_event(callback: (event: string) => void): () => void {
402-
this.#_raw_events.add(callback);
403-
return () => this.#_raw_events.delete(callback);
403+
this.#raw_events.add(callback);
404+
return () => this.#raw_events.delete(callback);
404405
}
405406

406407
#event_map: Map<

0 commit comments

Comments
 (0)