From 9fbe3aceb269ebb522bdbe12ca8285d0e178b99e Mon Sep 17 00:00:00 2001 From: jprochazk Date: Tue, 8 Jul 2025 16:17:57 +0200 Subject: [PATCH 1/4] temp --- rerun_notebook/src/js/widget.css | 10 --- rerun_notebook/src/js/widget.ts | 61 +++++++------------ rerun_notebook/src/rerun_notebook/__init__.py | 10 +-- 3 files changed, 28 insertions(+), 53 deletions(-) diff --git a/rerun_notebook/src/js/widget.css b/rerun_notebook/src/js/widget.css index 0584150218d2..5c03b33847b6 100644 --- a/rerun_notebook/src/js/widget.css +++ b/rerun_notebook/src/js/widget.css @@ -1,13 +1,3 @@ -.rerun_notebook canvas { - width: 100%; - height: 100%; - - min-width: 200px; - min-height: 400px; - max-width: 960px; - max-height: 720px; -} - .rerun_notebook { margin: 0; padding: 0; diff --git a/rerun_notebook/src/js/widget.ts b/rerun_notebook/src/js/widget.ts index 6ecd6e80ae82..a0537f0b8083 100644 --- a/rerun_notebook/src/js/widget.ts +++ b/rerun_notebook/src/js/widget.ts @@ -15,8 +15,8 @@ const PANELS = ["top", "blueprint", "selection", "time"] as const; /* Specifies attributes defined with traitlets in ../rerun_notebook/__init__.py */ interface WidgetModel { - _width?: number; - _height?: number; + _width?: number | string; + _height?: number | string; _url?: string; _panel_states?: PanelStates; @@ -36,15 +36,15 @@ class ViewerWidget { channel: LogChannel | null = null; - constructor(model: AnyModel) { + constructor(model: AnyModel, el: HTMLElement) { this.url = model.get("_url"); model.on("change:_url", this.on_change_url); this.panel_states = model.get("_panel_states"); model.on("change:_panel_states", this.on_change_panel_states); - model.on("change:_width", (_, width) => this.on_resize(null, { width })); - model.on("change:_height", (_, height) => this.on_resize(null, { height })); + model.on("change:_width", (_, width) => this.on_resize(width, undefined)); + model.on("change:_height", (_, height) => this.on_resize(undefined, height)); model.on("msg:custom", this.on_custom_message); @@ -54,53 +54,37 @@ class ViewerWidget { this.viewer.on("ready", () => { this.channel = this.viewer.open_channel("temp"); - - this.on_resize(null, { - width: model.get("_width"), - height: model.get("_height"), - }); + this.on_change_panel_states(null, this.panel_states); model.send("ready"); }); - } - - async start(el: HTMLElement) { - await this.viewer.start(this.url ?? null, el, this.options); - this.on_change_panel_states(null, this.panel_states); + // `start` is asynchronous, but we don't need to await it + this.viewer.start(this.url ?? null, el, this.options); + // `on_resize` must be called after synchronous portion of `start` + this.on_resize(model.get("_width"), model.get("_height")); } stop() { this.viewer.stop(); } - on_resize = (_: unknown, new_size: { width?: number; height?: number }) => { + on_resize(width?: number | string, height?: number | string) { const canvas = this.viewer.canvas; if (!canvas) throw new Error("on_resize called before viewer ready"); - const MIN_WIDTH = 200; - const MIN_HEIGHT = 200; - - if (new_size.width) { - const newWidth = Math.max(new_size.width, MIN_WIDTH); - canvas.style.width = `${newWidth}px`; - canvas.style.minWidth = "none"; - canvas.style.maxWidth = "none"; - } else { - canvas.style.width = ""; - canvas.style.minWidth = ""; - canvas.style.maxWidth = ""; + if (typeof width === "string" && width === "auto") { + canvas.style.width = "100%"; + } else if (typeof width === "number") { + canvas.style.width = `${Math.max(200, width)}px`; } - if (new_size.height) { - const newHeight = Math.max(new_size.height, MIN_HEIGHT); - canvas.style.height = `${newHeight}px`; - canvas.style.minHeight = "none"; - canvas.style.maxHeight = "none"; - } else { - canvas.style.height = ""; - canvas.style.minHeight = ""; - canvas.style.maxHeight = ""; + if (typeof height === "string" && height === "auto") { + canvas.style.height = "auto"; + canvas.style.aspectRatio = "16 / 9"; + } else if (typeof height === "number") { + canvas.style.height = `${Math.max(200, height)}px`; + canvas.style.aspectRatio = ""; } }; @@ -193,8 +177,7 @@ class ViewerWidget { const render: Render = ({ model, el }) => { el.classList.add("rerun_notebook"); - let widget = new ViewerWidget(model); - widget.start(el); + let widget = new ViewerWidget(model, el); return () => widget.stop(); }; diff --git a/rerun_notebook/src/rerun_notebook/__init__.py b/rerun_notebook/src/rerun_notebook/__init__.py index 0bc72f41ca1e..8788c2989c98 100644 --- a/rerun_notebook/src/rerun_notebook/__init__.py +++ b/rerun_notebook/src/rerun_notebook/__init__.py @@ -140,6 +140,8 @@ def _ipython_display_(self) -> None: display(self._html) + + class Viewer(anywidget.AnyWidget): # type: ignore[misc] _esm = ESM_MOD _css = CSS_PATH @@ -151,8 +153,8 @@ class Viewer(anywidget.AnyWidget): # type: ignore[misc] # # Example: `set_time_ctrl` uses `self.send`, and the state of the timeline is exposed via Viewer events. - _width = traitlets.Int(allow_none=True).tag(sync=True) - _height = traitlets.Int(allow_none=True).tag(sync=True) + _width = traitlets.Union([traitlets.Int(), traitlets.Unicode()], allow_none=True).tag(sync=True) + _height = traitlets.Union([traitlets.Int(), traitlets.Unicode()], allow_none=True).tag(sync=True) _url = traitlets.Unicode(allow_none=True).tag(sync=True) @@ -173,8 +175,8 @@ class Viewer(anywidget.AnyWidget): # type: ignore[misc] def __init__( self, *, - width: int | None = None, - height: int | None = None, + width: int | Literal["auto"] | None = None, + height: int | Literal["auto"] | None = None, url: str | None = None, panel_states: Mapping[Panel, PanelState] | None = None, fallback_token: str | None = None, From ae97f2dfab2fbf1a0dd21ef15e82a5c82600ed12 Mon Sep 17 00:00:00 2001 From: jprochazk Date: Tue, 8 Jul 2025 17:44:55 +0200 Subject: [PATCH 2/4] implement auto-sizing --- rerun_notebook/src/js/widget.css | 6 ++ rerun_notebook/src/js/widget.ts | 72 +++++++++++-------- rerun_notebook/src/rerun_notebook/__init__.py | 8 +-- rerun_py/rerun_sdk/rerun/notebook.py | 4 +- 4 files changed, 56 insertions(+), 34 deletions(-) diff --git a/rerun_notebook/src/js/widget.css b/rerun_notebook/src/js/widget.css index 5c03b33847b6..7e143070802f 100644 --- a/rerun_notebook/src/js/widget.css +++ b/rerun_notebook/src/js/widget.css @@ -6,6 +6,12 @@ background: transparent !important; } +.rerun_notebook > div { + margin: 0; + padding: 0; + background: transparent !important; +} + div.cell-output-ipywidget-background { margin: 0; padding: 0; diff --git a/rerun_notebook/src/js/widget.ts b/rerun_notebook/src/js/widget.ts index a0537f0b8083..f1f3707043aa 100644 --- a/rerun_notebook/src/js/widget.ts +++ b/rerun_notebook/src/js/widget.ts @@ -15,24 +15,54 @@ const PANELS = ["top", "blueprint", "selection", "time"] as const; /* Specifies attributes defined with traitlets in ../rerun_notebook/__init__.py */ interface WidgetModel { - _width?: number | string; - _height?: number | string; + _width: number | string; + _height: number | string; _url?: string; _panel_states?: PanelStates; - _time_ctrl: [timeline: string | null, time: number | null, play: boolean]; - _recording_id?: string; _fallback_token?: string; } type Opt = T | null | undefined; +function _resize(el: HTMLElement, width: number | string, height: number | string) { + const style = el.style; + + if (typeof width === "string" && width === "auto") { + style.width = "100%"; + } else if (typeof width === "number") { + style.width = `${Math.max(200, width)}px`; + } else { + style.width = "640px"; + } + + if (typeof height === "string" && height === "auto") { + style.height = "auto"; + style.aspectRatio = "16 / 9"; + } else if (typeof height === "number") { + style.height = `${Math.max(200, height)}px`; + style.aspectRatio = ""; + } else { + style.height = "640px"; + style.aspectRatio = ""; + } +} + +function dbg(...args: any[]): boolean { + console.log(...args) + return true; +} + class ViewerWidget { viewer: WebViewer = new WebViewer(); url: Opt = null; panel_states: Opt = null; - options: WebViewerOptions = { hide_welcome_screen: true }; + options: WebViewerOptions = { + hide_welcome_screen: true, + width: "100%", + height: "100%", + }; channel: LogChannel | null = null; @@ -43,8 +73,8 @@ class ViewerWidget { this.panel_states = model.get("_panel_states"); model.on("change:_panel_states", this.on_change_panel_states); - model.on("change:_width", (_, width) => this.on_resize(width, undefined)); - model.on("change:_height", (_, height) => this.on_resize(undefined, height)); + model.on("change:_width", (_, width) => dbg("resize") && this.on_resize(el, width, model.get("_height"))); + model.on("change:_height", (_, height) => dbg("resize") && this.on_resize(el, model.get("_width"), height)); model.on("msg:custom", this.on_custom_message); @@ -59,33 +89,16 @@ class ViewerWidget { model.send("ready"); }); - // `start` is asynchronous, but we don't need to await it this.viewer.start(this.url ?? null, el, this.options); - // `on_resize` must be called after synchronous portion of `start` - this.on_resize(model.get("_width"), model.get("_height")); + this.on_resize(el, model.get("_width"), model.get("_height")); } stop() { this.viewer.stop(); } - on_resize(width?: number | string, height?: number | string) { - const canvas = this.viewer.canvas; - if (!canvas) throw new Error("on_resize called before viewer ready"); - - if (typeof width === "string" && width === "auto") { - canvas.style.width = "100%"; - } else if (typeof width === "number") { - canvas.style.width = `${Math.max(200, width)}px`; - } - - if (typeof height === "string" && height === "auto") { - canvas.style.height = "auto"; - canvas.style.aspectRatio = "16 / 9"; - } else if (typeof height === "number") { - canvas.style.height = `${Math.max(200, height)}px`; - canvas.style.aspectRatio = ""; - } + on_resize(parent: HTMLElement, width: number | string, height: number | string) { + _resize(parent, width, height) }; on_change_url = (_: unknown, new_url?: Opt) => { @@ -177,7 +190,10 @@ class ViewerWidget { const render: Render = ({ model, el }) => { el.classList.add("rerun_notebook"); - let widget = new ViewerWidget(model, el); + const container = document.createElement("div"); + el.append(container); + + let widget = new ViewerWidget(model, container); return () => widget.stop(); }; diff --git a/rerun_notebook/src/rerun_notebook/__init__.py b/rerun_notebook/src/rerun_notebook/__init__.py index 8788c2989c98..941fdc5bea61 100644 --- a/rerun_notebook/src/rerun_notebook/__init__.py +++ b/rerun_notebook/src/rerun_notebook/__init__.py @@ -153,8 +153,8 @@ class Viewer(anywidget.AnyWidget): # type: ignore[misc] # # Example: `set_time_ctrl` uses `self.send`, and the state of the timeline is exposed via Viewer events. - _width = traitlets.Union([traitlets.Int(), traitlets.Unicode()], allow_none=True).tag(sync=True) - _height = traitlets.Union([traitlets.Int(), traitlets.Unicode()], allow_none=True).tag(sync=True) + _width = traitlets.Union([traitlets.Int(), traitlets.Unicode()]).tag(sync=True) + _height = traitlets.Union([traitlets.Int(), traitlets.Unicode()]).tag(sync=True) _url = traitlets.Unicode(allow_none=True).tag(sync=True) @@ -175,8 +175,8 @@ class Viewer(anywidget.AnyWidget): # type: ignore[misc] def __init__( self, *, - width: int | Literal["auto"] | None = None, - height: int | Literal["auto"] | None = None, + width: int | Literal["auto"], + height: int | Literal["auto"], url: str | None = None, panel_states: Mapping[Panel, PanelState] | None = None, fallback_token: str | None = None, diff --git a/rerun_py/rerun_sdk/rerun/notebook.py b/rerun_py/rerun_sdk/rerun/notebook.py index 27357e4d8056..60bb26c82529 100644 --- a/rerun_py/rerun_sdk/rerun/notebook.py +++ b/rerun_py/rerun_sdk/rerun/notebook.py @@ -66,8 +66,8 @@ class Viewer: def __init__( self, *, - width: int | None = None, - height: int | None = None, + width: int | Literal["auto"] | None = None, + height: int | Literal["auto"] | None = None, url: str | None = None, blueprint: BlueprintLike | None = None, recording: RecordingStream | None = None, From 3c766ff066febe70abaead76e7f028dea62e507d Mon Sep 17 00:00:00 2001 From: jprochazk Date: Tue, 8 Jul 2025 17:45:14 +0200 Subject: [PATCH 3/4] fmt --- rerun_notebook/src/rerun_notebook/__init__.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/rerun_notebook/src/rerun_notebook/__init__.py b/rerun_notebook/src/rerun_notebook/__init__.py index 941fdc5bea61..ce097419944b 100644 --- a/rerun_notebook/src/rerun_notebook/__init__.py +++ b/rerun_notebook/src/rerun_notebook/__init__.py @@ -140,8 +140,6 @@ def _ipython_display_(self) -> None: display(self._html) - - class Viewer(anywidget.AnyWidget): # type: ignore[misc] _esm = ESM_MOD _css = CSS_PATH From 2d92205483e034b89007f2018460e8a8dcd129bc Mon Sep 17 00:00:00 2001 From: jprochazk Date: Tue, 8 Jul 2025 17:48:48 +0200 Subject: [PATCH 4/4] documentation --- rerun_py/rerun_sdk/rerun/notebook.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/rerun_py/rerun_sdk/rerun/notebook.py b/rerun_py/rerun_sdk/rerun/notebook.py index 60bb26c82529..7d9c7bea419a 100644 --- a/rerun_py/rerun_sdk/rerun/notebook.py +++ b/rerun_py/rerun_sdk/rerun/notebook.py @@ -83,10 +83,14 @@ def __init__( Parameters ---------- - width : int - The width of the viewer in pixels. - height : int - The height of the viewer in pixels. + width: + The width of the viewer in pixels, or "auto". + + When set to "auto", scales to 100% of the notebook cell's width. + height: + The height of the viewer in pixels, or "auto". + + When set to "auto", scales using a 16:9 aspect ratio with `width`. url: Optional URL passed to the viewer for displaying its contents. recording: