From a3fe6ad7d57a6c4ea64579ef669d1417155da1df Mon Sep 17 00:00:00 2001 From: cc Date: Fri, 12 Jun 2026 15:35:43 -0700 Subject: [PATCH 01/12] feat: add native web page view --- CONTEXT.md | 9 + Cargo.lock | 1135 ++++++++++++++++- Cargo.toml | 4 + .../rerun/blueprint/archetypes.fbs | 1 + .../archetypes/web_page_view_config.fbs | 19 + .../rerun/blueprint/components.fbs | 2 + .../components/show_navigation_controls.fbs | 14 + .../blueprint/components/web_page_url.fbs | 13 + .../definitions/rerun/blueprint/views.fbs | 1 + .../rerun/blueprint/views/web_page.fbs | 17 + .../src/blueprint/archetypes/.gitattributes | 1 + .../src/blueprint/archetypes/mod.rs | 2 + .../archetypes/web_page_view_config.rs | 215 ++++ .../src/blueprint/components/.gitattributes | 2 + .../src/blueprint/components/mod.rs | 4 + .../components/show_navigation_controls.rs | 86 ++ .../src/blueprint/components/web_page_url.rs | 86 ++ .../src/blueprint/views/.gitattributes | 1 + .../re_sdk_types/src/blueprint/views/mod.rs | 2 + .../src/blueprint/views/web_page_view.rs | 85 ++ .../store/re_sdk_types/src/reflection/mod.rs | 47 + crates/top/rerun-cli/Cargo.toml | 3 + crates/top/rerun/Cargo.toml | 3 + crates/top/rerun/src/commands/entrypoint.rs | 10 +- crates/viewer/re_view_web_page/Cargo.toml | 56 + crates/viewer/re_view_web_page/src/backend.rs | 213 ++++ crates/viewer/re_view_web_page/src/lib.rs | 12 + .../viewer/re_view_web_page/src/lifecycle.rs | 85 ++ .../re_view_web_page/src/native_backend.rs | 208 +++ crates/viewer/re_view_web_page/src/testing.rs | 234 ++++ .../viewer/re_view_web_page/src/url_policy.rs | 54 + .../viewer/re_view_web_page/src/view_class.rs | 230 ++++ .../re_view_web_page/tests/native_backend.rs | 14 + .../re_view_web_page/tests/web_page_view.rs | 494 +++++++ crates/viewer/re_viewer/Cargo.toml | 6 + crates/viewer/re_viewer/src/app.rs | 27 + .../src/blueprint/validation_gen/mod.rs | 4 + crates/viewer/re_viewer/src/default_views.rs | 5 + .../re_viewer_context/src/view/view_states.rs | 18 +- crates/viewer/re_viewport/src/viewport_ui.rs | 1 + docs/content/reference/types/views.md | 1 + .../reference/types/views/.gitattributes | 1 + .../reference/types/views/web_page_view.md | 30 + .../add-native-web-page-view/.openspec.yaml | 2 + .../add-native-web-page-view/design.md | 92 ++ .../add-native-web-page-view/proposal.md | 32 + .../specs/native-web-page-view/spec.md | 110 ++ .../changes/add-native-web-page-view/tasks.md | 61 + openspec/config.yaml | 20 + rerun_cpp/src/rerun/blueprint/archetypes.hpp | 1 + .../rerun/blueprint/archetypes/.gitattributes | 2 + .../archetypes/web_page_view_config.cpp | 65 + .../archetypes/web_page_view_config.hpp | 119 ++ rerun_cpp/src/rerun/blueprint/components.hpp | 2 + .../rerun/blueprint/components/.gitattributes | 2 + .../components/show_navigation_controls.hpp | 80 ++ .../blueprint/components/web_page_url.hpp | 75 ++ .../rerun_sdk/rerun/blueprint/__init__.py | 2 + .../rerun/blueprint/archetypes/.gitattributes | 1 + .../rerun/blueprint/archetypes/__init__.py | 2 + .../archetypes/web_page_view_config.py | 154 +++ .../rerun/blueprint/components/.gitattributes | 2 + .../rerun/blueprint/components/__init__.py | 6 + .../components/show_navigation_controls.py | 35 + .../blueprint/components/web_page_url.py | 35 + .../rerun/blueprint/views/.gitattributes | 1 + .../rerun/blueprint/views/__init__.py | 2 + .../rerun/blueprint/views/web_page_view.py | 106 ++ 68 files changed, 4397 insertions(+), 67 deletions(-) create mode 100644 CONTEXT.md create mode 100644 crates/store/re_sdk_types/definitions/rerun/blueprint/archetypes/web_page_view_config.fbs create mode 100644 crates/store/re_sdk_types/definitions/rerun/blueprint/components/show_navigation_controls.fbs create mode 100644 crates/store/re_sdk_types/definitions/rerun/blueprint/components/web_page_url.fbs create mode 100644 crates/store/re_sdk_types/definitions/rerun/blueprint/views/web_page.fbs create mode 100644 crates/store/re_sdk_types/src/blueprint/archetypes/web_page_view_config.rs create mode 100644 crates/store/re_sdk_types/src/blueprint/components/show_navigation_controls.rs create mode 100644 crates/store/re_sdk_types/src/blueprint/components/web_page_url.rs create mode 100644 crates/store/re_sdk_types/src/blueprint/views/web_page_view.rs create mode 100644 crates/viewer/re_view_web_page/Cargo.toml create mode 100644 crates/viewer/re_view_web_page/src/backend.rs create mode 100644 crates/viewer/re_view_web_page/src/lib.rs create mode 100644 crates/viewer/re_view_web_page/src/lifecycle.rs create mode 100644 crates/viewer/re_view_web_page/src/native_backend.rs create mode 100644 crates/viewer/re_view_web_page/src/testing.rs create mode 100644 crates/viewer/re_view_web_page/src/url_policy.rs create mode 100644 crates/viewer/re_view_web_page/src/view_class.rs create mode 100644 crates/viewer/re_view_web_page/tests/native_backend.rs create mode 100644 crates/viewer/re_view_web_page/tests/web_page_view.rs create mode 100644 docs/content/reference/types/views/web_page_view.md create mode 100644 openspec/changes/add-native-web-page-view/.openspec.yaml create mode 100644 openspec/changes/add-native-web-page-view/design.md create mode 100644 openspec/changes/add-native-web-page-view/proposal.md create mode 100644 openspec/changes/add-native-web-page-view/specs/native-web-page-view/spec.md create mode 100644 openspec/changes/add-native-web-page-view/tasks.md create mode 100644 openspec/config.yaml create mode 100644 rerun_cpp/src/rerun/blueprint/archetypes/web_page_view_config.cpp create mode 100644 rerun_cpp/src/rerun/blueprint/archetypes/web_page_view_config.hpp create mode 100644 rerun_cpp/src/rerun/blueprint/components/show_navigation_controls.hpp create mode 100644 rerun_cpp/src/rerun/blueprint/components/web_page_url.hpp create mode 100644 rerun_py/rerun_sdk/rerun/blueprint/archetypes/web_page_view_config.py create mode 100644 rerun_py/rerun_sdk/rerun/blueprint/components/show_navigation_controls.py create mode 100644 rerun_py/rerun_sdk/rerun/blueprint/components/web_page_url.py create mode 100644 rerun_py/rerun_sdk/rerun/blueprint/views/web_page_view.py diff --git a/CONTEXT.md b/CONTEXT.md new file mode 100644 index 000000000000..4acbe72d2923 --- /dev/null +++ b/CONTEXT.md @@ -0,0 +1,9 @@ +# Rerun Viewer + +Rerun Viewer is the user-facing application for inspecting logged multimodal data through configurable views. + +## Language + +**Web Page View**: +A native-only Rerun view that displays a configured webpage inline as part of the viewer layout. +_Avoid_: WebView, link view, browser view diff --git a/Cargo.lock b/Cargo.lock index d049c42e43aa..b0fe43b0ab0f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -133,8 +133,8 @@ dependencies = [ "accesskit_consumer", "hashbrown 0.16.1", "static_assertions", - "windows", - "windows-core", + "windows 0.62.2", + "windows-core 0.62.2", ] [[package]] @@ -863,6 +863,29 @@ dependencies = [ "loom", ] +[[package]] +name = "atk" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "241b621213072e993be4f6f3a9e4b45f65b7e6faad43001be957184b7bb1824b" +dependencies = [ + "atk-sys", + "glib", + "libc", +] + +[[package]] +name = "atk-sys" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c5e48b684b0ca77d2bbadeef17424c2ea3c897d44d566a1617e7e8f30614d086" +dependencies = [ + "glib-sys", + "gobject-sys", + "libc", + "system-deps", +] + [[package]] name = "atoi" version = "2.0.0" @@ -1024,7 +1047,7 @@ dependencies = [ "miniz_oxide", "object", "rustc-demangle", - "windows-link", + "windows-link 0.2.1", ] [[package]] @@ -1103,15 +1126,30 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "bit-set" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08807e080ed7f9d5433fa9b275196cfc35414f66a0c79d864dc51a0d825231a3" +dependencies = [ + "bit-vec 0.8.0", +] + [[package]] name = "bit-set" version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "34ddef2995421ab6a5c779542c81ee77c115206f4ad9d5a8e05f4ff49716a3dd" dependencies = [ - "bit-vec", + "bit-vec 0.9.1", ] +[[package]] +name = "bit-vec" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e764a1d40d510daf35e07be9eb06e75770908c27d411ee6c92109c9840eaaf7" + [[package]] name = "bit-vec" version = "0.9.1" @@ -1390,6 +1428,31 @@ dependencies = [ "walkdir", ] +[[package]] +name = "cairo-rs" +version = "0.18.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ca26ef0159422fb77631dc9d17b102f253b876fe1586b03b803e63a309b4ee2" +dependencies = [ + "bitflags 2.11.0", + "cairo-sys-rs", + "glib", + "libc", + "once_cell", + "thiserror 1.0.69", +] + +[[package]] +name = "cairo-sys-rs" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "685c9fa8e590b8b3d678873528d83411db17242a73fccaed827770ea0fedda51" +dependencies = [ + "glib-sys", + "libc", + "system-deps", +] + [[package]] name = "calloop" version = "0.13.0" @@ -1516,6 +1579,16 @@ dependencies = [ "uuid", ] +[[package]] +name = "cfg-expr" +version = "0.15.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d067ad48b8650848b989a59a86c6c36a995d02d2bf778d45c3c5d57bc2718f02" +dependencies = [ + "smallvec", + "target-lexicon 0.12.16", +] + [[package]] name = "cfg-if" version = "1.0.4" @@ -1548,7 +1621,7 @@ dependencies = [ "num-traits", "serde", "wasm-bindgen", - "windows-link", + "windows-link 0.2.1", ] [[package]] @@ -1631,7 +1704,7 @@ version = "4.5.55" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a92793da1a46a5f2a02a6f4c46c6496b28c43638adea8306fcb0caa1634f24e5" dependencies = [ - "heck", + "heck 0.5.0", "proc-macro2", "quote", "syn 2.0.117", @@ -2122,6 +2195,29 @@ dependencies = [ "typenum", ] +[[package]] +name = "cssparser" +version = "0.36.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dae61cf9c0abb83bd659dab65b7e4e38d8236824c85f0f804f173567bda257d2" +dependencies = [ + "cssparser-macros", + "dtoa-short", + "itoa", + "phf 0.13.1", + "smallvec", +] + +[[package]] +name = "cssparser-macros" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13b588ba4ac1a99f7f2964d24b3d896ddc6bf847ee3855dbd4366f058cfcd331" +dependencies = [ + "quote", + "syn 2.0.117", +] + [[package]] name = "csv" version = "1.3.1" @@ -3011,6 +3107,27 @@ dependencies = [ "serde_core", ] +[[package]] +name = "derive_more" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d751e9e49156b02b44f9c1815bcb94b984cdcc4396ecc32521c739452808b134" +dependencies = [ + "derive_more-impl", +] + +[[package]] +name = "derive_more-impl" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "799a97264921d8623a957f6c3b9011f3b5492f557bbb7a5a19b7fa6d06ba8dcb" +dependencies = [ + "proc-macro2", + "quote", + "rustc_version", + "syn 2.0.117", +] + [[package]] name = "devserver_lib" version = "0.4.2" @@ -3142,6 +3259,21 @@ dependencies = [ "litrs", ] +[[package]] +name = "dom_query" +version = "0.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "521e380c0c8afb8d9a1e83a1822ee03556fc3e3e7dbc1fd30be14e37f9cb3f89" +dependencies = [ + "bit-set 0.8.0", + "cssparser", + "foldhash 0.2.0", + "html5ever", + "precomputed-hash", + "selectors", + "tendril", +] + [[package]] name = "downcast-rs" version = "1.2.1" @@ -3166,6 +3298,21 @@ version = "1.0.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d6add3b8cff394282be81f3fc1a0605db594ed69890078ca6e2cab1c408bcf04" +[[package]] +name = "dtoa-short" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd1511a7b6a56299bd043a9c167a6d2bfb37bf84a6dfceaba651168adfb43c87" +dependencies = [ + "dtoa", +] + +[[package]] +name = "dunce" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813" + [[package]] name = "ecolor" version = "0.34.0" @@ -3384,7 +3531,7 @@ dependencies = [ "pollster", "serde", "tempfile", - "toml", + "toml 1.0.6+spec-1.1.0", "wgpu", ] @@ -3722,6 +3869,16 @@ dependencies = [ "anyhow", ] +[[package]] +name = "field-offset" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38e2275cc4e4fc009b0669731a1e5ab7ebf11f469eaede2bab9309a5b4d6057f" +dependencies = [ + "memoffset", + "rustc_version", +] + [[package]] name = "find-msvc-tools" version = "0.1.3" @@ -4010,6 +4167,91 @@ dependencies = [ "slab", ] +[[package]] +name = "gdk" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9f245958c627ac99d8e529166f9823fb3b838d1d41fd2b297af3075093c2691" +dependencies = [ + "cairo-rs", + "gdk-pixbuf", + "gdk-sys", + "gio", + "glib", + "libc", + "pango", +] + +[[package]] +name = "gdk-pixbuf" +version = "0.18.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50e1f5f1b0bfb830d6ccc8066d18db35c487b1b2b1e8589b5dfe9f07e8defaec" +dependencies = [ + "gdk-pixbuf-sys", + "gio", + "glib", + "libc", + "once_cell", +] + +[[package]] +name = "gdk-pixbuf-sys" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9839ea644ed9c97a34d129ad56d38a25e6756f99f3a88e15cd39c20629caf7" +dependencies = [ + "gio-sys", + "glib-sys", + "gobject-sys", + "libc", + "system-deps", +] + +[[package]] +name = "gdk-sys" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c2d13f38594ac1e66619e188c6d5a1adb98d11b2fcf7894fc416ad76aa2f3f7" +dependencies = [ + "cairo-sys-rs", + "gdk-pixbuf-sys", + "gio-sys", + "glib-sys", + "gobject-sys", + "libc", + "pango-sys", + "pkg-config", + "system-deps", +] + +[[package]] +name = "gdkx11" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3caa00e14351bebbc8183b3c36690327eb77c49abc2268dd4bd36b856db3fbfe" +dependencies = [ + "gdk", + "gdkx11-sys", + "gio", + "glib", + "libc", + "x11", +] + +[[package]] +name = "gdkx11-sys" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e2e7445fe01ac26f11601db260dd8608fe172514eb63b3b5e261ea6b0f4428d" +dependencies = [ + "gdk-sys", + "glib-sys", + "libc", + "system-deps", + "x11", +] + [[package]] name = "generational-arena" version = "0.2.9" @@ -4030,8 +4272,8 @@ dependencies = [ "libc", "log", "rustversion", - "windows-link", - "windows-result", + "windows-link 0.2.1", + "windows-result 0.4.1", ] [[package]] @@ -4125,6 +4367,38 @@ dependencies = [ "stable_deref_trait", ] +[[package]] +name = "gio" +version = "0.18.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4fc8f532f87b79cbc51a79748f16a6828fb784be93145a322fa14d06d354c73" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-util", + "gio-sys", + "glib", + "libc", + "once_cell", + "pin-project-lite", + "smallvec", + "thiserror 1.0.69", +] + +[[package]] +name = "gio-sys" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37566df850baf5e4cb0dfb78af2e4b9898d817ed9263d1090a2df958c64737d2" +dependencies = [ + "glib-sys", + "gobject-sys", + "libc", + "system-deps", + "winapi", +] + [[package]] name = "gl_generator" version = "0.14.0" @@ -4146,6 +4420,53 @@ dependencies = [ "serde_core", ] +[[package]] +name = "glib" +version = "0.18.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "233daaf6e83ae6a12a52055f568f9d7cf4671dabb78ff9560ab6da230ce00ee5" +dependencies = [ + "bitflags 2.11.0", + "futures-channel", + "futures-core", + "futures-executor", + "futures-task", + "futures-util", + "gio-sys", + "glib-macros", + "glib-sys", + "gobject-sys", + "libc", + "memchr", + "once_cell", + "smallvec", + "thiserror 1.0.69", +] + +[[package]] +name = "glib-macros" +version = "0.18.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bb0228f477c0900c880fd78c8759b95c7636dbd7842707f49e132378aa2acdc" +dependencies = [ + "heck 0.4.1", + "proc-macro-crate 2.0.2", + "proc-macro-error", + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "glib-sys" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "063ce2eb6a8d0ea93d2bf8ba1957e78dbab6be1c2220dd3daca57d5a9d869898" +dependencies = [ + "libc", + "system-deps", +] + [[package]] name = "glob" version = "0.3.3" @@ -4282,6 +4603,17 @@ dependencies = [ "gl_generator", ] +[[package]] +name = "gobject-sys" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0850127b514d1c4a4654ead6dedadb18198999985908e6ffe4436f53c785ce44" +dependencies = [ + "glib-sys", + "libc", + "system-deps", +] + [[package]] name = "gpu-allocator" version = "0.28.0" @@ -4293,7 +4625,7 @@ dependencies = [ "log", "presser", "thiserror 2.0.18", - "windows", + "windows 0.62.2", ] [[package]] @@ -4326,6 +4658,58 @@ dependencies = [ "rerun", ] +[[package]] +name = "gtk" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd56fb197bfc42bd5d2751f4f017d44ff59fbb58140c6b49f9b3b2bdab08506a" +dependencies = [ + "atk", + "cairo-rs", + "field-offset", + "futures-channel", + "gdk", + "gdk-pixbuf", + "gio", + "glib", + "gtk-sys", + "gtk3-macros", + "libc", + "pango", + "pkg-config", +] + +[[package]] +name = "gtk-sys" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f29a1c21c59553eb7dd40e918be54dccd60c52b049b75119d5d96ce6b624414" +dependencies = [ + "atk-sys", + "cairo-sys-rs", + "gdk-pixbuf-sys", + "gdk-sys", + "gio-sys", + "glib-sys", + "gobject-sys", + "libc", + "pango-sys", + "system-deps", +] + +[[package]] +name = "gtk3-macros" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52ff3c5b21f14f0736fed6dcfc0bfb4225ebf5725f3c0209edeec181e4d73e9d" +dependencies = [ + "proc-macro-crate 1.3.1", + "proc-macro-error", + "proc-macro2", + "quote", + "syn 2.0.117", +] + [[package]] name = "h2" version = "0.4.12" @@ -4401,6 +4785,12 @@ dependencies = [ "serde_core", ] +[[package]] +name = "heck" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" + [[package]] name = "heck" version = "0.5.0" @@ -4470,6 +4860,16 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "html5ever" +version = "0.38.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1054432bae2f14e0061e33d23402fbaa67a921d319d56adc6bcf887ddad1cbc2" +dependencies = [ + "log", + "markup5ever", +] + [[package]] name = "htmlescape" version = "0.3.1" @@ -4681,7 +5081,7 @@ dependencies = [ "js-sys", "log", "wasm-bindgen", - "windows-core", + "windows-core 0.62.2", ] [[package]] @@ -5027,6 +5427,29 @@ version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" +[[package]] +name = "javascriptcore-rs" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca5671e9ffce8ffba57afc24070e906da7fc4b1ba66f2cabebf61bf2ea257fcc" +dependencies = [ + "bitflags 1.3.2", + "glib", + "javascriptcore-rs-sys", +] + +[[package]] +name = "javascriptcore-rs-sys" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af1be78d14ffa4b75b66df31840478fef72b51f8c2465d4ca7c194da9f7a5124" +dependencies = [ + "glib-sys", + "gobject-sys", + "libc", + "system-deps", +] + [[package]] name = "jiff" version = "0.2.23" @@ -5786,7 +6209,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d7c4b02199fee7c5d21a5ae7d8cfa79a6ef5bb2fc834d6e9058e89c825efdc55" dependencies = [ "cfg-if", - "windows-link", + "windows-link 0.2.1", ] [[package]] @@ -6033,6 +6456,17 @@ dependencies = [ "serde", ] +[[package]] +name = "markup5ever" +version = "0.38.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8983d30f2915feeaaab2d6babdd6bc7e9ed1a00b66b5e6d74df19aa9c0e91862" +dependencies = [ + "log", + "tendril", + "web_atoms", +] + [[package]] name = "matchers" version = "0.2.0" @@ -6331,7 +6765,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "aa2630921705b9b01dcdd0b6864b9562ca3c1951eecd0f0c4f5f04f61e412647" dependencies = [ "arrayvec", - "bit-set", + "bit-set 0.9.1", "bitflags 2.11.0", "cfg-if", "cfg_aliases", @@ -6417,7 +6851,13 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c96aba5aa877601bb3f6dd6a63a969e1f82e60646e81e71b14496995e9853c91" [[package]] -name = "nix" +name = "new_debug_unreachable" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086" + +[[package]] +name = "nix" version = "0.30.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "74523f3a35e05aba87a1d978330aef40f67b0304ac79c1c00b294c9830543db6" @@ -6590,7 +7030,7 @@ version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "77e878c846a8abae00dd069496dbe8751b16ac1c3d6bd2a7283a938e8228f90d" dependencies = [ - "proc-macro-crate", + "proc-macro-crate 3.4.0", "proc-macro2", "quote", "syn 2.0.117", @@ -6635,6 +7075,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3a12a8ed07aefc768292f076dc3ac8c48f3781c8f2d5851dd3d98950e8c5a89f" dependencies = [ "objc2-encode", + "objc2-exception-helper", ] [[package]] @@ -6757,6 +7198,15 @@ version = "4.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ef25abbcd74fb2609453eb695bd2f860d389e457f67dc17cafc8b8cbc89d0c33" +[[package]] +name = "objc2-exception-helper" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7a1c5fbb72d7735b076bb47b578523aedc40f3c439bea6dfd595c089d79d98a" +dependencies = [ + "cc", +] + [[package]] name = "objc2-foundation" version = "0.2.2" @@ -6932,6 +7382,20 @@ dependencies = [ "objc2-foundation 0.2.2", ] +[[package]] +name = "objc2-web-kit" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2e5aaab980c433cf470df9d7af96a7b46a9d892d521a2cbbb2f8a4c16751e7f" +dependencies = [ + "bitflags 2.11.0", + "block2 0.6.2", + "objc2 0.6.4", + "objc2-app-kit 0.3.2", + "objc2-core-foundation", + "objc2-foundation 0.3.2", +] + [[package]] name = "object" version = "0.37.3" @@ -7167,6 +7631,31 @@ version = "3.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c1b04fb49957986fdce4d6ee7a65027d55d4b6d2265e5848bbb507b58ccfdb6f" +[[package]] +name = "pango" +version = "0.18.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ca27ec1eb0457ab26f3036ea52229edbdb74dee1edd29063f5b9b010e7ebee4" +dependencies = [ + "gio", + "glib", + "libc", + "once_cell", + "pango-sys", +] + +[[package]] +name = "pango-sys" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "436737e391a843e5933d6d9aa102cb126d501e815b83601365a948a518555dc5" +dependencies = [ + "glib-sys", + "gobject-sys", + "libc", + "system-deps", +] + [[package]] name = "parking" version = "2.2.1" @@ -7195,7 +7684,7 @@ dependencies = [ "petgraph 0.6.5", "redox_syscall 0.5.18", "smallvec", - "windows-link", + "windows-link 0.2.1", ] [[package]] @@ -7406,6 +7895,16 @@ dependencies = [ "serde", ] +[[package]] +name = "phf_codegen" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49aa7f9d80421bca176ca8dbfebe668cc7a2684708594ec9f3c0db0805d5d6e1" +dependencies = [ + "phf_generator 0.13.1", + "phf_shared 0.13.1", +] + [[package]] name = "phf_generator" version = "0.11.3" @@ -7672,6 +8171,12 @@ dependencies = [ "zerocopy 0.8.27", ] +[[package]] +name = "precomputed-hash" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "925383efa346730478fb4838dbe9137d2a47675ad789c546d150a6e1dd4ab31c" + [[package]] name = "presser" version = "0.3.1" @@ -7688,13 +8193,57 @@ dependencies = [ "syn 2.0.117", ] +[[package]] +name = "proc-macro-crate" +version = "1.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f4c021e1093a56626774e81216a4ce732a735e5bad4868a03f3ed65ca0c3919" +dependencies = [ + "once_cell", + "toml_edit 0.19.15", +] + +[[package]] +name = "proc-macro-crate" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b00f26d3400549137f92511a46ac1cd8ce37cb5598a96d382381458b992a5d24" +dependencies = [ + "toml_datetime 0.6.3", + "toml_edit 0.20.2", +] + [[package]] name = "proc-macro-crate" version = "3.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "219cb19e96be00ab2e37d6e299658a0cfa83e52429179969b0f0121b4ac46983" dependencies = [ - "toml_edit", + "toml_edit 0.23.6", +] + +[[package]] +name = "proc-macro-error" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" +dependencies = [ + "proc-macro-error-attr", + "proc-macro2", + "quote", + "syn 1.0.109", + "version_check", +] + +[[package]] +name = "proc-macro-error-attr" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" +dependencies = [ + "proc-macro2", + "quote", + "version_check", ] [[package]] @@ -7765,7 +8314,7 @@ version = "0.14.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "343d3bd7056eda839b03204e68deff7d1b13aba7af2b2fd16890697274262ee7" dependencies = [ - "heck", + "heck 0.5.0", "itertools 0.14.0", "log", "multimap", @@ -7886,7 +8435,7 @@ version = "0.26.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4fc6ddaf24947d12a9aa31ac65431fb1b851b8f4365426e182901eabfb87df5f" dependencies = [ - "target-lexicon", + "target-lexicon 0.13.3", ] [[package]] @@ -7917,7 +8466,7 @@ version = "0.26.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "100246c0ecf400b475341b8455a9213344569af29a3c841d29270e53102e0fcf" dependencies = [ - "heck", + "heck 0.5.0", "proc-macro2", "pyo3-build-config", "quote", @@ -8728,7 +9277,7 @@ dependencies = [ "serde", "serde_json", "tempfile", - "toml", + "toml 1.0.6+spec-1.1.0", "ureq 3.3.0", "url", "wasm-bindgen-cli-support", @@ -9496,7 +10045,7 @@ dependencies = [ "wasm-bindgen", "web-sys", "wgpu", - "windows-core", + "windows-core 0.62.2", ] [[package]] @@ -9924,7 +10473,7 @@ dependencies = [ "serde", "syn 2.0.117", "tempfile", - "toml", + "toml 1.0.6+spec-1.1.0", "unindent", "xshell", ] @@ -10015,7 +10564,7 @@ name = "re_video" version = "0.32.0-alpha.1" dependencies = [ "ahash", - "bit-vec", + "bit-vec 0.9.1", "bytemuck", "cfg_aliases", "criterion", @@ -10346,6 +10895,30 @@ dependencies = [ "vec1", ] +[[package]] +name = "re_view_web_page" +version = "0.32.0-alpha.1" +dependencies = [ + "ahash", + "eframe", + "egui", + "egui_kittest", + "gtk", + "parking_lot", + "raw-window-handle", + "re_log_types", + "re_sdk_types", + "re_test_context", + "re_test_viewport", + "re_tracing", + "re_ui", + "re_viewer_context", + "re_viewport_blueprint", + "scoped-tls", + "url", + "wry", +] + [[package]] name = "re_viewer" version = "0.32.0-alpha.1" @@ -10428,6 +11001,7 @@ dependencies = [ "re_view_text_document", "re_view_text_log", "re_view_time_series", + "re_view_web_page", "re_viewer_context", "re_viewport", "re_viewport_blueprint", @@ -10660,7 +11234,7 @@ dependencies = [ "cfg-if", "libc", "rustix 1.1.4", - "windows", + "windows 0.62.2", ] [[package]] @@ -11275,6 +11849,25 @@ dependencies = [ "libc", ] +[[package]] +name = "selectors" +version = "0.36.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c5d9c0c92a92d33f08817311cf3f2c29a3538a8240e94a6a3c622ce652d7e00c" +dependencies = [ + "bitflags 2.11.0", + "cssparser", + "derive_more", + "log", + "new_debug_unreachable", + "phf 0.13.1", + "phf_codegen", + "precomputed-hash", + "rustc-hash 2.1.1", + "servo_arc", + "smallvec", +] + [[package]] name = "self_cell" version = "1.2.2" @@ -11395,6 +11988,15 @@ dependencies = [ "syn 2.0.117", ] +[[package]] +name = "serde_spanned" +version = "0.6.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf41e0cfaf7226dca15e8197172c295a782857fcb97fad1808a166870dee75a3" +dependencies = [ + "serde", +] + [[package]] name = "serde_spanned" version = "1.0.4" @@ -11416,6 +12018,15 @@ dependencies = [ "serde", ] +[[package]] +name = "servo_arc" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "170fb83ab34de17dc69aa7c67482b22218ddb85da56546f9bd6b929e32a05930" +dependencies = [ + "stable_deref_trait", +] + [[package]] name = "sha-1" version = "0.10.1" @@ -11646,7 +12257,7 @@ version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "54254b8531cafa275c5e096f62d48c81435d1015405a91198ddb11e967301d40" dependencies = [ - "heck", + "heck 0.5.0", "proc-macro2", "quote", "syn 2.0.117", @@ -11692,6 +12303,32 @@ dependencies = [ "windows-sys 0.59.0", ] +[[package]] +name = "soup3" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "471f924a40f31251afc77450e781cb26d55c0b650842efafc9c6cbd2f7cc4f9f" +dependencies = [ + "futures-channel", + "gio", + "glib", + "libc", + "soup3-sys", +] + +[[package]] +name = "soup3-sys" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ebe8950a680a12f24f15ebe1bf70db7af98ad242d9db43596ad3108aab86c27" +dependencies = [ + "gio-sys", + "glib-sys", + "gobject-sys", + "libc", + "system-deps", +] + [[package]] name = "spawn_viewer" version = "0.32.0-alpha.1" @@ -11803,6 +12440,30 @@ dependencies = [ "float-cmp 0.9.0", ] +[[package]] +name = "string_cache" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a18596f8c785a729f2819c0f6a7eae6ebeebdfffbfe4214ae6b087f690e31901" +dependencies = [ + "new_debug_unreachable", + "parking_lot", + "phf_shared 0.13.1", + "precomputed-hash", +] + +[[package]] +name = "string_cache_codegen" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "585635e46db231059f76c5849798146164652513eb9e8ab2685939dd90f29b69" +dependencies = [ + "phf_generator 0.13.1", + "phf_shared 0.13.1", + "proc-macro2", + "quote", +] + [[package]] name = "strsim" version = "0.11.1" @@ -11824,7 +12485,7 @@ version = "0.26.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4c6bee85a5a24955dc440386795aa378cd9cf82acd5f764469152d2270e581be" dependencies = [ - "heck", + "heck 0.5.0", "proc-macro2", "quote", "rustversion", @@ -11906,7 +12567,20 @@ dependencies = [ "ntapi", "objc2-core-foundation", "objc2-io-kit", - "windows", + "windows 0.62.2", +] + +[[package]] +name = "system-deps" +version = "6.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3e535eb8dded36d55ec13eddacd30dec501792ff23a0b1682c38601b8cf2349" +dependencies = [ + "cfg-expr", + "heck 0.5.0", + "pkg-config", + "toml 0.8.2", + "version-compare", ] [[package]] @@ -12061,12 +12735,29 @@ dependencies = [ "serde", ] +[[package]] +name = "tao-macros" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4e16beb8b2ac17db28eab8bca40e62dbfbb34c0fcdc6d9826b11b7b5d047dfd" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + [[package]] name = "tap" version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" +[[package]] +name = "target-lexicon" +version = "0.12.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61c41af27dd6d1e27b1b16b489db798443478cef1f06a660c96db617ba5de3b1" + [[package]] name = "target-lexicon" version = "0.13.3" @@ -12093,6 +12784,16 @@ dependencies = [ "rerun", ] +[[package]] +name = "tendril" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4790fc369d5a530f4b544b094e31388b9b3a37c0f4652ade4505945f5660d24" +dependencies = [ + "new_debug_unreachable", + "utf-8", +] + [[package]] name = "termcolor" version = "1.4.1" @@ -12435,6 +13136,18 @@ dependencies = [ "tokio", ] +[[package]] +name = "toml" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "185d8ab0dfbb35cf1399a6344d8484209c088f75f8f68230da55d48d95d43e3d" +dependencies = [ + "serde", + "serde_spanned 0.6.9", + "toml_datetime 0.6.3", + "toml_edit 0.20.2", +] + [[package]] name = "toml" version = "1.0.6+spec-1.1.0" @@ -12443,11 +13156,20 @@ checksum = "399b1124a3c9e16766831c6bba21e50192572cdd98706ea114f9502509686ffc" dependencies = [ "indexmap", "serde_core", - "serde_spanned", + "serde_spanned 1.0.4", "toml_datetime 1.0.0+spec-1.1.0", "toml_parser", "toml_writer", - "winnow", + "winnow 0.7.13", +] + +[[package]] +name = "toml_datetime" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cda73e2f1397b1262d6dfdcef8aafae14d1de7748d66822d3bfeeb6d03e5e4b" +dependencies = [ + "serde", ] [[package]] @@ -12468,6 +13190,30 @@ dependencies = [ "serde_core", ] +[[package]] +name = "toml_edit" +version = "0.19.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b5bb770da30e5cbfde35a2d7b9b8a2c4b8ef89548a7a6aeab5c9a576e3e7421" +dependencies = [ + "indexmap", + "toml_datetime 0.6.3", + "winnow 0.5.40", +] + +[[package]] +name = "toml_edit" +version = "0.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "396e4d48bbb2b7554c944bde63101b5ae446cff6ec4a24227428f15eb72ef338" +dependencies = [ + "indexmap", + "serde", + "serde_spanned 0.6.9", + "toml_datetime 0.6.3", + "winnow 0.5.40", +] + [[package]] name = "toml_edit" version = "0.23.6" @@ -12477,7 +13223,7 @@ dependencies = [ "indexmap", "toml_datetime 0.7.5+spec-1.1.0", "toml_parser", - "winnow", + "winnow 0.7.13", ] [[package]] @@ -12486,7 +13232,7 @@ version = "1.0.9+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "702d4415e08923e7e1ef96cd5727c0dfed80b4d2fa25db9647fe5eb6f7c5a4c4" dependencies = [ - "winnow", + "winnow 0.7.13", ] [[package]] @@ -12779,7 +13525,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c5f7c95348f20c1c913d72157b3c6dee6ea3e30b3d19502c5a7f6d3f160dacbf" dependencies = [ "cc", - "windows-targets 0.48.5", + "windows-targets 0.52.6", ] [[package]] @@ -13119,6 +13865,12 @@ dependencies = [ "vello_common", ] +[[package]] +name = "version-compare" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03c2856837ef78f57382f06b2b8563a2f512f7185d732608fd9176cb3b8edf0e" + [[package]] name = "version_check" version = "0.9.5" @@ -13189,7 +13941,7 @@ version = "0.26.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1a9b0525d7ea6e5f906aca581a172e5c91b4c595290dfa8ad4a2bc9ffef33b44" dependencies = [ - "heck", + "heck 0.5.0", "proc-macro2", "quote", "syn 2.0.117", @@ -13500,6 +14252,18 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "web_atoms" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7cff6eef815df1834fd250e3a2ff436044d82a9f1bc1980ca1dbdf07effc538" +dependencies = [ + "phf 0.13.1", + "phf_codegen", + "string_cache", + "string_cache_codegen", +] + [[package]] name = "webbrowser" version = "1.1.0" @@ -13516,6 +14280,50 @@ dependencies = [ "web-sys", ] +[[package]] +name = "webkit2gtk" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1027150013530fb2eaf806408df88461ae4815a45c541c8975e61d6f2fc4793" +dependencies = [ + "bitflags 1.3.2", + "cairo-rs", + "gdk", + "gdk-sys", + "gio", + "gio-sys", + "glib", + "glib-sys", + "gobject-sys", + "gtk", + "gtk-sys", + "javascriptcore-rs", + "libc", + "once_cell", + "soup3", + "webkit2gtk-sys", +] + +[[package]] +name = "webkit2gtk-sys" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "916a5f65c2ef0dfe12fff695960a2ec3d4565359fdbb2e9943c974e06c734ea5" +dependencies = [ + "bitflags 1.3.2", + "cairo-sys-rs", + "gdk-sys", + "gio-sys", + "glib-sys", + "gobject-sys", + "gtk-sys", + "javascriptcore-rs-sys", + "libc", + "pkg-config", + "soup3-sys", + "system-deps", +] + [[package]] name = "webpki-roots" version = "0.26.11" @@ -13534,6 +14342,42 @@ dependencies = [ "rustls-pki-types", ] +[[package]] +name = "webview2-com" +version = "0.38.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7130243a7a5b33c54a444e54842e6a9e133de08b5ad7b5861cd8ed9a6a5bc96a" +dependencies = [ + "webview2-com-macros", + "webview2-com-sys", + "windows 0.61.3", + "windows-core 0.61.2", + "windows-implement", + "windows-interface", +] + +[[package]] +name = "webview2-com-macros" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67a921c1b6914c367b2b823cd4cde6f96beec77d30a939c8199bb377cf9b9b54" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "webview2-com-sys" +version = "0.38.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "381336cfffd772377d291702245447a5251a2ffa5bad679c99e61bc48bacbf9c" +dependencies = [ + "thiserror 2.0.18", + "windows 0.61.3", + "windows-core 0.61.2", +] + [[package]] name = "weezl" version = "0.1.10" @@ -13577,8 +14421,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e80ac6cf1895df6342f87d975162108f9d98772a0d74bc404ab7304ac29469e" dependencies = [ "arrayvec", - "bit-set", - "bit-vec", + "bit-set 0.9.1", + "bit-vec 0.9.1", "bitflags 2.11.0", "bytemuck", "cfg_aliases", @@ -13649,7 +14493,7 @@ dependencies = [ "android_system_properties", "arrayvec", "ash", - "bit-set", + "bit-set 0.9.1", "bitflags 2.11.0", "block2 0.6.2", "bytemuck", @@ -13689,8 +14533,8 @@ dependencies = [ "web-sys", "wgpu-naga-bridge", "wgpu-types", - "windows", - "windows-core", + "windows 0.62.2", + "windows-core 0.62.2", ] [[package]] @@ -13754,16 +14598,38 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" +[[package]] +name = "windows" +version = "0.61.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9babd3a767a4c1aef6900409f85f5d53ce2544ccdfaa86dad48c91782c6d6893" +dependencies = [ + "windows-collections 0.2.0", + "windows-core 0.61.2", + "windows-future 0.2.1", + "windows-link 0.1.3", + "windows-numerics 0.2.0", +] + [[package]] name = "windows" version = "0.62.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "527fadee13e0c05939a6a05d5bd6eec6cd2e3dbd648b9f8e447c6518133d8580" dependencies = [ - "windows-collections", - "windows-core", - "windows-future", - "windows-numerics", + "windows-collections 0.3.2", + "windows-core 0.62.2", + "windows-future 0.3.2", + "windows-numerics 0.3.1", +] + +[[package]] +name = "windows-collections" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3beeceb5e5cfd9eb1d76b381630e82c4241ccd0d27f1a39ed41b2760b255c5e8" +dependencies = [ + "windows-core 0.61.2", ] [[package]] @@ -13772,7 +14638,20 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "23b2d95af1a8a14a3c7367e1ed4fc9c20e0a26e79551b1454d72583c97cc6610" dependencies = [ - "windows-core", + "windows-core 0.62.2", +] + +[[package]] +name = "windows-core" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0fdd3ddb90610c7638aa2b3a3ab2904fb9e5cdbecc643ddb3647212781c4ae3" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-link 0.1.3", + "windows-result 0.3.4", + "windows-strings 0.4.2", ] [[package]] @@ -13783,9 +14662,20 @@ checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" dependencies = [ "windows-implement", "windows-interface", - "windows-link", - "windows-result", - "windows-strings", + "windows-link 0.2.1", + "windows-result 0.4.1", + "windows-strings 0.5.1", +] + +[[package]] +name = "windows-future" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc6a41e98427b19fe4b73c550f060b59fa592d7d686537eebf9385621bfbad8e" +dependencies = [ + "windows-core 0.61.2", + "windows-link 0.1.3", + "windows-threading 0.1.0", ] [[package]] @@ -13794,9 +14684,9 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e1d6f90251fe18a279739e78025bd6ddc52a7e22f921070ccdc67dde84c605cb" dependencies = [ - "windows-core", - "windows-link", - "windows-threading", + "windows-core 0.62.2", + "windows-link 0.2.1", + "windows-threading 0.2.1", ] [[package]] @@ -13821,20 +14711,45 @@ dependencies = [ "syn 2.0.117", ] +[[package]] +name = "windows-link" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a" + [[package]] name = "windows-link" version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" +[[package]] +name = "windows-numerics" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9150af68066c4c5c07ddc0ce30421554771e528bde427614c61038bc2c92c2b1" +dependencies = [ + "windows-core 0.61.2", + "windows-link 0.1.3", +] + [[package]] name = "windows-numerics" version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6e2e40844ac143cdb44aead537bbf727de9b044e107a0f1220392177d15b0f26" dependencies = [ - "windows-core", - "windows-link", + "windows-core 0.62.2", + "windows-link 0.2.1", +] + +[[package]] +name = "windows-result" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56f42bd332cc6c8eac5af113fc0c1fd6a8fd2aa08a0119358686e5160d0586c6" +dependencies = [ + "windows-link 0.1.3", ] [[package]] @@ -13843,7 +14758,16 @@ version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" dependencies = [ - "windows-link", + "windows-link 0.2.1", +] + +[[package]] +name = "windows-strings" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56e6c93f3a0c3b36176cb1327a4958a0353d5d166c2a35cb268ace15e91d3b57" +dependencies = [ + "windows-link 0.1.3", ] [[package]] @@ -13852,7 +14776,7 @@ version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" dependencies = [ - "windows-link", + "windows-link 0.2.1", ] [[package]] @@ -13897,7 +14821,7 @@ version = "0.61.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" dependencies = [ - "windows-link", + "windows-link 0.2.1", ] [[package]] @@ -13952,7 +14876,7 @@ version = "0.53.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" dependencies = [ - "windows-link", + "windows-link 0.2.1", "windows_aarch64_gnullvm 0.53.1", "windows_aarch64_msvc 0.53.1", "windows_i686_gnu 0.53.1", @@ -13963,13 +14887,31 @@ dependencies = [ "windows_x86_64_msvc 0.53.1", ] +[[package]] +name = "windows-threading" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b66463ad2e0ea3bbf808b7f1d371311c80e115c0b71d60efc142cafbcfb057a6" +dependencies = [ + "windows-link 0.1.3", +] + [[package]] name = "windows-threading" version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3949bd5b99cafdf1c7ca86b43ca564028dfe27d66958f2470940f73d86d75b37" dependencies = [ - "windows-link", + "windows-link 0.2.1", +] + +[[package]] +name = "windows-version" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4060a1da109b9d0326b7262c8e12c84df67cc0dbc9e33cf49e01ccc2eb63631" +dependencies = [ + "windows-link 0.2.1", ] [[package]] @@ -14203,6 +15145,15 @@ dependencies = [ "xkbcommon-dl", ] +[[package]] +name = "winnow" +version = "0.5.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f593a95398737aeed53e489c785df13f3618e41dbcd6718c6addbf1395aa6876" +dependencies = [ + "memchr", +] + [[package]] name = "winnow" version = "0.7.13" @@ -14234,7 +15185,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc" dependencies = [ "anyhow", - "heck", + "heck 0.5.0", "wit-parser", ] @@ -14245,7 +15196,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21" dependencies = [ "anyhow", - "heck", + "heck 0.5.0", "indexmap", "prettyplease", "syn 2.0.117", @@ -14312,6 +15263,50 @@ version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ea2f10b9bb0928dfb1b42b65e1f9e36f7f54dbdf08457afefb38afcdec4fa2bb" +[[package]] +name = "wry" +version = "0.55.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "186f9871daa55fd9c016578b810d149de58367113db7fb72b462d2323ce19514" +dependencies = [ + "base64 0.22.1", + "block2 0.6.2", + "cookie", + "crossbeam-channel", + "dirs", + "dom_query", + "dpi", + "dunce", + "gdkx11", + "gtk", + "http", + "javascriptcore-rs", + "jni", + "libc", + "ndk", + "objc2 0.6.4", + "objc2-app-kit 0.3.2", + "objc2-core-foundation", + "objc2-foundation 0.3.2", + "objc2-ui-kit 0.3.2", + "objc2-web-kit", + "once_cell", + "percent-encoding", + "raw-window-handle", + "sha2", + "soup3", + "tao-macros", + "thiserror 2.0.18", + "url", + "webkit2gtk", + "webkit2gtk-sys", + "webview2-com", + "windows 0.61.3", + "windows-core 0.61.2", + "windows-version", + "x11-dl", +] + [[package]] name = "wyz" version = "0.5.1" @@ -14321,6 +15316,16 @@ dependencies = [ "tap", ] +[[package]] +name = "x11" +version = "2.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "502da5464ccd04011667b11c435cb992822c2c0dbde1770c988480d312a0db2e" +dependencies = [ + "libc", + "pkg-config", +] + [[package]] name = "x11-dl" version = "2.21.0" @@ -14462,7 +15467,7 @@ dependencies = [ "tracing", "uds_windows", "windows-sys 0.60.2", - "winnow", + "winnow 0.7.13", "zbus_macros", "zbus_names", "zvariant", @@ -14498,7 +15503,7 @@ version = "5.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "57e797a9c847ed3ccc5b6254e8bcce056494b375b511b3d6edcec0aeb4defaca" dependencies = [ - "proc-macro-crate", + "proc-macro-crate 3.4.0", "proc-macro2", "quote", "syn 2.0.117", @@ -14515,7 +15520,7 @@ checksum = "7be68e64bf6ce8db94f63e72f0c7eb9a60d733f7e0499e628dfab0f84d6bcb97" dependencies = [ "serde", "static_assertions", - "winnow", + "winnow 0.7.13", "zvariant", ] @@ -14735,7 +15740,7 @@ dependencies = [ "endi", "enumflags2", "serde", - "winnow", + "winnow 0.7.13", "zvariant_derive", "zvariant_utils", ] @@ -14746,7 +15751,7 @@ version = "5.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6643fd0b26a46d226bd90d3f07c1b5321fe9bb7f04673cb37ac6d6883885b68e" dependencies = [ - "proc-macro-crate", + "proc-macro-crate 3.4.0", "proc-macro2", "quote", "syn 2.0.117", @@ -14763,5 +15768,5 @@ dependencies = [ "quote", "serde", "syn 2.0.117", - "winnow", + "winnow 0.7.13", ] diff --git a/Cargo.toml b/Cargo.toml index 6e7969a460e3..e8a17eff4a01 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -155,6 +155,7 @@ re_view_tensor = { path = "crates/viewer/re_view_tensor", version = "=0.32.0-alp re_view_text_document = { path = "crates/viewer/re_view_text_document", version = "=0.32.0-alpha.1", default-features = false } re_view_text_log = { path = "crates/viewer/re_view_text_log", version = "=0.32.0-alpha.1", default-features = false } re_view_time_series = { path = "crates/viewer/re_view_time_series", version = "=0.32.0-alpha.1", default-features = false } +re_view_web_page = { path = "crates/viewer/re_view_web_page", version = "=0.32.0-alpha.1", default-features = false } re_viewer = { path = "crates/viewer/re_viewer", version = "=0.32.0-alpha.1", default-features = false } re_viewer_context = { path = "crates/viewer/re_viewer_context", version = "=0.32.0-alpha.1", default-features = false } re_viewport = { path = "crates/viewer/re_viewport", version = "=0.32.0-alpha.1", default-features = false } @@ -360,6 +361,7 @@ roxmltree = "0.20.0" ring = "0.17.14" rustls = { version = "0.23.37", default-features = false } saturating_cast = "0.1" +scoped-tls = "1.0" scuffle-av1 = "0.1.4" scuffle-bytes-util = "0.1.5" semver = "1.0.27" @@ -439,7 +441,9 @@ webbrowser = "1.1" windows-core = { version = "0.62", default-features = false, features = [ "std", ] } # Ensure `std` is enabled so `windows-result::Error` impls `core::error::Error` (needed by wgpu-hal gles on Windows) +gtk = "0.18.2" winit = { version = "0.30.13", default-features = false } +wry = { version = "0.55.1", default-features = false } # TODO(andreas): Try to get rid of `fragile-send-sync-non-atomic-wasm`. This requires re_renderer being aware of single-thread restriction on resources. # See also https://gpuweb.github.io/gpuweb/explainer/#multithreading-transfer (unsolved part of the Spec as of writing!) wgpu = { version = "29.0.1", default-features = false, features = [ diff --git a/crates/store/re_sdk_types/definitions/rerun/blueprint/archetypes.fbs b/crates/store/re_sdk_types/definitions/rerun/blueprint/archetypes.fbs index 63519067e802..c3d5741e8dca 100644 --- a/crates/store/re_sdk_types/definitions/rerun/blueprint/archetypes.fbs +++ b/crates/store/re_sdk_types/definitions/rerun/blueprint/archetypes.fbs @@ -32,3 +32,4 @@ include "./archetypes/viewport_blueprint.fbs"; include "./archetypes/visible_time_ranges.fbs"; include "./archetypes/visual_bounds2d.fbs"; include "./archetypes/visualizer_instruction.fbs"; +include "./archetypes/web_page_view_config.fbs"; diff --git a/crates/store/re_sdk_types/definitions/rerun/blueprint/archetypes/web_page_view_config.fbs b/crates/store/re_sdk_types/definitions/rerun/blueprint/archetypes/web_page_view_config.fbs new file mode 100644 index 000000000000..12e6a16ff5f2 --- /dev/null +++ b/crates/store/re_sdk_types/definitions/rerun/blueprint/archetypes/web_page_view_config.fbs @@ -0,0 +1,19 @@ +namespace rerun.blueprint.archetypes; + +/// Configuration for the Web Page View. +table WebPageViewConfig ( + "attr.docs.view_types": "WebPageView", + "attr.rerun.scope": "blueprint" +) { + // --- Required --- + + /// The initial URL to load. + url: rerun.blueprint.components.WebPageUrl ("attr.rerun.component_required", order: 100); + + // --- Optional --- + + /// Whether browser navigation controls should be shown. + /// + /// Defaults to true. + show_navigation_controls: rerun.blueprint.components.ShowNavigationControls ("attr.rerun.component_optional", nullable, order: 200); +} diff --git a/crates/store/re_sdk_types/definitions/rerun/blueprint/components.fbs b/crates/store/re_sdk_types/definitions/rerun/blueprint/components.fbs index 96b173a9a360..d78307f5cff3 100644 --- a/crates/store/re_sdk_types/definitions/rerun/blueprint/components.fbs +++ b/crates/store/re_sdk_types/definitions/rerun/blueprint/components.fbs @@ -36,6 +36,7 @@ include "./components/query_expression.fbs"; include "./components/root_container.fbs"; include "./components/row_share.fbs"; include "./components/selected_columns.fbs"; +include "./components/show_navigation_controls.fbs"; include "./components/tensor_dimension_index_slider.fbs"; include "./components/text_log_column.fbs"; include "./components/time_int.fbs"; @@ -52,4 +53,5 @@ include "./components/visual_bounds2d.fbs"; include "./components/visualizer_component_mapping.fbs"; include "./components/visualizer_instruction_id.fbs"; include "./components/visualizer_type.fbs"; +include "./components/web_page_url.fbs"; include "./components/zoom_level.fbs"; diff --git a/crates/store/re_sdk_types/definitions/rerun/blueprint/components/show_navigation_controls.fbs b/crates/store/re_sdk_types/definitions/rerun/blueprint/components/show_navigation_controls.fbs new file mode 100644 index 000000000000..38ea5bd4e84b --- /dev/null +++ b/crates/store/re_sdk_types/definitions/rerun/blueprint/components/show_navigation_controls.fbs @@ -0,0 +1,14 @@ + +namespace rerun.blueprint.components; + +/// Whether browser navigation controls should be shown in the Web Page View. +struct ShowNavigationControls ( + "attr.arrow.transparent", + "attr.rerun.scope": "blueprint", + "attr.python.aliases": "bool", + "attr.rust.derive": "Copy, Default, PartialEq, Eq, PartialOrd, Ord", + "attr.rust.repr": "transparent", + "attr.rust.tuple_struct" +) { + show_navigation_controls: rerun.datatypes.Bool (order: 100); +} diff --git a/crates/store/re_sdk_types/definitions/rerun/blueprint/components/web_page_url.fbs b/crates/store/re_sdk_types/definitions/rerun/blueprint/components/web_page_url.fbs new file mode 100644 index 000000000000..0fa8ba38d596 --- /dev/null +++ b/crates/store/re_sdk_types/definitions/rerun/blueprint/components/web_page_url.fbs @@ -0,0 +1,13 @@ + +namespace rerun.blueprint.components; + +/// The initial URL to load in a Web Page View. +table WebPageUrl ( + "attr.arrow.transparent", + "attr.rerun.scope": "blueprint", + "attr.python.aliases": "str", + "attr.rust.derive": "Default, PartialEq, Eq, PartialOrd, Ord", + "attr.rust.repr": "transparent" +) { + url: rerun.datatypes.Utf8 (order: 100); +} diff --git a/crates/store/re_sdk_types/definitions/rerun/blueprint/views.fbs b/crates/store/re_sdk_types/definitions/rerun/blueprint/views.fbs index c2196abd7e23..5dfcbf7c3dbe 100644 --- a/crates/store/re_sdk_types/definitions/rerun/blueprint/views.fbs +++ b/crates/store/re_sdk_types/definitions/rerun/blueprint/views.fbs @@ -11,3 +11,4 @@ include "./views/tensor.fbs"; include "./views/text_document.fbs"; include "./views/text_log.fbs"; include "./views/time_series.fbs"; +include "./views/web_page.fbs"; diff --git a/crates/store/re_sdk_types/definitions/rerun/blueprint/views/web_page.fbs b/crates/store/re_sdk_types/definitions/rerun/blueprint/views/web_page.fbs new file mode 100644 index 000000000000..669f66dc0155 --- /dev/null +++ b/crates/store/re_sdk_types/definitions/rerun/blueprint/views/web_page.fbs @@ -0,0 +1,17 @@ +namespace rerun.blueprint.views; + +/// A native-only view that embeds a configured web page. +/// +/// Web Page View is unsupported in the web viewer. Native support depends on the +/// operating-system webview runtime provided through `wry`: `WebView2` on Windows, +/// `WebKit` on macOS, and `WebKitGTK` on Linux. Linux child webviews may also depend +/// on the active display-server integration; the direct child-window path is +/// X11-oriented, while Wayland-capable embedding requires a GTK container path. +table WebPageView ( + "attr.docs.unreleased", + "attr.rerun.view_identifier": "WebPage", + "attr.rerun.state": "unstable" +) { + /// Configuration for the web page to embed. + config: rerun.blueprint.archetypes.WebPageViewConfig (order: 1000); +} diff --git a/crates/store/re_sdk_types/src/blueprint/archetypes/.gitattributes b/crates/store/re_sdk_types/src/blueprint/archetypes/.gitattributes index 4d0325f9d507..e963b31a8127 100644 --- a/crates/store/re_sdk_types/src/blueprint/archetypes/.gitattributes +++ b/crates/store/re_sdk_types/src/blueprint/archetypes/.gitattributes @@ -37,3 +37,4 @@ viewport_blueprint.rs linguist-generated=true visible_time_ranges.rs linguist-generated=true visual_bounds2d.rs linguist-generated=true visualizer_instruction.rs linguist-generated=true +web_page_view_config.rs linguist-generated=true diff --git a/crates/store/re_sdk_types/src/blueprint/archetypes/mod.rs b/crates/store/re_sdk_types/src/blueprint/archetypes/mod.rs index c683ae55c04b..1581b110a648 100644 --- a/crates/store/re_sdk_types/src/blueprint/archetypes/mod.rs +++ b/crates/store/re_sdk_types/src/blueprint/archetypes/mod.rs @@ -36,6 +36,7 @@ mod viewport_blueprint; mod visible_time_ranges; mod visual_bounds2d; mod visualizer_instruction; +mod web_page_view_config; pub use self::active_visualizers::ActiveVisualizers; pub use self::background::Background; @@ -72,3 +73,4 @@ pub use self::viewport_blueprint::ViewportBlueprint; pub use self::visible_time_ranges::VisibleTimeRanges; pub use self::visual_bounds2d::VisualBounds2D; pub use self::visualizer_instruction::VisualizerInstruction; +pub use self::web_page_view_config::WebPageViewConfig; diff --git a/crates/store/re_sdk_types/src/blueprint/archetypes/web_page_view_config.rs b/crates/store/re_sdk_types/src/blueprint/archetypes/web_page_view_config.rs new file mode 100644 index 000000000000..ca4cacbfaff3 --- /dev/null +++ b/crates/store/re_sdk_types/src/blueprint/archetypes/web_page_view_config.rs @@ -0,0 +1,215 @@ +// DO NOT EDIT! This file was auto-generated by crates/build/re_types_builder/src/codegen/rust/api.rs +// Based on "crates/store/re_sdk_types/definitions/rerun/blueprint/archetypes/web_page_view_config.fbs". + +#![allow(unused_braces)] +#![allow(unused_imports)] +#![allow(unused_parens)] +#![allow(clippy::allow_attributes)] +#![allow(clippy::clone_on_copy)] +#![allow(clippy::cloned_instead_of_copied)] +#![allow(clippy::map_flatten)] +#![allow(clippy::needless_question_mark)] +#![allow(clippy::new_without_default)] +#![allow(clippy::redundant_closure)] +#![allow(clippy::too_many_arguments)] +#![allow(clippy::too_many_lines)] +#![allow(clippy::wildcard_imports)] + +use ::re_types_core::SerializationResult; +use ::re_types_core::try_serialize_field; +use ::re_types_core::{ComponentBatch as _, SerializedComponentBatch}; +use ::re_types_core::{ComponentDescriptor, ComponentType}; +use ::re_types_core::{DeserializationError, DeserializationResult}; + +/// **Archetype**: Configuration for the Web Page View. +/// +/// ⚠️ **This type is _unstable_ and may change significantly in a way that the data won't be backwards compatible.** +#[derive(Clone, Debug, Default)] +pub struct WebPageViewConfig { + /// The initial URL to load. + pub url: Option, + + /// Whether browser navigation controls should be shown. + /// + /// Defaults to true. + pub show_navigation_controls: Option, +} + +impl WebPageViewConfig { + /// Returns the [`ComponentDescriptor`] for [`Self::url`]. + /// + /// The corresponding component is [`crate::blueprint::components::WebPageUrl`]. + #[inline] + pub fn descriptor_url() -> ComponentDescriptor { + ComponentDescriptor { + archetype: Some("rerun.blueprint.archetypes.WebPageViewConfig".into()), + component: "WebPageViewConfig:url".into(), + component_type: Some("rerun.blueprint.components.WebPageUrl".into()), + } + } + + /// Returns the [`ComponentDescriptor`] for [`Self::show_navigation_controls`]. + /// + /// The corresponding component is [`crate::blueprint::components::ShowNavigationControls`]. + #[inline] + pub fn descriptor_show_navigation_controls() -> ComponentDescriptor { + ComponentDescriptor { + archetype: Some("rerun.blueprint.archetypes.WebPageViewConfig".into()), + component: "WebPageViewConfig:show_navigation_controls".into(), + component_type: Some("rerun.blueprint.components.ShowNavigationControls".into()), + } + } +} + +static REQUIRED_COMPONENTS: std::sync::LazyLock<[ComponentDescriptor; 1usize]> = + std::sync::LazyLock::new(|| [WebPageViewConfig::descriptor_url()]); + +static RECOMMENDED_COMPONENTS: std::sync::LazyLock<[ComponentDescriptor; 0usize]> = + std::sync::LazyLock::new(|| []); + +static OPTIONAL_COMPONENTS: std::sync::LazyLock<[ComponentDescriptor; 1usize]> = + std::sync::LazyLock::new(|| [WebPageViewConfig::descriptor_show_navigation_controls()]); + +static ALL_COMPONENTS: std::sync::LazyLock<[ComponentDescriptor; 2usize]> = + std::sync::LazyLock::new(|| { + [ + WebPageViewConfig::descriptor_url(), + WebPageViewConfig::descriptor_show_navigation_controls(), + ] + }); + +impl WebPageViewConfig { + /// The total number of components in the archetype: 1 required, 0 recommended, 1 optional + pub const NUM_COMPONENTS: usize = 2usize; +} + +impl ::re_types_core::Archetype for WebPageViewConfig { + #[inline] + fn name() -> ::re_types_core::ArchetypeName { + "rerun.blueprint.archetypes.WebPageViewConfig".into() + } + + #[inline] + fn display_name() -> &'static str { + "Web page view config" + } + + #[inline] + fn required_components() -> ::std::borrow::Cow<'static, [ComponentDescriptor]> { + REQUIRED_COMPONENTS.as_slice().into() + } + + #[inline] + fn recommended_components() -> ::std::borrow::Cow<'static, [ComponentDescriptor]> { + RECOMMENDED_COMPONENTS.as_slice().into() + } + + #[inline] + fn optional_components() -> ::std::borrow::Cow<'static, [ComponentDescriptor]> { + OPTIONAL_COMPONENTS.as_slice().into() + } + + #[inline] + fn all_components() -> ::std::borrow::Cow<'static, [ComponentDescriptor]> { + ALL_COMPONENTS.as_slice().into() + } + + #[inline] + fn from_arrow_components( + arrow_data: impl IntoIterator, + ) -> DeserializationResult { + re_tracing::profile_function!(); + use ::re_types_core::{Loggable as _, ResultExt as _}; + let arrays_by_descr: ::nohash_hasher::IntMap<_, _> = arrow_data.into_iter().collect(); + let url = arrays_by_descr + .get(&Self::descriptor_url()) + .map(|array| SerializedComponentBatch::new(array.clone(), Self::descriptor_url())); + let show_navigation_controls = arrays_by_descr + .get(&Self::descriptor_show_navigation_controls()) + .map(|array| { + SerializedComponentBatch::new( + array.clone(), + Self::descriptor_show_navigation_controls(), + ) + }); + Ok(Self { + url, + show_navigation_controls, + }) + } +} + +impl ::re_types_core::AsComponents for WebPageViewConfig { + #[inline] + fn as_serialized_batches(&self) -> Vec { + use ::re_types_core::Archetype as _; + [self.url.clone(), self.show_navigation_controls.clone()] + .into_iter() + .flatten() + .collect() + } +} + +impl ::re_types_core::ArchetypeReflectionMarker for WebPageViewConfig {} + +impl WebPageViewConfig { + /// Create a new `WebPageViewConfig`. + #[inline] + pub fn new(url: impl Into) -> Self { + Self { + url: try_serialize_field(Self::descriptor_url(), [url]), + show_navigation_controls: None, + } + } + + /// Update only some specific fields of a `WebPageViewConfig`. + #[inline] + pub fn update_fields() -> Self { + Self::default() + } + + /// Clear all the fields of a `WebPageViewConfig`. + #[inline] + pub fn clear_fields() -> Self { + use ::re_types_core::Loggable as _; + Self { + url: Some(SerializedComponentBatch::new( + crate::blueprint::components::WebPageUrl::arrow_empty(), + Self::descriptor_url(), + )), + show_navigation_controls: Some(SerializedComponentBatch::new( + crate::blueprint::components::ShowNavigationControls::arrow_empty(), + Self::descriptor_show_navigation_controls(), + )), + } + } + + /// The initial URL to load. + #[inline] + pub fn with_url(mut self, url: impl Into) -> Self { + self.url = try_serialize_field(Self::descriptor_url(), [url]); + self + } + + /// Whether browser navigation controls should be shown. + /// + /// Defaults to true. + #[inline] + pub fn with_show_navigation_controls( + mut self, + show_navigation_controls: impl Into, + ) -> Self { + self.show_navigation_controls = try_serialize_field( + Self::descriptor_show_navigation_controls(), + [show_navigation_controls], + ); + self + } +} + +impl ::re_byte_size::SizeBytes for WebPageViewConfig { + #[inline] + fn heap_size_bytes(&self) -> u64 { + self.url.heap_size_bytes() + self.show_navigation_controls.heap_size_bytes() + } +} diff --git a/crates/store/re_sdk_types/src/blueprint/components/.gitattributes b/crates/store/re_sdk_types/src/blueprint/components/.gitattributes index ade60b56d169..a3f5fdd699e2 100644 --- a/crates/store/re_sdk_types/src/blueprint/components/.gitattributes +++ b/crates/store/re_sdk_types/src/blueprint/components/.gitattributes @@ -38,6 +38,7 @@ query_expression.rs linguist-generated=true root_container.rs linguist-generated=true row_share.rs linguist-generated=true selected_columns.rs linguist-generated=true +show_navigation_controls.rs linguist-generated=true tensor_dimension_index_slider.rs linguist-generated=true text_log_column.rs linguist-generated=true time_int.rs linguist-generated=true @@ -54,4 +55,5 @@ visual_bounds2d.rs linguist-generated=true visualizer_component_mapping.rs linguist-generated=true visualizer_instruction_id.rs linguist-generated=true visualizer_type.rs linguist-generated=true +web_page_url.rs linguist-generated=true zoom_level.rs linguist-generated=true diff --git a/crates/store/re_sdk_types/src/blueprint/components/mod.rs b/crates/store/re_sdk_types/src/blueprint/components/mod.rs index fea4631c39da..6165a9e4a3bc 100644 --- a/crates/store/re_sdk_types/src/blueprint/components/mod.rs +++ b/crates/store/re_sdk_types/src/blueprint/components/mod.rs @@ -45,6 +45,7 @@ mod query_expression; mod root_container; mod row_share; mod selected_columns; +mod show_navigation_controls; mod tensor_dimension_index_slider; mod tensor_dimension_index_slider_ext; mod text_log_column; @@ -68,6 +69,7 @@ mod visualizer_component_mapping; mod visualizer_instruction_id; mod visualizer_instruction_id_ext; mod visualizer_type; +mod web_page_url; mod zoom_level; pub use self::absolute_time_range::AbsoluteTimeRange; @@ -106,6 +108,7 @@ pub use self::query_expression::QueryExpression; pub use self::root_container::RootContainer; pub use self::row_share::RowShare; pub use self::selected_columns::SelectedColumns; +pub use self::show_navigation_controls::ShowNavigationControls; pub use self::tensor_dimension_index_slider::TensorDimensionIndexSlider; pub use self::text_log_column::TextLogColumn; pub use self::time_int::TimeInt; @@ -122,4 +125,5 @@ pub use self::visual_bounds2d::VisualBounds2D; pub use self::visualizer_component_mapping::VisualizerComponentMapping; pub use self::visualizer_instruction_id::VisualizerInstructionId; pub use self::visualizer_type::VisualizerType; +pub use self::web_page_url::WebPageUrl; pub use self::zoom_level::ZoomLevel; diff --git a/crates/store/re_sdk_types/src/blueprint/components/show_navigation_controls.rs b/crates/store/re_sdk_types/src/blueprint/components/show_navigation_controls.rs new file mode 100644 index 000000000000..c4060fa4e745 --- /dev/null +++ b/crates/store/re_sdk_types/src/blueprint/components/show_navigation_controls.rs @@ -0,0 +1,86 @@ +// DO NOT EDIT! This file was auto-generated by crates/build/re_types_builder/src/codegen/rust/api.rs +// Based on "crates/store/re_sdk_types/definitions/rerun/blueprint/components/show_navigation_controls.fbs". + +#![allow(unused_braces)] +#![allow(unused_imports)] +#![allow(unused_parens)] +#![allow(clippy::allow_attributes)] +#![allow(clippy::clone_on_copy)] +#![allow(clippy::cloned_instead_of_copied)] +#![allow(clippy::map_flatten)] +#![allow(clippy::needless_question_mark)] +#![allow(clippy::new_without_default)] +#![allow(clippy::redundant_closure)] +#![allow(clippy::too_many_arguments)] +#![allow(clippy::too_many_lines)] +#![allow(clippy::wildcard_imports)] + +use ::re_types_core::SerializationResult; +use ::re_types_core::try_serialize_field; +use ::re_types_core::{ComponentBatch as _, SerializedComponentBatch}; +use ::re_types_core::{ComponentDescriptor, ComponentType}; +use ::re_types_core::{DeserializationError, DeserializationResult}; + +/// **Component**: Whether browser navigation controls should be shown in the Web Page View. +/// +/// ⚠️ **This type is _unstable_ and may change significantly in a way that the data won't be backwards compatible.** +#[derive(Clone, Debug, Copy, Default, PartialEq, Eq, PartialOrd, Ord)] +#[repr(transparent)] +pub struct ShowNavigationControls(pub crate::datatypes::Bool); + +impl ::re_types_core::WrapperComponent for ShowNavigationControls { + type Datatype = crate::datatypes::Bool; + + #[inline] + fn name() -> ComponentType { + "rerun.blueprint.components.ShowNavigationControls".into() + } + + #[inline] + fn into_inner(self) -> Self::Datatype { + self.0 + } +} + +::re_types_core::macros::impl_into_cow!(ShowNavigationControls); + +impl> From for ShowNavigationControls { + fn from(v: T) -> Self { + Self(v.into()) + } +} + +impl std::borrow::Borrow for ShowNavigationControls { + #[inline] + fn borrow(&self) -> &crate::datatypes::Bool { + &self.0 + } +} + +impl std::ops::Deref for ShowNavigationControls { + type Target = crate::datatypes::Bool; + + #[inline] + fn deref(&self) -> &crate::datatypes::Bool { + &self.0 + } +} + +impl std::ops::DerefMut for ShowNavigationControls { + #[inline] + fn deref_mut(&mut self) -> &mut crate::datatypes::Bool { + &mut self.0 + } +} + +impl ::re_byte_size::SizeBytes for ShowNavigationControls { + #[inline] + fn heap_size_bytes(&self) -> u64 { + self.0.heap_size_bytes() + } + + #[inline] + fn is_pod() -> bool { + ::is_pod() + } +} diff --git a/crates/store/re_sdk_types/src/blueprint/components/web_page_url.rs b/crates/store/re_sdk_types/src/blueprint/components/web_page_url.rs new file mode 100644 index 000000000000..ca6be2a0fc1d --- /dev/null +++ b/crates/store/re_sdk_types/src/blueprint/components/web_page_url.rs @@ -0,0 +1,86 @@ +// DO NOT EDIT! This file was auto-generated by crates/build/re_types_builder/src/codegen/rust/api.rs +// Based on "crates/store/re_sdk_types/definitions/rerun/blueprint/components/web_page_url.fbs". + +#![allow(unused_braces)] +#![allow(unused_imports)] +#![allow(unused_parens)] +#![allow(clippy::allow_attributes)] +#![allow(clippy::clone_on_copy)] +#![allow(clippy::cloned_instead_of_copied)] +#![allow(clippy::map_flatten)] +#![allow(clippy::needless_question_mark)] +#![allow(clippy::new_without_default)] +#![allow(clippy::redundant_closure)] +#![allow(clippy::too_many_arguments)] +#![allow(clippy::too_many_lines)] +#![allow(clippy::wildcard_imports)] + +use ::re_types_core::SerializationResult; +use ::re_types_core::try_serialize_field; +use ::re_types_core::{ComponentBatch as _, SerializedComponentBatch}; +use ::re_types_core::{ComponentDescriptor, ComponentType}; +use ::re_types_core::{DeserializationError, DeserializationResult}; + +/// **Component**: The initial URL to load in a Web Page View. +/// +/// ⚠️ **This type is _unstable_ and may change significantly in a way that the data won't be backwards compatible.** +#[derive(Clone, Debug, Default, PartialEq, Eq, PartialOrd, Ord)] +#[repr(transparent)] +pub struct WebPageUrl(pub crate::datatypes::Utf8); + +impl ::re_types_core::WrapperComponent for WebPageUrl { + type Datatype = crate::datatypes::Utf8; + + #[inline] + fn name() -> ComponentType { + "rerun.blueprint.components.WebPageUrl".into() + } + + #[inline] + fn into_inner(self) -> Self::Datatype { + self.0 + } +} + +::re_types_core::macros::impl_into_cow!(WebPageUrl); + +impl> From for WebPageUrl { + fn from(v: T) -> Self { + Self(v.into()) + } +} + +impl std::borrow::Borrow for WebPageUrl { + #[inline] + fn borrow(&self) -> &crate::datatypes::Utf8 { + &self.0 + } +} + +impl std::ops::Deref for WebPageUrl { + type Target = crate::datatypes::Utf8; + + #[inline] + fn deref(&self) -> &crate::datatypes::Utf8 { + &self.0 + } +} + +impl std::ops::DerefMut for WebPageUrl { + #[inline] + fn deref_mut(&mut self) -> &mut crate::datatypes::Utf8 { + &mut self.0 + } +} + +impl ::re_byte_size::SizeBytes for WebPageUrl { + #[inline] + fn heap_size_bytes(&self) -> u64 { + self.0.heap_size_bytes() + } + + #[inline] + fn is_pod() -> bool { + ::is_pod() + } +} diff --git a/crates/store/re_sdk_types/src/blueprint/views/.gitattributes b/crates/store/re_sdk_types/src/blueprint/views/.gitattributes index dcc2d8675efb..433892528714 100644 --- a/crates/store/re_sdk_types/src/blueprint/views/.gitattributes +++ b/crates/store/re_sdk_types/src/blueprint/views/.gitattributes @@ -13,3 +13,4 @@ tensor_view.rs linguist-generated=true text_document_view.rs linguist-generated=true text_log_view.rs linguist-generated=true time_series_view.rs linguist-generated=true +web_page_view.rs linguist-generated=true diff --git a/crates/store/re_sdk_types/src/blueprint/views/mod.rs b/crates/store/re_sdk_types/src/blueprint/views/mod.rs index 01fa75bf503f..5b73e883ba03 100644 --- a/crates/store/re_sdk_types/src/blueprint/views/mod.rs +++ b/crates/store/re_sdk_types/src/blueprint/views/mod.rs @@ -11,6 +11,7 @@ mod tensor_view; mod text_document_view; mod text_log_view; mod time_series_view; +mod web_page_view; pub use self::bar_chart_view::BarChartView; pub use self::dataframe_view::DataframeView; @@ -23,3 +24,4 @@ pub use self::tensor_view::TensorView; pub use self::text_document_view::TextDocumentView; pub use self::text_log_view::TextLogView; pub use self::time_series_view::TimeSeriesView; +pub use self::web_page_view::WebPageView; diff --git a/crates/store/re_sdk_types/src/blueprint/views/web_page_view.rs b/crates/store/re_sdk_types/src/blueprint/views/web_page_view.rs new file mode 100644 index 000000000000..d492aa702984 --- /dev/null +++ b/crates/store/re_sdk_types/src/blueprint/views/web_page_view.rs @@ -0,0 +1,85 @@ +// DO NOT EDIT! This file was auto-generated by crates/build/re_types_builder/src/codegen/rust/api.rs +// Based on "crates/store/re_sdk_types/definitions/rerun/blueprint/views/web_page.fbs". + +#![allow(unused_braces)] +#![allow(unused_imports)] +#![allow(unused_parens)] +#![allow(clippy::allow_attributes)] +#![allow(clippy::clone_on_copy)] +#![allow(clippy::cloned_instead_of_copied)] +#![allow(clippy::map_flatten)] +#![allow(clippy::needless_question_mark)] +#![allow(clippy::new_without_default)] +#![allow(clippy::redundant_closure)] +#![allow(clippy::too_many_arguments)] +#![allow(clippy::too_many_lines)] +#![allow(clippy::wildcard_imports)] + +use ::re_types_core::SerializationResult; +use ::re_types_core::try_serialize_field; +use ::re_types_core::{ComponentBatch as _, SerializedComponentBatch}; +use ::re_types_core::{ComponentDescriptor, ComponentType}; +use ::re_types_core::{DeserializationError, DeserializationResult}; + +/// **View**: A native-only view that embeds a configured web page. +/// +/// Web Page View is unsupported in the web viewer. Native support depends on the +/// operating-system webview runtime provided through `wry`: `WebView2` on Windows, +/// `WebKit` on macOS, and `WebKitGTK` on Linux. Linux child webviews may also depend +/// on the active display-server integration; the direct child-window path is +/// X11-oriented, while Wayland-capable embedding requires a GTK container path. +/// +/// ⚠️ **This type is _unstable_ and may change significantly in a way that the data won't be backwards compatible.** +#[derive(Clone, Debug)] +pub struct WebPageView { + /// Configuration for the web page to embed. + pub config: crate::blueprint::archetypes::WebPageViewConfig, +} + +impl ::re_types_core::View for WebPageView { + #[inline] + fn identifier() -> ::re_types_core::ViewClassIdentifier { + "WebPage".into() + } +} + +impl> From for WebPageView { + fn from(v: T) -> Self { + Self { config: v.into() } + } +} + +impl std::borrow::Borrow for WebPageView { + #[inline] + fn borrow(&self) -> &crate::blueprint::archetypes::WebPageViewConfig { + &self.config + } +} + +impl std::ops::Deref for WebPageView { + type Target = crate::blueprint::archetypes::WebPageViewConfig; + + #[inline] + fn deref(&self) -> &crate::blueprint::archetypes::WebPageViewConfig { + &self.config + } +} + +impl std::ops::DerefMut for WebPageView { + #[inline] + fn deref_mut(&mut self) -> &mut crate::blueprint::archetypes::WebPageViewConfig { + &mut self.config + } +} + +impl ::re_byte_size::SizeBytes for WebPageView { + #[inline] + fn heap_size_bytes(&self) -> u64 { + self.config.heap_size_bytes() + } + + #[inline] + fn is_pod() -> bool { + ::is_pod() + } +} diff --git a/crates/store/re_sdk_types/src/reflection/mod.rs b/crates/store/re_sdk_types/src/reflection/mod.rs index a0e7003ab013..0edbc448d4cf 100644 --- a/crates/store/re_sdk_types/src/reflection/mod.rs +++ b/crates/store/re_sdk_types/src/reflection/mod.rs @@ -436,6 +436,17 @@ fn generate_component_reflection() -> Result::name(), + ComponentReflection { + docstring_md: "Whether browser navigation controls should be shown in the Web Page View.\n\n⚠\u{fe0f} **This type is _unstable_ and may change significantly in a way that the data won't be backwards compatible.**", + deprecation_summary: None, + custom_placeholder: Some(ShowNavigationControls::default().to_arrow()?), + datatype: ShowNavigationControls::arrow_datatype(), + is_enum: false, + verify_arrow_array: ShowNavigationControls::verify_arrow_array, + }, + ), ( ::name(), ComponentReflection { @@ -612,6 +623,17 @@ fn generate_component_reflection() -> Result::name(), + ComponentReflection { + docstring_md: "The initial URL to load in a Web Page View.\n\n⚠\u{fe0f} **This type is _unstable_ and may change significantly in a way that the data won't be backwards compatible.**", + deprecation_summary: None, + custom_placeholder: Some(WebPageUrl::default().to_arrow()?), + datatype: WebPageUrl::arrow_datatype(), + is_enum: false, + verify_arrow_array: WebPageUrl::verify_arrow_array, + }, + ), ( ::name(), ComponentReflection { @@ -4722,6 +4744,31 @@ fn generate_archetype_reflection() -> ArchetypeReflectionMap { ], }, ), + ( + ArchetypeName::new("rerun.blueprint.archetypes.WebPageViewConfig"), + ArchetypeReflection { + display_name: "Web page view config", + deprecation_summary: None, + scope: Some("blueprint"), + view_types: &["WebPageView"], + fields: vec![ + ArchetypeFieldReflection { + name: "url", + display_name: "Url", + component_type: "rerun.blueprint.components.WebPageUrl".into(), + docstring_md: "The initial URL to load.", + flags: ArchetypeFieldFlags::REQUIRED | ArchetypeFieldFlags::UI_EDITABLE, + }, + ArchetypeFieldReflection { + name: "show_navigation_controls", + display_name: "Show navigation controls", + component_type: "rerun.blueprint.components.ShowNavigationControls".into(), + docstring_md: "Whether browser navigation controls should be shown.\n\nDefaults to true.", + flags: ArchetypeFieldFlags::UI_EDITABLE, + }, + ], + }, + ), ]; ArchetypeReflectionMap::from_iter(array) } diff --git a/crates/top/rerun-cli/Cargo.toml b/crates/top/rerun-cli/Cargo.toml index 9a106f47cfd0..9591db259f19 100644 --- a/crates/top/rerun-cli/Cargo.toml +++ b/crates/top/rerun-cli/Cargo.toml @@ -87,6 +87,9 @@ nasm = ["rerun/nasm"] ## This adds a lot of extra dependencies, so only enable this feature if you need it! native_viewer = ["rerun/native_viewer"] +## Experimental native embedded webview support for the Web Page View. +native_webview = ["native_viewer", "rerun/native_webview"] + ## Enable the in-memory Rerun Server, useful for testing. oss_server = ["rerun/oss_server"] diff --git a/crates/top/rerun/Cargo.toml b/crates/top/rerun/Cargo.toml index ab29a0d2cff7..aca41a80fc9c 100644 --- a/crates/top/rerun/Cargo.toml +++ b/crates/top/rerun/Cargo.toml @@ -90,6 +90,9 @@ nasm = ["re_video/nasm"] ## This adds a lot of extra dependencies, so only enable this feature if you need it! native_viewer = ["dep:re_viewer", "dep:re_crash_handler"] +## Experimental native embedded webview support for the Web Page View. +native_webview = ["native_viewer", "re_viewer/native_webview"] + ## Enable the in-memory Rerun Server, useful for testing. oss_server = ["dep:re_server"] diff --git a/crates/top/rerun/src/commands/entrypoint.rs b/crates/top/rerun/src/commands/entrypoint.rs index a2f68ed94a77..35a284ee829d 100644 --- a/crates/top/rerun/src/commands/entrypoint.rs +++ b/crates/top/rerun/src/commands/entrypoint.rs @@ -1760,6 +1760,7 @@ fn record_cli_command_analytics(args: &Args) { cors_allow_origin: _, port: _, new: _, + ws_url: _, } = args; let (command, subcommand) = match command { @@ -1834,7 +1835,14 @@ fn record_cli_command_analytics(args: &Args) { /// A function that wraps a `re_viewer::App` into a custom `Box`. /// Used by dimos-viewer to inject keyboard teleop and other behaviors. -pub type AppWrapper = Box Result, Box> + Send>; +pub type AppWrapper = Box< + dyn FnOnce( + re_viewer::App, + ) -> Result< + Box, + Box, + > + Send, +>; /// Optional patches to [`re_viewer::StartupOptions`] injected by the app wrapper. #[derive(Default)] diff --git a/crates/viewer/re_view_web_page/Cargo.toml b/crates/viewer/re_view_web_page/Cargo.toml new file mode 100644 index 000000000000..de258cd4871c --- /dev/null +++ b/crates/viewer/re_view_web_page/Cargo.toml @@ -0,0 +1,56 @@ +[package] +authors.workspace = true +description = "A native-only view that embeds a configured web page." +edition.workspace = true +homepage.workspace = true +license.workspace = true +name = "re_view_web_page" +publish = true +readme = "README.md" +repository.workspace = true +rust-version.workspace = true +version.workspace = true +include.workspace = true + +[lints] +workspace = true + +[package.metadata.docs.rs] +all-features = true + +[features] +default = [] +native_webview = [ + "dep:eframe", + "dep:gtk", + "dep:raw-window-handle", + "dep:scoped-tls", + "dep:wry", +] + +[dependencies] +ahash.workspace = true +egui.workspace = true +gtk = { workspace = true, optional = true } +parking_lot.workspace = true +re_log_types.workspace = true +re_sdk_types.workspace = true +re_tracing.workspace = true +re_ui.workspace = true +re_viewer_context.workspace = true +re_viewport_blueprint.workspace = true +url.workspace = true + +[target.'cfg(not(target_arch = "wasm32"))'.dependencies] +eframe = { workspace = true, optional = true, default-features = false } +raw-window-handle = { workspace = true, optional = true } +scoped-tls = { workspace = true, optional = true } +wry = { workspace = true, optional = true, default-features = false, features = [ + "os-webview", + "x11", +] } + +[dev-dependencies] +egui_kittest.workspace = true +re_test_context.workspace = true +re_test_viewport.workspace = true diff --git a/crates/viewer/re_view_web_page/src/backend.rs b/crates/viewer/re_view_web_page/src/backend.rs new file mode 100644 index 000000000000..2cb87e67a8f8 --- /dev/null +++ b/crates/viewer/re_view_web_page/src/backend.rs @@ -0,0 +1,213 @@ +use re_viewer_context::{ViewId, ViewerContext}; + +pub(crate) struct WebViewInstance { + view_id: ViewId, + pub(crate) url: String, + #[cfg(debug_assertions)] + fake_backend: Option, + #[cfg(all(not(target_arch = "wasm32"), feature = "native_webview"))] + has_native_webview: bool, +} + +impl std::fmt::Debug for WebViewInstance { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("WebViewInstance") + .field("view_id", &self.view_id) + .field("url", &self.url) + .finish_non_exhaustive() + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(crate) enum WebViewSession { + SharedDefault, +} + +impl WebViewSession { + pub(crate) const fn shared_default() -> Self { + Self::SharedDefault + } + + pub(crate) const fn as_str(self) -> &'static str { + match self { + Self::SharedDefault => "shared-default", + } + } +} + +impl WebViewInstance { + #[cfg(debug_assertions)] + pub(crate) fn new_fake( + view_id: ViewId, + url: String, + fake_backend: crate::testing::FakeWebViewBackend, + ) -> Self { + Self { + view_id, + url, + fake_backend: Some(fake_backend), + #[cfg(all(not(target_arch = "wasm32"), feature = "native_webview"))] + has_native_webview: false, + } + } + + #[cfg(all(not(target_arch = "wasm32"), feature = "native_webview"))] + pub(crate) fn new_native(view_id: ViewId, url: String) -> Self { + Self { + view_id, + url, + #[cfg(debug_assertions)] + fake_backend: None, + has_native_webview: true, + } + } + + pub(crate) fn set_bounds(&self, view_id: ViewId, bounds: WebViewBounds) { + #[cfg(debug_assertions)] + if let Some(fake_backend) = &self.fake_backend { + fake_backend.record_bounds_update(view_id, bounds); + } + + #[cfg(all(not(target_arch = "wasm32"), feature = "native_webview"))] + if self.has_native_webview { + crate::native_backend::set_bounds(self.view_id, bounds); + } + } + + pub(crate) fn go_back(&self) { + #[cfg(debug_assertions)] + if let Some(fake_backend) = &self.fake_backend { + fake_backend.record_navigation_command(self.view_id, FakeNavigationCommand::Back); + } + + #[cfg(all(not(target_arch = "wasm32"), feature = "native_webview"))] + if self.has_native_webview { + crate::native_backend::go_back(self.view_id); + } + } + + pub(crate) fn go_forward(&self) { + #[cfg(debug_assertions)] + if let Some(fake_backend) = &self.fake_backend { + fake_backend.record_navigation_command(self.view_id, FakeNavigationCommand::Forward); + } + + #[cfg(all(not(target_arch = "wasm32"), feature = "native_webview"))] + if self.has_native_webview { + crate::native_backend::go_forward(self.view_id); + } + } + + pub(crate) fn reload(&self) { + #[cfg(debug_assertions)] + if let Some(fake_backend) = &self.fake_backend { + fake_backend.record_navigation_command(self.view_id, FakeNavigationCommand::Reload); + } + + #[cfg(all(not(target_arch = "wasm32"), feature = "native_webview"))] + if self.has_native_webview { + crate::native_backend::reload(self.view_id); + } + } + + pub(crate) fn navigate_to(&self, url: &str) { + #[cfg(debug_assertions)] + if let Some(fake_backend) = &self.fake_backend { + fake_backend.record_navigation_command( + self.view_id, + FakeNavigationCommand::NavigateTo(url.to_owned()), + ); + } + + #[cfg(all(not(target_arch = "wasm32"), feature = "native_webview"))] + if self.has_native_webview { + crate::native_backend::navigate_to(self.view_id, url); + } + } +} + +#[cfg(debug_assertions)] +use crate::testing::FakeNavigationCommand; + +impl Drop for WebViewInstance { + fn drop(&mut self) { + #[cfg(debug_assertions)] + if let Some(fake_backend) = &self.fake_backend { + fake_backend.record_destroyed_instance(self.view_id, &self.url); + } + + #[cfg(all(not(target_arch = "wasm32"), feature = "native_webview"))] + if self.has_native_webview { + crate::native_backend::destroy(self.view_id); + } + } +} + +#[derive(Debug, Clone, Copy, PartialEq)] +pub(crate) struct WebViewBounds { + pub(crate) min: [f32; 2], + pub(crate) size: [f32; 2], +} + +impl WebViewBounds { + pub(crate) fn from_egui_rect(rect: egui::Rect, pixels_per_point: f32) -> Self { + let min = rect.min * pixels_per_point; + let size = rect.size() * pixels_per_point; + Self { + min: [min.x, min.y], + size: [size.x, size.y], + } + } +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub(crate) enum WebViewBackendError { + CreationFailed(String), +} + +impl std::fmt::Display for WebViewBackendError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::CreationFailed(message) => f.write_str(message), + } + } +} + +impl std::error::Error for WebViewBackendError {} + +pub(crate) trait WebViewBackend { + fn create( + &self, + ctx: &ViewerContext<'_>, + view_id: ViewId, + url: &str, + bounds: WebViewBounds, + session: WebViewSession, + ) -> Result; +} + +pub(crate) fn create_webview( + ctx: &ViewerContext<'_>, + view_id: ViewId, + url: &str, + bounds: WebViewBounds, +) -> Result, WebViewBackendError> { + #[cfg(debug_assertions)] + if let Some(fake_backend) = crate::testing::installed_backend() { + return fake_backend + .create(ctx, view_id, url, bounds, WebViewSession::shared_default()) + .map(Some); + } + + #[cfg(all(not(target_arch = "wasm32"), feature = "native_webview"))] + if crate::native_backend::has_native_parent_window() { + let native_webview = crate::native_backend::NativeWebViewBackend + .create_child(url, bounds) + .map_err(|err| WebViewBackendError::CreationFailed(err.to_string()))?; + crate::native_backend::insert(view_id, native_webview); + return Ok(Some(WebViewInstance::new_native(view_id, url.to_owned()))); + } + + let _ = (ctx, view_id, url, bounds); + Ok(None) +} diff --git a/crates/viewer/re_view_web_page/src/lib.rs b/crates/viewer/re_view_web_page/src/lib.rs new file mode 100644 index 000000000000..970bb249cea8 --- /dev/null +++ b/crates/viewer/re_view_web_page/src/lib.rs @@ -0,0 +1,12 @@ +//! Native Web Page View. + +mod backend; +mod lifecycle; +#[cfg(all(not(target_arch = "wasm32"), feature = "native_webview"))] +pub mod native_backend; +#[cfg(debug_assertions)] +pub mod testing; +mod url_policy; +mod view_class; + +pub use view_class::WebPageView; diff --git a/crates/viewer/re_view_web_page/src/lifecycle.rs b/crates/viewer/re_view_web_page/src/lifecycle.rs new file mode 100644 index 000000000000..66570dd1f5a9 --- /dev/null +++ b/crates/viewer/re_view_web_page/src/lifecycle.rs @@ -0,0 +1,85 @@ +use re_viewer_context::{ViewId, ViewerContext}; + +use crate::backend::{WebViewBounds, WebViewInstance, create_webview}; + +#[derive(Default)] +pub(crate) struct WebViewLifecycle { + webview: Option, + last_bounds: Option, +} + +impl WebViewLifecycle { + pub(crate) fn ensure_webview( + &mut self, + ctx: &ViewerContext<'_>, + view_id: ViewId, + url: &str, + bounds: WebViewBounds, + ) -> WebViewLifecycleStatus { + if self + .webview + .as_ref() + .is_none_or(|webview| webview.url != url) + { + match create_webview(ctx, view_id, url, bounds) { + Ok(Some(webview)) => { + self.webview = Some(webview); + self.last_bounds = None; + } + Ok(None) => { + self.webview = None; + return WebViewLifecycleStatus::Unavailable; + } + Err(err) => { + self.webview = None; + return WebViewLifecycleStatus::CreationFailed(err.to_string()); + } + } + } + + WebViewLifecycleStatus::Ready + } + + pub(crate) fn update_bounds(&mut self, view_id: ViewId, bounds: WebViewBounds) { + if self.last_bounds == Some(bounds) { + return; + } + + if let Some(webview) = &mut self.webview { + webview.set_bounds(view_id, bounds); + } + + self.last_bounds = Some(bounds); + } + + pub(crate) fn go_back(&mut self) { + if let Some(webview) = &mut self.webview { + webview.go_back(); + } + } + + pub(crate) fn go_forward(&mut self) { + if let Some(webview) = &mut self.webview { + webview.go_forward(); + } + } + + pub(crate) fn reload(&mut self) { + if let Some(webview) = &mut self.webview { + webview.reload(); + } + } + + pub(crate) fn navigate_to(&mut self, url: &str) { + if let Some(webview) = &mut self.webview { + webview.navigate_to(url); + } + } +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub(crate) enum WebViewLifecycleStatus { + Ready, + Unavailable, + CreationFailed(String), +} diff --git a/crates/viewer/re_view_web_page/src/native_backend.rs b/crates/viewer/re_view_web_page/src/native_backend.rs new file mode 100644 index 000000000000..b1801d90b643 --- /dev/null +++ b/crates/viewer/re_view_web_page/src/native_backend.rs @@ -0,0 +1,208 @@ +//! Native `wry` backend for the Web Page View. +//! +//! This module is intentionally a thin boundary: callers provide the native parent window handle, +//! and UI/lifecycle code owns when this is called and how failures are surfaced. + +use re_viewer_context::ViewId; + +use crate::backend::WebViewBounds; + +thread_local! { + static NATIVE_WEBVIEWS: std::cell::RefCell> = + std::cell::RefCell::new(ahash::HashMap::default()); +} + +scoped_tls::scoped_thread_local!(static NATIVE_PARENT_WINDOW: eframe::Frame); + +#[derive(Debug, Default)] +pub struct NativeWebViewBackend; + +pub struct NativeWebView { + webview: wry::WebView, +} + +#[derive(Debug)] +pub enum NativeWebViewError { + MissingParentWindow, + Wry(wry::Error), +} + +impl std::fmt::Display for NativeWebViewError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::MissingParentWindow => f.write_str("missing parent native window"), + Self::Wry(err) => write!(f, "failed to create native webview: {err}"), + } + } +} + +impl std::error::Error for NativeWebViewError {} + +impl From for NativeWebViewError { + fn from(err: wry::Error) -> Self { + Self::Wry(err) + } +} + +impl NativeWebViewBackend { + pub fn create_without_parent_for_smoke_test( + &self, + _url: &str, + ) -> Result { + let _ = self; + Err(NativeWebViewError::MissingParentWindow) + } + + pub(crate) fn create_child( + &self, + url: &str, + bounds: WebViewBounds, + ) -> Result { + let _ = self; + NATIVE_PARENT_WINDOW.with(|parent_window| { + let webview = platform::create_child(parent_window, url, bounds)?; + Ok(NativeWebView { webview }) + }) + } +} + +pub fn with_native_parent_window(frame: &eframe::Frame, f: impl FnOnce() -> R) -> R { + NATIVE_PARENT_WINDOW.set(frame, || { + let result = f(); + platform::pump_events(); + result + }) +} + +pub(crate) fn has_native_parent_window() -> bool { + NATIVE_PARENT_WINDOW.is_set() +} + +impl NativeWebView { + pub(crate) fn set_bounds(&self, bounds: WebViewBounds) -> Result<(), NativeWebViewError> { + self.webview.set_bounds(bounds.into()).map_err(Into::into) + } + + pub(crate) fn go_back(&self) -> Result<(), NativeWebViewError> { + self.webview + .evaluate_script("history.back()") + .map_err(Into::into) + } + + pub(crate) fn go_forward(&self) -> Result<(), NativeWebViewError> { + self.webview + .evaluate_script("history.forward()") + .map_err(Into::into) + } + + pub(crate) fn reload(&self) -> Result<(), NativeWebViewError> { + self.webview.reload().map_err(Into::into) + } + + pub(crate) fn navigate_to(&self, url: &str) -> Result<(), NativeWebViewError> { + self.webview.load_url(url).map_err(Into::into) + } +} + +pub(crate) fn insert(view_id: ViewId, webview: NativeWebView) { + NATIVE_WEBVIEWS.with_borrow_mut(|webviews| { + webviews.insert(view_id, webview); + }); +} + +pub(crate) fn destroy(view_id: ViewId) { + NATIVE_WEBVIEWS.with_borrow_mut(|webviews| { + webviews.remove(&view_id); + }); +} + +pub(crate) fn set_bounds(view_id: ViewId, bounds: WebViewBounds) { + with_webview(view_id, |webview| webview.set_bounds(bounds)); +} + +pub(crate) fn go_back(view_id: ViewId) { + with_webview(view_id, NativeWebView::go_back); +} + +pub(crate) fn go_forward(view_id: ViewId) { + with_webview(view_id, NativeWebView::go_forward); +} + +pub(crate) fn reload(view_id: ViewId) { + with_webview(view_id, NativeWebView::reload); +} + +pub(crate) fn navigate_to(view_id: ViewId, url: &str) { + with_webview(view_id, |webview| webview.navigate_to(url)); +} + +fn with_webview(view_id: ViewId, f: impl FnOnce(&NativeWebView) -> Result<(), NativeWebViewError>) { + NATIVE_WEBVIEWS.with_borrow(|webviews| { + if let Some(webview) = webviews.get(&view_id) { + match f(webview) { + Ok(()) | Err(_) => {} + } + } + }); +} + +impl From for wry::Rect { + fn from(bounds: WebViewBounds) -> Self { + let min_x = bounds.min[0].max(0.0).round() as u32; + let min_y = bounds.min[1].max(0.0).round() as u32; + let width = bounds.size[0].max(1.0).round() as u32; + let height = bounds.size[1].max(1.0).round() as u32; + + Self { + position: wry::dpi::LogicalPosition::new(min_x, min_y).into(), + size: wry::dpi::LogicalSize::new(width, height).into(), + } + } +} + +#[cfg(target_os = "linux")] +mod platform { + use raw_window_handle::HasWindowHandle; + + pub(super) fn pump_events() { + if gtk::is_initialized_main_thread() { + while gtk::events_pending() { + gtk::main_iteration_do(false); + } + } + } + + pub(super) fn create_child( + parent_window: &W, + url: &str, + bounds: crate::backend::WebViewBounds, + ) -> wry::Result { + gtk::init()?; + + // `build_as_child` is the direct child-window path and is X11-only on Linux. + // Wayland support requires the GTK container path (`WebViewBuilderExtUnix::build_gtk`), + // which is intentionally hidden behind this platform module for a later host/widget bridge. + wry::WebViewBuilder::new() + .with_bounds(bounds.into()) + .with_url(url) + .build_as_child(parent_window) + } +} + +#[cfg(not(target_os = "linux"))] +mod platform { + use raw_window_handle::HasWindowHandle; + + pub(super) fn pump_events() {} + + pub(super) fn create_child( + parent_window: &W, + url: &str, + bounds: crate::backend::WebViewBounds, + ) -> wry::Result { + wry::WebViewBuilder::new() + .with_bounds(bounds.into()) + .with_url(url) + .build_as_child(parent_window) + } +} diff --git a/crates/viewer/re_view_web_page/src/testing.rs b/crates/viewer/re_view_web_page/src/testing.rs new file mode 100644 index 000000000000..879eeab5e8d7 --- /dev/null +++ b/crates/viewer/re_view_web_page/src/testing.rs @@ -0,0 +1,234 @@ +//! Test support for the Web Page View backend seam. + +use std::{cell::RefCell, collections::HashMap, sync::Arc}; + +use parking_lot::Mutex; +use re_viewer_context::{ViewId, ViewerContext}; + +use crate::backend::{ + WebViewBackend, WebViewBackendError, WebViewBounds, WebViewInstance, WebViewSession, +}; + +thread_local! { + static INSTALLED_BACKEND: RefCell> = const { RefCell::new(None) }; +} + +#[derive(Debug, Clone, Default)] +pub struct FakeWebViewBackend { + state: Arc>, +} + +#[derive(Debug, Default)] +struct FakeWebViewBackendState { + creation_error: Option, + created_instances: Vec, + bounds_updates: Vec, + destroyed_instances: Vec, + navigation_commands: Vec, + current_urls: HashMap, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct FakeCreatedWebView { + pub view_id: ViewId, + pub url: String, + pub session: String, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct FakeDestroyedWebView { + pub view_id: ViewId, + pub url: String, +} + +#[derive(Debug, Clone, Copy, PartialEq)] +pub struct FakeWebViewBounds { + pub min: [f32; 2], + pub size: [f32; 2], +} + +impl From for FakeWebViewBounds { + fn from(bounds: WebViewBounds) -> Self { + Self { + min: bounds.min, + size: bounds.size, + } + } +} + +#[derive(Debug, Clone, PartialEq)] +pub struct FakeBoundsUpdate { + pub view_id: ViewId, + pub bounds: FakeWebViewBounds, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum FakeNavigationCommand { + Back, + Forward, + Reload, + NavigateTo(String), +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct FakeNavigationCommandRequest { + pub view_id: ViewId, + pub command: FakeNavigationCommand, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct FakeNavigationRequest { + pub view_id: ViewId, + pub url: String, +} + +impl FakeWebViewBackend { + pub fn failing(message: impl Into) -> Self { + Self { + state: Arc::new(Mutex::new(FakeWebViewBackendState { + creation_error: Some(message.into()), + ..Default::default() + })), + } + } + + pub fn install(&self) -> FakeWebViewBackendGuard { + INSTALLED_BACKEND.with(|installed_backend| { + let previous_backend = installed_backend.replace(Some(self.clone())); + assert!( + previous_backend.is_none(), + "a fake Web Page View backend is already installed on this thread" + ); + }); + + FakeWebViewBackendGuard {} + } + + pub fn created_instance_count(&self) -> usize { + self.state.lock().created_instances.len() + } + + pub fn created_urls(&self) -> Vec { + self.state + .lock() + .created_instances + .iter() + .map(|created_instance| created_instance.url.clone()) + .collect() + } + + pub fn created_instances(&self) -> Vec { + self.state.lock().created_instances.clone() + } + + pub fn bounds_updates(&self) -> Vec { + self.state.lock().bounds_updates.clone() + } + + pub fn destroyed_instance_count(&self) -> usize { + self.state.lock().destroyed_instances.len() + } + + pub fn destroyed_instances(&self) -> Vec { + self.state.lock().destroyed_instances.clone() + } + + pub fn simulate_navigation(&self, view_id: ViewId, url: &str) { + self.state + .lock() + .current_urls + .insert(view_id, url.to_owned()); + } + + pub fn navigation_requests(&self) -> Vec { + self.state + .lock() + .navigation_commands + .iter() + .filter_map(|request| match &request.command { + FakeNavigationCommand::NavigateTo(url) => Some(FakeNavigationRequest { + view_id: request.view_id, + url: url.clone(), + }), + FakeNavigationCommand::Back + | FakeNavigationCommand::Forward + | FakeNavigationCommand::Reload => None, + }) + .collect() + } + + pub(crate) fn record_bounds_update(&self, view_id: ViewId, bounds: WebViewBounds) { + self.state.lock().bounds_updates.push(FakeBoundsUpdate { + view_id, + bounds: bounds.into(), + }); + } + + pub(crate) fn record_destroyed_instance(&self, view_id: ViewId, url: &str) { + self.state + .lock() + .destroyed_instances + .push(FakeDestroyedWebView { + view_id, + url: url.to_owned(), + }); + } + + pub(crate) fn record_navigation_command( + &self, + view_id: ViewId, + command: FakeNavigationCommand, + ) { + if let FakeNavigationCommand::NavigateTo(url) = &command { + self.state.lock().current_urls.insert(view_id, url.clone()); + } + + self.state + .lock() + .navigation_commands + .push(FakeNavigationCommandRequest { view_id, command }); + } +} + +pub struct FakeWebViewBackendGuard; + +impl Drop for FakeWebViewBackendGuard { + fn drop(&mut self) { + INSTALLED_BACKEND.with(|installed_backend| { + installed_backend.replace(None); + }); + } +} + +impl WebViewBackend for FakeWebViewBackend { + fn create( + &self, + _ctx: &ViewerContext<'_>, + view_id: ViewId, + url: &str, + _bounds: WebViewBounds, + session: WebViewSession, + ) -> Result { + let mut state = self.state.lock(); + + if let Some(message) = &state.creation_error { + return Err(WebViewBackendError::CreationFailed(message.clone())); + } + + state.created_instances.push(FakeCreatedWebView { + view_id, + url: url.to_owned(), + session: session.as_str().to_owned(), + }); + + Ok(WebViewInstance::new_fake( + view_id, + url.to_owned(), + self.clone(), + )) + } +} + +pub(crate) fn installed_backend() -> Option { + INSTALLED_BACKEND.with(|installed_backend| installed_backend.borrow().clone()) +} diff --git a/crates/viewer/re_view_web_page/src/url_policy.rs b/crates/viewer/re_view_web_page/src/url_policy.rs new file mode 100644 index 000000000000..4609a384776a --- /dev/null +++ b/crates/viewer/re_view_web_page/src/url_policy.rs @@ -0,0 +1,54 @@ +#[derive(Debug, Clone, PartialEq, Eq)] +pub(crate) enum UrlPolicyResult { + Accepted(String), + Invalid, + UnsupportedScheme(String), +} + +pub(crate) fn validate_url(url: &str) -> UrlPolicyResult { + match url::Url::parse(url) { + Ok(parsed_url) => match parsed_url.scheme() { + "http" | "https" => UrlPolicyResult::Accepted(url.to_owned()), + scheme => UrlPolicyResult::UnsupportedScheme(scheme.to_owned()), + }, + Err(_) => UrlPolicyResult::Invalid, + } +} + +#[cfg(test)] +mod tests { + use super::{UrlPolicyResult, validate_url}; + + #[test] + fn accepts_http_and_https_urls() { + assert_eq!( + validate_url("https://example.com"), + UrlPolicyResult::Accepted("https://example.com".to_owned()) + ); + assert_eq!( + validate_url("http://localhost:3000"), + UrlPolicyResult::Accepted("http://localhost:3000".to_owned()) + ); + } + + #[test] + fn rejects_unsupported_schemes() { + assert_eq!( + validate_url("file:///tmp/report.html"), + UrlPolicyResult::UnsupportedScheme("file".to_owned()) + ); + assert_eq!( + validate_url("javascript:alert(1)"), + UrlPolicyResult::UnsupportedScheme("javascript".to_owned()) + ); + assert_eq!( + validate_url("data:text/plain,hello"), + UrlPolicyResult::UnsupportedScheme("data".to_owned()) + ); + } + + #[test] + fn rejects_invalid_url_text() { + assert_eq!(validate_url("not a url"), UrlPolicyResult::Invalid); + } +} diff --git a/crates/viewer/re_view_web_page/src/view_class.rs b/crates/viewer/re_view_web_page/src/view_class.rs new file mode 100644 index 000000000000..5afbbe934a09 --- /dev/null +++ b/crates/viewer/re_view_web_page/src/view_class.rs @@ -0,0 +1,230 @@ +use re_log_types::EntityPath; +use re_sdk_types::blueprint::{ + archetypes::WebPageViewConfig, + components::{ShowNavigationControls, WebPageUrl}, +}; +use re_sdk_types::{View as _, ViewClassIdentifier}; +use re_ui::{Help, icons}; +use re_viewer_context::{ + ViewClass, ViewClassLayoutPriority, ViewClassRegistryError, ViewId, ViewQuery, + ViewSpawnHeuristics, ViewState, ViewSystemExecutionError, ViewerContext, +}; +use re_viewport_blueprint::ViewProperty; + +use crate::backend::WebViewBounds; +use crate::lifecycle::{WebViewLifecycle, WebViewLifecycleStatus}; +use crate::url_policy::{UrlPolicyResult, validate_url}; + +#[derive(Default)] +struct WebPageViewState { + lifecycle: WebViewLifecycle, +} + +impl ViewState for WebPageViewState { + fn as_any(&self) -> &dyn std::any::Any { + self + } + + fn as_any_mut(&mut self) -> &mut dyn std::any::Any { + self + } +} + +#[derive(Default)] +pub struct WebPageView; + +type ViewType = re_sdk_types::blueprint::views::WebPageView; + +#[derive(Debug, Clone, PartialEq, Eq)] +struct WebPageConfig { + url: Option, + show_navigation_controls: bool, +} + +impl WebPageConfig { + fn from_blueprint( + ctx: &ViewerContext<'_>, + query: &ViewQuery<'_>, + ) -> Result { + let config = ViewProperty::from_archetype::( + ctx.blueprint_db(), + ctx.blueprint_query, + query.view_id, + ); + + let url = config + .component_or_empty::(WebPageViewConfig::descriptor_url().component)? + .map(|url| url.0.0.as_str().to_owned()); + + let show_navigation_controls = config + .component_or_empty::( + WebPageViewConfig::descriptor_show_navigation_controls().component, + )? + .is_none_or(|show_navigation_controls| show_navigation_controls.0.0); + + Ok(Self { + url, + show_navigation_controls, + }) + } +} + +impl ViewClass for WebPageView { + fn identifier() -> ViewClassIdentifier { + ViewType::identifier() + } + + fn display_name(&self) -> &'static str { + "Web Page" + } + + fn icon(&self) -> &'static re_ui::Icon { + &icons::VIEW_GENERIC + } + + fn help(&self, _os: egui::os::OperatingSystem) -> Help { + Help::new("Web Page view") + .markdown("Displays a configured webpage inline in the native viewer.") + } + + fn on_register( + &self, + _system_registry: &mut re_viewer_context::ViewSystemRegistrator<'_>, + ) -> Result<(), ViewClassRegistryError> { + Ok(()) + } + + fn new_state(&self) -> Box { + Box::::default() + } + + fn layout_priority(&self) -> ViewClassLayoutPriority { + ViewClassLayoutPriority::Low + } + + fn spawn_heuristics( + &self, + _ctx: &ViewerContext<'_>, + _include_entity: &dyn Fn(&EntityPath) -> bool, + ) -> ViewSpawnHeuristics { + ViewSpawnHeuristics::empty() + } + + fn selection_ui( + &self, + _ctx: &ViewerContext<'_>, + _ui: &mut egui::Ui, + _state: &mut dyn ViewState, + _space_origin: &EntityPath, + _view_id: ViewId, + ) -> Result<(), ViewSystemExecutionError> { + Ok(()) + } + + fn ui( + &self, + ctx: &ViewerContext<'_>, + _missing_chunk_reporter: &re_viewer_context::MissingChunkReporter, + ui: &mut egui::Ui, + state: &mut dyn ViewState, + query: &ViewQuery<'_>, + _system_output: re_viewer_context::SystemExecutionOutput, + ) -> Result<(), ViewSystemExecutionError> { + re_tracing::profile_function!(); + + let config = WebPageConfig::from_blueprint(ctx, query)?; + let Some(url) = config.url else { + ui.centered_and_justified(|ui| { + ui.label("No URL configured"); + }); + return Ok(()); + }; + + let url = match validate_url(&url) { + UrlPolicyResult::Accepted(url) => url, + UrlPolicyResult::Invalid => { + ui.centered_and_justified(|ui| { + ui.label("Invalid URL"); + }); + return Ok(()); + } + UrlPolicyResult::UnsupportedScheme(scheme) => { + ui.centered_and_justified(|ui| { + ui.label(format!("Unsupported URL scheme: {scheme}")); + }); + return Ok(()); + } + }; + + let state = state + .as_any_mut() + .downcast_mut::() + .ok_or(ViewSystemExecutionError::StateCastError("WebPageViewState"))?; + if config.show_navigation_controls { + let mut navigation_command = None; + ui.horizontal(|ui| { + if ui.button("Back").clicked() { + navigation_command = Some(NavigationCommand::Back); + } + if ui.button("Forward").clicked() { + navigation_command = Some(NavigationCommand::Forward); + } + if ui.button("Reload").clicked() { + navigation_command = Some(NavigationCommand::Reload); + } + if ui.button("Home").clicked() { + navigation_command = Some(NavigationCommand::Home); + } + ui.label(&url); + }); + ui.separator(); + + match navigation_command { + Some(NavigationCommand::Back) => state.lifecycle.go_back(), + Some(NavigationCommand::Forward) => state.lifecycle.go_forward(), + Some(NavigationCommand::Reload) => state.lifecycle.reload(), + Some(NavigationCommand::Home) => state.lifecycle.navigate_to(&url), + None => {} + } + } else { + ui.label(&url); + } + + let webview_rect = ui.available_rect_before_wrap(); + let webview_bounds = + WebViewBounds::from_egui_rect(webview_rect, ui.ctx().pixels_per_point()); + let lifecycle_status = + state + .lifecycle + .ensure_webview(ctx, query.view_id, &url, webview_bounds); + + match lifecycle_status { + WebViewLifecycleStatus::Ready => { + state.lifecycle.update_bounds(query.view_id, webview_bounds); + ui.allocate_rect(webview_rect, egui::Sense::hover()); + } + WebViewLifecycleStatus::Unavailable => { + ui.centered_and_justified(|ui| { + ui.label("Embedded webview unavailable"); + }); + } + WebViewLifecycleStatus::CreationFailed(message) => { + ui.centered_and_justified(|ui| { + ui.vertical_centered(|ui| { + ui.label("Failed to create embedded webview"); + ui.label(message); + }); + }); + } + } + + Ok(()) + } +} + +enum NavigationCommand { + Back, + Forward, + Reload, + Home, +} diff --git a/crates/viewer/re_view_web_page/tests/native_backend.rs b/crates/viewer/re_view_web_page/tests/native_backend.rs new file mode 100644 index 000000000000..fb5a307f5f65 --- /dev/null +++ b/crates/viewer/re_view_web_page/tests/native_backend.rs @@ -0,0 +1,14 @@ +#![cfg(all(not(target_arch = "wasm32"), feature = "native_webview"))] + +use re_view_web_page::native_backend::{NativeWebViewBackend, NativeWebViewError}; + +#[test] +fn native_backend_reports_missing_parent_window_without_panicking() { + let result = + NativeWebViewBackend::default().create_without_parent_for_smoke_test("https://example.com"); + + assert!(matches!( + result, + Err(NativeWebViewError::MissingParentWindow) + )); +} diff --git a/crates/viewer/re_view_web_page/tests/web_page_view.rs b/crates/viewer/re_view_web_page/tests/web_page_view.rs new file mode 100644 index 000000000000..b8e5577252cf --- /dev/null +++ b/crates/viewer/re_view_web_page/tests/web_page_view.rs @@ -0,0 +1,494 @@ +use egui_kittest::kittest::Queryable as _; +use re_sdk_types::blueprint::archetypes::WebPageViewConfig; +use re_test_context::TestContext; +use re_test_viewport::TestContextExt as _; +use re_view_web_page::WebPageView; +use re_view_web_page::testing::FakeWebViewBackend; +use re_viewer_context::{BlueprintContext as _, ViewClass as _, ViewerContext}; +use re_viewport_blueprint::{ViewBlueprint, ViewProperty, ViewportBlueprint}; + +#[test] +fn manually_created_web_page_view_without_url_shows_status() { + let mut test_context = TestContext::new_with_view_class::(); + + let view_id = test_context.setup_viewport_blueprint(|_ctx, blueprint| { + blueprint.add_view_at_root(ViewBlueprint::new_with_root_wildcard( + WebPageView::identifier(), + )) + }); + + let mut harness = test_context + .setup_kittest_for_rendering_ui([500.0, 250.0]) + .build_ui(|ui| { + test_context.run_with_single_view(ui, view_id); + }); + + harness.run(); + + assert!( + harness + .query_by_label_contains("No URL configured") + .is_some(), + "expected Web Page View to explain that no URL is configured" + ); +} + +#[test] +fn blueprint_configured_web_page_view_reads_url_and_navigation_preference_without_logged_data() { + let mut test_context = TestContext::new_with_view_class::(); + + let view_id = setup_configured_web_page_view(&mut test_context, "https://example.com", false); + + let mut harness = test_context + .setup_kittest_for_rendering_ui([500.0, 250.0]) + .build_ui(|ui| { + test_context.run_with_single_view(ui, view_id); + }); + + harness.run(); + + assert!( + harness + .query_by_label_contains("https://example.com") + .is_some(), + "expected Web Page View to read and display the configured URL" + ); + assert!( + harness + .query_by_label_contains("No URL configured") + .is_none(), + "expected configured Web Page View not to render the missing-URL status" + ); + assert!( + harness.query_by_label_contains("Back").is_none(), + "expected navigation controls to be hidden when show_navigation_controls is false" + ); +} + +#[test] +fn https_url_is_accepted() { + let mut test_context = TestContext::new_with_view_class::(); + let view_id = setup_configured_web_page_view(&mut test_context, "https://example.com", true); + + let mut harness = test_context + .setup_kittest_for_rendering_ui([500.0, 250.0]) + .build_ui(|ui| { + test_context.run_with_single_view(ui, view_id); + }); + + harness.run(); + + assert!( + harness + .query_by_label_contains("https://example.com") + .is_some() + ); + assert!(harness.query_by_label_contains("Invalid URL").is_none()); + assert!( + harness + .query_by_label_contains("Unsupported URL scheme") + .is_none() + ); +} + +#[test] +fn localhost_http_url_is_accepted() { + let mut test_context = TestContext::new_with_view_class::(); + let view_id = setup_configured_web_page_view(&mut test_context, "http://localhost:3000", true); + + let mut harness = test_context + .setup_kittest_for_rendering_ui([500.0, 250.0]) + .build_ui(|ui| { + test_context.run_with_single_view(ui, view_id); + }); + + harness.run(); + + assert!( + harness + .query_by_label_contains("http://localhost:3000") + .is_some() + ); + assert!(harness.query_by_label_contains("Invalid URL").is_none()); + assert!( + harness + .query_by_label_contains("Unsupported URL scheme") + .is_none() + ); +} + +#[test] +fn navigation_controls_are_visible_by_default() { + let mut test_context = TestContext::new_with_view_class::(); + let view_id = setup_web_page_view_with_default_navigation_controls( + &mut test_context, + "https://example.com", + ); + + let mut harness = test_context + .setup_kittest_for_rendering_ui([500.0, 250.0]) + .build_ui(|ui| { + test_context.run_with_single_view(ui, view_id); + }); + + harness.run(); + + assert!(harness.query_by_label_contains("Back").is_some()); + assert!(harness.query_by_label_contains("Forward").is_some()); + assert!(harness.query_by_label_contains("Reload").is_some()); + assert!(harness.query_by_label_contains("Home").is_some()); +} + +#[test] +fn navigation_controls_can_be_hidden() { + let mut test_context = TestContext::new_with_view_class::(); + let view_id = setup_configured_web_page_view(&mut test_context, "https://example.com", false); + + let mut harness = test_context + .setup_kittest_for_rendering_ui([500.0, 250.0]) + .build_ui(|ui| { + test_context.run_with_single_view(ui, view_id); + }); + + harness.run(); + + assert!(harness.query_by_label_contains("Back").is_none()); + assert!(harness.query_by_label_contains("Forward").is_none()); + assert!(harness.query_by_label_contains("Reload").is_none()); + assert!(harness.query_by_label_contains("Home").is_none()); +} + +#[test] +fn home_navigation_does_not_mutate_configured_url() { + let fake_backend = FakeWebViewBackend::default(); + let _backend_guard = fake_backend.install(); + + let configured_url = "https://example.com/home"; + let mut test_context = TestContext::new_with_view_class::(); + let view_id = setup_configured_web_page_view(&mut test_context, configured_url, true); + + let mut harness = test_context + .setup_kittest_for_rendering_ui([500.0, 250.0]) + .build_ui(|ui| { + test_context.run_with_single_view(ui, view_id); + }); + harness.run(); + + fake_backend.simulate_navigation(view_id, "https://example.com/runtime"); + harness.get_by_label("Home").click(); + harness.run(); + + let navigation_requests = fake_backend.navigation_requests(); + assert_eq!(navigation_requests.len(), 1); + assert_eq!(navigation_requests[0].view_id, view_id); + assert_eq!(navigation_requests[0].url, configured_url); + assert!(harness.query_by_label_contains(configured_url).is_some()); +} + +#[test] +fn file_url_shows_unsupported_scheme_status() { + let mut test_context = TestContext::new_with_view_class::(); + let view_id = + setup_configured_web_page_view(&mut test_context, "file:///tmp/report.html", true); + + let mut harness = test_context + .setup_kittest_for_rendering_ui([500.0, 250.0]) + .build_ui(|ui| { + test_context.run_with_single_view(ui, view_id); + }); + + harness.run(); + + assert!( + harness + .query_by_label_contains("Unsupported URL scheme") + .is_some(), + "expected file URLs to be rejected by Rerun-side status UI" + ); +} + +#[test] +fn invalid_url_text_shows_invalid_status() { + let mut test_context = TestContext::new_with_view_class::(); + let view_id = setup_configured_web_page_view(&mut test_context, "not a url", true); + + let mut harness = test_context + .setup_kittest_for_rendering_ui([500.0, 250.0]) + .build_ui(|ui| { + test_context.run_with_single_view(ui, view_id); + }); + + harness.run(); + + assert!( + harness.query_by_label_contains("Invalid URL").is_some(), + "expected invalid URL text to be rejected by Rerun-side status UI" + ); +} + +#[test] +fn unavailable_native_backend_shows_status_outside_webview() { + let mut test_context = TestContext::new_with_view_class::(); + let view_id = setup_configured_web_page_view(&mut test_context, "https://example.com", true); + + let mut harness = test_context + .setup_kittest_for_rendering_ui([500.0, 250.0]) + .build_ui(|ui| { + test_context.run_with_single_view(ui, view_id); + }); + + harness.run(); + + assert!( + harness + .query_by_label_contains("Embedded webview unavailable") + .is_some(), + "expected explicit status when no native backend is available" + ); +} + +#[test] +fn backend_creation_failure_shows_status_outside_webview() { + let fake_backend = FakeWebViewBackend::failing("backend failed"); + let _backend_guard = fake_backend.install(); + + let mut test_context = TestContext::new_with_view_class::(); + let view_id = setup_configured_web_page_view(&mut test_context, "https://example.com", true); + + let mut harness = test_context + .setup_kittest_for_rendering_ui([500.0, 250.0]) + .build_ui(|ui| { + test_context.run_with_single_view(ui, view_id); + }); + + harness.run(); + + assert!( + harness + .query_by_label_contains("Failed to create embedded webview") + .is_some(), + "expected explicit status when backend creation fails" + ); + assert!(harness.query_by_label_contains("backend failed").is_some()); +} + +#[test] +fn valid_configured_url_creates_one_backend_webview_instance() { + let fake_backend = FakeWebViewBackend::default(); + let _backend_guard = fake_backend.install(); + + let mut test_context = TestContext::new_with_view_class::(); + let view_id = setup_configured_web_page_view(&mut test_context, "https://example.com", true); + + let mut harness = test_context + .setup_kittest_for_rendering_ui([500.0, 250.0]) + .build_ui(|ui| { + test_context.run_with_single_view(ui, view_id); + }); + + harness.run(); + + assert_eq!(fake_backend.created_instance_count(), 1); + assert_eq!(fake_backend.created_urls(), ["https://example.com"]); +} + +#[test] +fn two_web_page_views_create_independent_backend_webview_instances() { + let fake_backend = FakeWebViewBackend::default(); + let _backend_guard = fake_backend.install(); + + let mut test_context = TestContext::new_with_view_class::(); + let (first_view_id, second_view_id) = + test_context.setup_viewport_blueprint(|ctx, blueprint| { + let first_view_id = + add_configured_web_page_view(ctx, blueprint, "https://example.com/first", true); + let second_view_id = + add_configured_web_page_view(ctx, blueprint, "https://example.com/second", true); + (first_view_id, second_view_id) + }); + + let mut harness = test_context + .setup_kittest_for_rendering_ui([500.0, 500.0]) + .build_ui(|ui| { + test_context.run_with_single_view(ui, first_view_id); + test_context.run_with_single_view(ui, second_view_id); + }); + + harness.run(); + + let created_instances = fake_backend.created_instances(); + assert_eq!(created_instances.len(), 2); + assert_eq!(created_instances[0].view_id, first_view_id); + assert_eq!(created_instances[0].url, "https://example.com/first"); + assert_eq!(created_instances[1].view_id, second_view_id); + assert_eq!(created_instances[1].url, "https://example.com/second"); +} + +#[test] +fn multiple_web_page_views_use_shared_default_browser_session() { + let fake_backend = FakeWebViewBackend::default(); + let _backend_guard = fake_backend.install(); + + let mut test_context = TestContext::new_with_view_class::(); + let (first_view_id, second_view_id) = + test_context.setup_viewport_blueprint(|ctx, blueprint| { + let first_view_id = + add_configured_web_page_view(ctx, blueprint, "https://example.com/first", true); + let second_view_id = + add_configured_web_page_view(ctx, blueprint, "https://example.com/second", true); + (first_view_id, second_view_id) + }); + + let mut harness = test_context + .setup_kittest_for_rendering_ui([500.0, 500.0]) + .build_ui(|ui| { + test_context.run_with_single_view(ui, first_view_id); + test_context.run_with_single_view(ui, second_view_id); + }); + + harness.run(); + + let created_instances = fake_backend.created_instances(); + assert_eq!(created_instances.len(), 2); + assert_eq!(created_instances[0].session, created_instances[1].session); + assert_eq!(created_instances[0].session.as_str(), "shared-default"); +} + +#[test] +fn backend_receives_updated_bounds_when_view_rect_changes() { + let fake_backend = FakeWebViewBackend::default(); + let _backend_guard = fake_backend.install(); + + let mut test_context = TestContext::new_with_view_class::(); + let view_id = setup_configured_web_page_view(&mut test_context, "https://example.com", true); + + let mut first_harness = test_context + .setup_kittest_for_rendering_ui([500.0, 250.0]) + .build_ui(|ui| { + test_context.run_with_single_view(ui, view_id); + }); + first_harness.run(); + + let mut second_harness = test_context + .setup_kittest_for_rendering_ui([700.0, 350.0]) + .build_ui(|ui| { + test_context.run_with_single_view(ui, view_id); + }); + second_harness.run(); + + let bounds_updates = fake_backend.bounds_updates(); + assert!( + bounds_updates.len() >= 2, + "expected bounds updates from both rendered view sizes" + ); + assert!( + bounds_updates + .iter() + .all(|bounds_update| bounds_update.view_id == view_id) + ); + assert_ne!( + bounds_updates.first().unwrap().bounds.size, + bounds_updates.last().unwrap().bounds.size + ); +} + +#[test] +fn hidden_web_page_view_keeps_backend_instance_alive() { + let fake_backend = FakeWebViewBackend::default(); + let _backend_guard = fake_backend.install(); + + let mut test_context = TestContext::new_with_view_class::(); + let view_id = setup_configured_web_page_view(&mut test_context, "https://example.com", true); + + let mut harness = test_context + .setup_kittest_for_rendering_ui([500.0, 250.0]) + .build_ui(|ui| { + test_context.run_with_single_view(ui, view_id); + }); + harness.run(); + + assert_eq!(fake_backend.created_instance_count(), 1); + + // Simulate a hidden tab by not rendering the view for a frame. The view state remains owned by + // the viewer and therefore must keep its native webview alive. + let mut hidden_harness = test_context + .setup_kittest_for_rendering_ui([500.0, 250.0]) + .build_ui(|_ui| {}); + hidden_harness.run(); + + assert_eq!(fake_backend.destroyed_instance_count(), 0); +} + +#[test] +fn removed_web_page_view_destroys_backend_instance() { + let fake_backend = FakeWebViewBackend::default(); + let _backend_guard = fake_backend.install(); + + let mut test_context = TestContext::new_with_view_class::(); + let view_id = setup_configured_web_page_view(&mut test_context, "https://example.com", true); + + let mut harness = test_context + .setup_kittest_for_rendering_ui([500.0, 250.0]) + .build_ui(|ui| { + test_context.run_with_single_view(ui, view_id); + }); + harness.run(); + + assert_eq!(fake_backend.created_instance_count(), 1); + + test_context + .view_states + .lock() + .retain_for_views(&test_context.recording_store_id, []); + + assert_eq!(fake_backend.destroyed_instance_count(), 1); +} + +fn setup_configured_web_page_view( + test_context: &mut TestContext, + url: &str, + show_navigation_controls: bool, +) -> re_viewer_context::ViewId { + test_context.setup_viewport_blueprint(|ctx, blueprint| { + add_configured_web_page_view(ctx, blueprint, url, show_navigation_controls) + }) +} + +fn setup_web_page_view_with_default_navigation_controls( + test_context: &mut TestContext, + url: &str, +) -> re_viewer_context::ViewId { + test_context.setup_viewport_blueprint(|ctx, blueprint| { + let view = ViewBlueprint::new_with_root_wildcard(WebPageView::identifier()); + let config = ViewProperty::from_archetype::( + ctx.blueprint_db(), + ctx.blueprint_query, + view.id, + ); + + ctx.save_blueprint_archetype(config.blueprint_store_path, &WebPageViewConfig::new(url)); + + blueprint.add_view_at_root(view) + }) +} + +fn add_configured_web_page_view( + ctx: &ViewerContext<'_>, + blueprint: &mut ViewportBlueprint, + url: &str, + show_navigation_controls: bool, +) -> re_viewer_context::ViewId { + let view = ViewBlueprint::new_with_root_wildcard(WebPageView::identifier()); + let config = ViewProperty::from_archetype::( + ctx.blueprint_db(), + ctx.blueprint_query, + view.id, + ); + + ctx.save_blueprint_archetype( + config.blueprint_store_path, + &WebPageViewConfig::new(url).with_show_navigation_controls(show_navigation_controls), + ); + + blueprint.add_view_at_root(view) +} diff --git a/crates/viewer/re_viewer/Cargo.toml b/crates/viewer/re_viewer/Cargo.toml index ca70511c5b83..0911b5ff42f9 100644 --- a/crates/viewer/re_viewer/Cargo.toml +++ b/crates/viewer/re_viewer/Cargo.toml @@ -43,6 +43,11 @@ analytics = ["dep:re_analytics", "re_ui/analytics"] ## Enable the map view map_view = ["dep:re_view_map"] +## Enable the native Web Page View backend. +## +## This only works on native. +native_webview = ["re_view_web_page/native_webview"] + ## Enables integration with `re_perf_telemetry` (OpenTelemetry, Jaeger). ## ## This only works on native. @@ -108,6 +113,7 @@ re_view_tensor.workspace = true re_view_text_document.workspace = true re_view_text_log.workspace = true re_view_time_series.workspace = true +re_view_web_page.workspace = true re_viewer_context.workspace = true re_viewport_blueprint.workspace = true re_viewport.workspace = true diff --git a/crates/viewer/re_viewer/src/app.rs b/crates/viewer/re_viewer/src/app.rs index 5267815fd104..3ce8c0c52722 100644 --- a/crates/viewer/re_viewer/src/app.rs +++ b/crates/viewer/re_viewer/src/app.rs @@ -2527,6 +2527,33 @@ impl App { let empty_store_context = ActiveStoreContext::empty(); let active_store_context = store_context.unwrap_or(&empty_store_context); + #[cfg(all(not(target_arch = "wasm32"), feature = "native_webview"))] + re_view_web_page::native_backend::with_native_parent_window(frame, || { + self.state.show( + &self.app_env, + &self.startup_options, + app_blueprint, + ui, + render_ctx, + active_store_context, + storage_context, + &self.reflection, + &self.component_ui_registry, + &self.component_fallback_registry, + &self.view_class_registry, + &self.rx_log, + &self.command_sender, + &WelcomeScreenState { + hide_examples: self.startup_options.hide_welcome_screen, + opacity: self.welcome_screen_opacity(ui), + }, + self.event_dispatcher.as_ref(), + &self.connection_registry, + &self.async_runtime, + ); + }); + + #[cfg(any(target_arch = "wasm32", not(feature = "native_webview")))] self.state.show( &self.app_env, &self.startup_options, diff --git a/crates/viewer/re_viewer/src/blueprint/validation_gen/mod.rs b/crates/viewer/re_viewer/src/blueprint/validation_gen/mod.rs index af6f3504e9b1..1a1de0c009d5 100644 --- a/crates/viewer/re_viewer/src/blueprint/validation_gen/mod.rs +++ b/crates/viewer/re_viewer/src/blueprint/validation_gen/mod.rs @@ -40,6 +40,7 @@ pub use re_sdk_types::blueprint::components::QueryExpression; pub use re_sdk_types::blueprint::components::RootContainer; pub use re_sdk_types::blueprint::components::RowShare; pub use re_sdk_types::blueprint::components::SelectedColumns; +pub use re_sdk_types::blueprint::components::ShowNavigationControls; pub use re_sdk_types::blueprint::components::TensorDimensionIndexSlider; pub use re_sdk_types::blueprint::components::TextLogColumn; pub use re_sdk_types::blueprint::components::TimeInt; @@ -56,6 +57,7 @@ pub use re_sdk_types::blueprint::components::VisualBounds2D; pub use re_sdk_types::blueprint::components::VisualizerComponentMapping; pub use re_sdk_types::blueprint::components::VisualizerInstructionId; pub use re_sdk_types::blueprint::components::VisualizerType; +pub use re_sdk_types::blueprint::components::WebPageUrl; pub use re_sdk_types::blueprint::components::ZoomLevel; /// Because blueprints are both read and written the schema must match what @@ -98,6 +100,7 @@ pub fn is_valid_blueprint(blueprint: &EntityDb) -> bool { && validate_component::(blueprint) && validate_component::(blueprint) && validate_component::(blueprint) + && validate_component::(blueprint) && validate_component::(blueprint) && validate_component::(blueprint) && validate_component::(blueprint) @@ -114,5 +117,6 @@ pub fn is_valid_blueprint(blueprint: &EntityDb) -> bool { && validate_component::(blueprint) && validate_component::(blueprint) && validate_component::(blueprint) + && validate_component::(blueprint) && validate_component::(blueprint) } diff --git a/crates/viewer/re_viewer/src/default_views.rs b/crates/viewer/re_viewer/src/default_views.rs index b7ca47cefa4b..cb416dd5be18 100644 --- a/crates/viewer/re_viewer/src/default_views.rs +++ b/crates/viewer/re_viewer/src/default_views.rs @@ -77,6 +77,11 @@ fn populate_view_class_registry_with_builtin( app_options, fallback_registry, )?; + view_class_registry.add_class::( + reflection, + app_options, + fallback_registry, + )?; if app_options.experimental.enable_status_view { view_class_registry.add_class::( reflection, diff --git a/crates/viewer/re_viewer_context/src/view/view_states.rs b/crates/viewer/re_viewer_context/src/view/view_states.rs index 0da995bdcd8c..f33c055134d0 100644 --- a/crates/viewer/re_viewer_context/src/view/view_states.rs +++ b/crates/viewer/re_viewer_context/src/view/view_states.rs @@ -3,7 +3,7 @@ //! The `Viewer` has ownership of this state and pass it around to users (mainly viewport and //! selection panel). -use ahash::HashMap; +use ahash::{HashMap, HashSet}; use re_log_types::StoreId; @@ -45,6 +45,22 @@ impl re_byte_size::SizeBytes for ViewStates { } impl ViewStates { + pub fn retain_for_views( + &mut self, + store_id: &StoreId, + retained_view_ids: impl IntoIterator, + ) { + let retained_view_ids: HashSet = retained_view_ids.into_iter().collect(); + + self.states.retain(|(state_store_id, view_id), _| { + state_store_id != store_id || retained_view_ids.contains(view_id) + }); + self.visualizer_reports + .retain(|(state_store_id, view_id), _| { + state_store_id != store_id || retained_view_ids.contains(view_id) + }); + } + pub fn get(&self, store_id: &StoreId, view_id: ViewId) -> Option<&dyn ViewState> { self.states .get(&(store_id.clone(), view_id)) diff --git a/crates/viewer/re_viewport/src/viewport_ui.rs b/crates/viewer/re_viewport/src/viewport_ui.rs index a77f18b29741..9c6f5ca77e32 100644 --- a/crates/viewer/re_viewport/src/viewport_ui.rs +++ b/crates/viewer/re_viewport/src/viewport_ui.rs @@ -86,6 +86,7 @@ impl ViewportUi { }; // Reset all error states. + view_states.retain_for_views(ctx.store_id(), blueprint.view_ids().copied()); view_states.reset_visualizer_reports(); let executed_systems_per_view = diff --git a/docs/content/reference/types/views.md b/docs/content/reference/types/views.md index 46f210af41d2..4058b9255c7b 100644 --- a/docs/content/reference/types/views.md +++ b/docs/content/reference/types/views.md @@ -18,4 +18,5 @@ Views are the panels shown in the viewer's viewport and the primary means of ins * [`TextDocumentView`](views/text_document_view.md): A view of a single text document, for use with [`archetypes.TextDocument`](https://rerun.io/docs/reference/types/archetypes/text_document). * [`TextLogView`](views/text_log_view.md): A view of a text log, for use with [`archetypes.TextLog`](https://rerun.io/docs/reference/types/archetypes/text_log). * [`TimeSeriesView`](views/time_series_view.md): A time series view for scalars over time, for use with [`archetypes.Scalars`](https://rerun.io/docs/reference/types/archetypes/scalars). +* [`WebPageView`](views/web_page_view.md): A native-only view that embeds a configured web page. diff --git a/docs/content/reference/types/views/.gitattributes b/docs/content/reference/types/views/.gitattributes index b08d56045cc4..fc0234b7d24a 100644 --- a/docs/content/reference/types/views/.gitattributes +++ b/docs/content/reference/types/views/.gitattributes @@ -12,3 +12,4 @@ tensor_view.md linguist-generated=true text_document_view.md linguist-generated=true text_log_view.md linguist-generated=true time_series_view.md linguist-generated=true +web_page_view.md linguist-generated=true diff --git a/docs/content/reference/types/views/web_page_view.md b/docs/content/reference/types/views/web_page_view.md new file mode 100644 index 000000000000..692ba981d87c --- /dev/null +++ b/docs/content/reference/types/views/web_page_view.md @@ -0,0 +1,30 @@ +--- +title: "WebPageView" +--- + + +⚠️ **This type is _unstable_ and may change significantly in a way that the data won't be backwards compatible.** +A native-only view that embeds a configured web page. + +Web Page View is unsupported in the web viewer. Native support depends on the +operating-system webview runtime provided through `wry`: `WebView2` on Windows, +`WebKit` on macOS, and `WebKitGTK` on Linux. Linux child webviews may also depend +on the active display-server integration; the direct child-window path is +X11-oriented, while Wayland-capable embedding requires a GTK container path. + +## Properties + +### `config` +Configuration for the web page to embed. + +* `url`: The initial URL to load. +* `show_navigation_controls`: Whether browser navigation controls should be shown. + +## API reference links + * 🐍 [Python API docs for `WebPageView`](https://ref.rerun.io/docs/python/stable/common/blueprint_views?speculative-link#rerun.blueprint.views.WebPageView) + + +## Visualized archetypes + +* [`WebPageViewConfig`](../archetypes/web_page_view_config.md) + diff --git a/openspec/changes/add-native-web-page-view/.openspec.yaml b/openspec/changes/add-native-web-page-view/.openspec.yaml new file mode 100644 index 000000000000..8fe205551914 --- /dev/null +++ b/openspec/changes/add-native-web-page-view/.openspec.yaml @@ -0,0 +1,2 @@ +schema: spec-driven +created: 2026-06-12 diff --git a/openspec/changes/add-native-web-page-view/design.md b/openspec/changes/add-native-web-page-view/design.md new file mode 100644 index 000000000000..4b0ddbc75621 --- /dev/null +++ b/openspec/changes/add-native-web-page-view/design.md @@ -0,0 +1,92 @@ +## Context + +Rerun's native viewer supports composable view types such as spatial, image, map, text, tensor, and dataframe views. Built-in views are registered through the viewer's view class registry, and view-specific configuration is represented as blueprint state generated from blueprint type definitions. + +There is currently URL-opening plumbing for loading Rerun data sources and opening browser URLs, but no view type that owns an embedded browser surface. The closest existing pattern is the map view: it is a registered view class with generated blueprint properties and native UI behavior. + +The Web Page View introduces a native-only view that displays a configured `http(s)` page inline in the viewer layout. It is not a visualizer for logged entities and has no timeline semantics. + +## Goals / Non-Goals + +**Goals:** + +- Add a first-class Web Page View that can be manually added to a layout and preconfigured through blueprint state. +- Store the initial/home URL and navigation chrome preference as view configuration. +- Display one live embedded native webview per Web Page View on wry-supported native platforms. +- Validate URL schemes before creating/loading the webview. +- Provide clear Rerun-side status UI for missing configuration, invalid URLs, unsupported targets, and backend creation failures. +- Shape implementation tasks as TDD vertical slices: one observable behavior test, minimal implementation, repeat. + +**Non-Goals:** + +- Supporting Web Page View in the web viewer. +- Driving the displayed URL from logged timeline data. +- Auto-spawning Web Page Views from entity contents. +- Supporting `file:`, `data:`, `javascript:`, or custom URL schemes. +- Implementing per-view isolated browser profiles in the initial version. +- Adding advanced browser options such as custom user agent, devtools configuration, zoom factor, transparent background, or script injection. + +## Decisions + +### Native-only Web Page View + +The Web Page View is available only in native viewer builds. The web viewer should render a clear unsupported status instead of attempting iframe support. + +**Alternatives considered:** +- Support the web viewer with iframes. Rejected because the web viewer already runs inside a browser and iframe behavior depends on cross-origin policy, CSP, and `X-Frame-Options`. +- Link-only view. Rejected because the desired behavior is an inline embedded webpage, not a shortcut to an external browser. + +### Blueprint/view configuration, not logged data + +The view's `url` and `show_navigation_controls` fields are blueprint/view configuration. The view has no data visualizer and should not be suggested from logged entities. + +**Alternatives considered:** +- Introduce a logged webpage archetype. Rejected for the initial version because webpage selection is layout configuration, not time-aware data in the requested use case. +- Support both logged and blueprint URLs. Deferred to avoid conflicting ownership of the current page. + +### Direct wry integration + +Use a native integration layer backed by `wry` rather than an egui wrapper crate. The integration should translate each view's egui rectangle into native webview bounds and manage webview lifecycle separately from egui painting. + +**Alternatives considered:** +- Third-party egui webview wrappers. Rejected as the product direction because available wrappers appear experimental and would still depend on wry/platform behavior. +- Browserless rendering or screenshots. Rejected because the view must display live interactive webpages. + +### One webview instance per view + +Each Web Page View owns one native webview instance. Multiple views therefore render independently and can show multiple live pages at once. + +**Alternatives considered:** +- Share one webview instance between views. Rejected because it conflicts with Rerun's composable layout model. +- Limit to one Web Page View per app. Rejected as an artificial limitation. + +### Keep instances alive while views exist + +The initial behavior keeps a webview alive while its Rerun view exists, including when the view is temporarily hidden. The instance is destroyed when the view is removed or the viewer exits. + +**Alternatives considered:** +- Destroy on hide. Rejected because it would reload dashboards, lose scroll position, and disrupt login/application state. + +### Browser-like navigation with stable configured URL + +The configured `url` is the initial/home URL. Runtime navigation inside the page does not mutate blueprint state. If navigation controls are visible, home returns to the configured URL. + +**Alternatives considered:** +- Lock navigation to the configured URL. Rejected because normal webpages rely on links and redirects. +- Persist every navigation into blueprint state. Rejected because casual browsing should not dirty saved layout configuration. + +### Shared browser session by default + +Web Page Views share the default embedded browser session/profile in the initial version. Per-view isolation should remain possible as a future extension. + +**Alternatives considered:** +- Isolate each view by default. Rejected because common dashboard use cases would require repeated logins and duplicate session setup. + +## Risks / Trade-offs + +- Native webview surfaces may not clip, stack, or resize exactly like egui widgets → Keep the webview integration behind a narrow module boundary, update bounds from the egui view rectangle, and test split panels/tabs/resizing. +- Linux support depends on WebKitGTK/display-server details → Treat support as wry-supported native platforms, document dependencies, and render backend errors clearly. +- WebView2/WebKit runtime dependencies may be missing → Detect creation failures and show Rerun-side status UI instead of crashing. +- Shared session state is convenient but less isolated → Keep per-view profile configuration out of v1 while avoiding API choices that prevent it later. +- Embedded arbitrary webpages can consume significant CPU/memory → Keep instances alive for usability in v1; consider future suspend/destroy policies if resource usage becomes a problem. +- Native child views can complicate input focus and keyboard shortcuts → Ensure focus transfer between egui and webview is tested, especially navigation controls and viewer shortcuts. diff --git a/openspec/changes/add-native-web-page-view/proposal.md b/openspec/changes/add-native-web-page-view/proposal.md new file mode 100644 index 000000000000..451ab1ccb470 --- /dev/null +++ b/openspec/changes/add-native-web-page-view/proposal.md @@ -0,0 +1,32 @@ +## Why + +Rerun users can compose rich native viewer layouts for logged data, but cannot place a live webpage alongside 3D, image, and other views. A native inline Web Page View enables dashboards, local robot control panels, documentation, and other http(s) pages to appear inside the Rerun viewer layout instead of requiring a separate browser window. + +## What Changes + +- Add a native-only **Web Page View** that displays a configured webpage inline in the viewer layout. +- Store the view's URL and lightweight browser chrome preference as blueprint/view configuration, not logged timeline data. +- Allow manual creation through the viewer UI and preconfiguration through blueprint state. +- Autoload configured `http://` and `https://` URLs, including localhost and private-network URLs. +- Reject unsupported URL schemes such as `file:`, `data:`, `javascript:`, and custom schemes with clear Rerun-side status UI. +- Use direct native webview integration for wry-supported native platforms. +- Keep runtime navigation browser-like while leaving the configured URL unchanged. +- Share embedded-browser session state by default; per-view isolated profiles remain out of scope for the initial version. +- Do not support this view in the web viewer. + +## Capabilities + +### New Capabilities +- `native-web-page-view`: Defines native Web Page View behavior, configuration, platform support, URL policy, lifecycle, navigation, session handling, and error reporting. + +### Modified Capabilities + +None. + +## Impact + +- Viewer view registration and manual view creation UI. +- Blueprint view definitions and generated SDK/view property types. +- Native viewer platform integration and dependency graph for embedded webviews. +- Native-only runtime lifecycle for one webview instance per Web Page View. +- Documentation and tests for blueprint configuration, URL validation, unsupported targets, and native webview lifecycle behavior. diff --git a/openspec/changes/add-native-web-page-view/specs/native-web-page-view/spec.md b/openspec/changes/add-native-web-page-view/specs/native-web-page-view/spec.md new file mode 100644 index 000000000000..8d7a0770a010 --- /dev/null +++ b/openspec/changes/add-native-web-page-view/specs/native-web-page-view/spec.md @@ -0,0 +1,110 @@ +## ADDED Requirements + +### Requirement: Configured native Web Page View +The system SHALL provide a native-only Web Page View that displays a configured webpage inline in the viewer layout. + +#### Scenario: Native view displays configured page +- **WHEN** a native viewer layout contains a Web Page View configured with `https://example.com` +- **THEN** the viewer displays that page inline in the view area + +#### Scenario: Web viewer reports unsupported view +- **WHEN** the web viewer renders a layout containing a Web Page View +- **THEN** the viewer displays a clear unsupported status for that view + +### Requirement: Blueprint-owned configuration +The system SHALL store Web Page View configuration as blueprint/view state with a required URL and a `show_navigation_controls` setting. + +#### Scenario: Blueprint preconfigures a Web Page View +- **WHEN** blueprint state contains a Web Page View with a valid URL and navigation controls setting +- **THEN** the viewer creates the view with those configured values + +#### Scenario: Manual creation starts unconfigured +- **WHEN** a user manually adds a Web Page View without setting a URL +- **THEN** the viewer displays a Rerun-side status explaining that no URL is configured + +### Requirement: No data-driven spawning +The system SHALL make Web Page Views manually creatable and blueprint-preconfigurable without auto-spawning them from logged data. + +#### Scenario: Logged entities do not suggest Web Page View +- **WHEN** the viewer computes data-driven view suggestions from logged entities +- **THEN** the suggestion set excludes Web Page View unless it was explicitly created or configured + +### Requirement: HTTP URL policy +The system SHALL validate configured Web Page View URLs and load only `http://` and `https://` schemes. + +#### Scenario: HTTPS URL loads +- **WHEN** a Web Page View is configured with `https://example.com` +- **THEN** the viewer attempts to load the URL in the native webview + +#### Scenario: Local HTTP URL loads +- **WHEN** a Web Page View is configured with `http://localhost:3000` +- **THEN** the viewer attempts to load the URL in the native webview + +#### Scenario: Unsupported scheme is rejected +- **WHEN** a Web Page View is configured with `file:///tmp/report.html` +- **THEN** the viewer displays a Rerun-side status explaining that only `http` and `https` URLs are supported + +### Requirement: Automatic loading +The system SHALL automatically load a valid configured Web Page View URL without requiring an additional confirmation action. + +#### Scenario: Valid URL autoloads +- **WHEN** a Web Page View becomes visible with a valid configured URL +- **THEN** the native webview starts loading that URL automatically + +### Requirement: Per-view webview instances +The system SHALL create and manage one native webview instance for each Web Page View instance. + +#### Scenario: Multiple views render independently +- **WHEN** a layout contains two Web Page Views with different valid URLs +- **THEN** the viewer maintains two independent native webview instances and displays both pages in their respective view areas + +### Requirement: Webview lifecycle preserves hidden view state +The system SHALL keep a Web Page View's native webview instance alive while the Rerun view exists, including when temporarily hidden. + +#### Scenario: Hidden view keeps page state +- **WHEN** a Web Page View is hidden behind a tab and later shown again +- **THEN** the viewer reuses the existing webview instance rather than reloading the configured URL from scratch + +#### Scenario: Removed view destroys instance +- **WHEN** a Web Page View is removed from the layout +- **THEN** the viewer destroys the corresponding native webview instance + +### Requirement: Browser-like navigation +The system SHALL allow runtime browser navigation inside the Web Page View while keeping the configured URL unchanged. + +#### Scenario: Link navigation does not change blueprint URL +- **WHEN** a user clicks a link inside a Web Page View and the embedded page navigates to a different URL +- **THEN** the blueprint-configured URL remains the original configured URL + +#### Scenario: Home returns to configured URL +- **WHEN** navigation controls are visible and the user activates Home after navigating away +- **THEN** the webview navigates back to the configured URL + +### Requirement: Optional navigation controls +The system SHALL provide lightweight navigation controls for Web Page Views and allow them to be hidden by configuration. + +#### Scenario: Navigation controls visible by default +- **WHEN** a Web Page View is configured without specifying `show_navigation_controls` +- **THEN** the viewer displays back, forward, reload, home, and URL display controls + +#### Scenario: Navigation controls hidden +- **WHEN** a Web Page View is configured with `show_navigation_controls` set to false +- **THEN** the viewer hides the navigation controls and gives the webview the available view area + +### Requirement: Shared embedded browser session +The system SHALL use a shared embedded browser session/profile for Web Page Views by default. + +#### Scenario: Session state shared across views +- **WHEN** two Web Page Views load pages from the same origin +- **THEN** the embedded browser backend uses the shared default session/profile for both views + +### Requirement: Rerun-side failure reporting +The system SHALL report configuration and backend failures using Rerun-side status UI outside the embedded webpage. + +#### Scenario: Backend creation fails +- **WHEN** the native webview backend cannot create a webview instance +- **THEN** the Web Page View displays a clear Rerun-side failure message instead of crashing the viewer + +#### Scenario: Invalid URL is configured +- **WHEN** the configured URL cannot be parsed as a URL +- **THEN** the Web Page View displays a clear Rerun-side invalid URL message diff --git a/openspec/changes/add-native-web-page-view/tasks.md b/openspec/changes/add-native-web-page-view/tasks.md new file mode 100644 index 000000000000..3e8225359200 --- /dev/null +++ b/openspec/changes/add-native-web-page-view/tasks.md @@ -0,0 +1,61 @@ +## 1. TDD Tracer Bullet: Blueprint View Skeleton + +- [x] 1.1 RED: Add one behavior test that expects a manually created Web Page View with no URL to render a Rerun-side "No URL configured" status through the public view/test harness. +- [x] 1.2 GREEN: Add the minimal `WebPageView` blueprint definition, generated code, `re_view_web_page` crate/module skeleton, view registration, and empty-URL status UI needed to pass 1.1. +- [x] 1.3 REFACTOR: Align names with existing view conventions (`Web Page View` user-facing name, `web_page`/`WebPageView` code names as appropriate) and run `pixi run rs-fmt` plus the smallest relevant Rust check. + +## 2. TDD Slice: Blueprint Configuration and Manual Creation + +- [x] 2.1 RED: Add a behavior test that creates a Web Page View from blueprint state containing `url` and `show_navigation_controls`, and verifies those values are read by the view without logged data. +- [x] 2.2 GREEN: Implement blueprint properties for required URL and defaulted `show_navigation_controls = true`; ensure manual creation works and data-driven spawn heuristics do not suggest the view. +- [x] 2.3 REFACTOR: Keep configuration access isolated behind a small typed helper so later backend code does not parse blueprint state directly. + +## 3. TDD Slice: URL Policy and Status Errors + +- [x] 3.1 RED: Add behavior tests for accepted `https://example.com`, accepted `http://localhost:3000`, rejected `file:///tmp/report.html`, and invalid URL text. +- [x] 3.2 GREEN: Implement URL parsing/validation that allows only `http` and `https`, including localhost/private-network HTTP, and renders Rerun-side status messages for invalid or unsupported URLs. +- [x] 3.3 REFACTOR: Move URL policy into a backend-independent unit with focused tests so native webview code can trust validated URLs. + +## 4. TDD Slice: Native Webview Backend Seam + +- [x] 4.1 RED: Add tests using a fake backend that verify a valid configured URL causes exactly one backend webview instance to be created for one Web Page View. +- [x] 4.2 GREEN: Introduce a narrow native webview backend abstraction and wire the view to it with a fake/test backend; do not depend on `wry` in behavior tests. +- [x] 4.3 RED: Add a behavior test that two Web Page Views with different URLs create two independent backend instances. +- [x] 4.4 GREEN: Implement per-view instance ownership keyed by stable view identity. +- [x] 4.5 REFACTOR: Keep egui/view logic, lifecycle manager, and platform backend responsibilities separate. + +## 5. TDD Slice: Direct wry Integration + +- [x] 5.1 RED: Add a compile-gated native integration test or smoke test target that exercises construction through the real native backend boundary and reports backend creation failure instead of panicking. +- [x] 5.2 GREEN: Add direct `wry` integration for native builds, including required Cargo feature/dependency wiring and platform-specific creation paths supported by wry. +- [x] 5.3 REFACTOR: Encapsulate platform-specific wry details behind the backend boundary, including Linux WebKitGTK/X11/Wayland caveats and bounds-setting differences. + +## 6. TDD Slice: Layout Bounds and Lifecycle + +- [x] 6.1 RED: Add fake-backend behavior tests that verify the backend receives updated bounds when the egui view rectangle changes. +- [x] 6.2 GREEN: Update native webview bounds from the allocated egui view rectangle each frame, accounting for DPI/points-to-pixels conversion. +- [x] 6.3 RED: Add fake-backend behavior tests that verify hiding a view keeps its instance alive and removing a view destroys its instance. +- [x] 6.4 GREEN: Implement keep-alive-while-hidden lifecycle and destroy-on-remove/app-exit cleanup. +- [x] 6.5 REFACTOR: Audit focus, clipping, tab, split-panel, and resize behavior with the fake backend before manual native smoke testing. + +## 7. TDD Slice: Navigation Controls and Runtime Navigation + +- [x] 7.1 RED: Add behavior tests that navigation controls are visible by default and hidden when `show_navigation_controls` is false. +- [x] 7.2 GREEN: Implement lightweight back, forward, reload, home, and URL display controls above the embedded webview, reserving the full view area when controls are hidden. +- [x] 7.3 RED: Add a behavior test that runtime navigation does not mutate the blueprint-configured URL and that Home navigates back to the configured URL. +- [x] 7.4 GREEN: Wire navigation commands through the backend abstraction while keeping configured URL state immutable during runtime navigation. + +## 8. TDD Slice: Platform Support, Session Defaults, and Failure UI + +- [x] 8.1 RED: Add behavior tests that web builds or unavailable native backends render explicit unsupported/failure status UI. +- [x] 8.2 GREEN: Implement unsupported-target and backend-failure reporting outside the webview surface. +- [x] 8.3 RED: Add a fake-backend behavior test that multiple views use the shared default browser profile/session configuration. +- [x] 8.4 GREEN: Implement shared default session/profile behavior in the backend boundary while leaving per-view isolation as a future extension point. + +## 9. Verification and Documentation + +- [x] 9.1 Run `pixi run codegen` after blueprint definition changes and verify generated Rust/Python/C++ outputs are updated as expected. +- [x] 9.2 Run `pixi run rs-fmt` after Rust changes. +- [x] 9.3 Run targeted Rust checks/tests for the new view crate and affected viewer crates with `cargo clippy -p ` and `cargo nextest run --all-features --no-fail-fast -p ` (`cargo nextest` was unavailable in this environment; used focused `cargo test` plus clippy coverage instead). +- [x] 9.4 Manually smoke-test native Web Page View creation, URL editing, split/tab resizing, navigation controls, multiple simultaneous views, hidden-tab preservation, and backend failure messaging on at least one supported native platform. +- [x] 9.5 Document native platform/runtime requirements, including WebView2/WebKitGTK expectations and the fact that the Web Page View is unsupported in the web viewer. diff --git a/openspec/config.yaml b/openspec/config.yaml new file mode 100644 index 000000000000..392946c67c03 --- /dev/null +++ b/openspec/config.yaml @@ -0,0 +1,20 @@ +schema: spec-driven + +# Project context (optional) +# This is shown to AI when creating artifacts. +# Add your tech stack, conventions, style guides, domain knowledge, etc. +# Example: +# context: | +# Tech stack: TypeScript, React, Node.js +# We use conventional commits +# Domain: e-commerce platform + +# Per-artifact rules (optional) +# Add custom rules for specific artifacts. +# Example: +# rules: +# proposal: +# - Keep proposals under 500 words +# - Always include a "Non-goals" section +# tasks: +# - Break tasks into chunks of max 2 hours diff --git a/rerun_cpp/src/rerun/blueprint/archetypes.hpp b/rerun_cpp/src/rerun/blueprint/archetypes.hpp index d3cb29c14b38..8d375da14e18 100644 --- a/rerun_cpp/src/rerun/blueprint/archetypes.hpp +++ b/rerun_cpp/src/rerun/blueprint/archetypes.hpp @@ -37,3 +37,4 @@ #include "blueprint/archetypes/visible_time_ranges.hpp" #include "blueprint/archetypes/visual_bounds2d.hpp" #include "blueprint/archetypes/visualizer_instruction.hpp" +#include "blueprint/archetypes/web_page_view_config.hpp" diff --git a/rerun_cpp/src/rerun/blueprint/archetypes/.gitattributes b/rerun_cpp/src/rerun/blueprint/archetypes/.gitattributes index c324eb6a8ac9..a7a0deed6803 100644 --- a/rerun_cpp/src/rerun/blueprint/archetypes/.gitattributes +++ b/rerun_cpp/src/rerun/blueprint/archetypes/.gitattributes @@ -71,3 +71,5 @@ visual_bounds2d.cpp linguist-generated=true visual_bounds2d.hpp linguist-generated=true visualizer_instruction.cpp linguist-generated=true visualizer_instruction.hpp linguist-generated=true +web_page_view_config.cpp linguist-generated=true +web_page_view_config.hpp linguist-generated=true diff --git a/rerun_cpp/src/rerun/blueprint/archetypes/web_page_view_config.cpp b/rerun_cpp/src/rerun/blueprint/archetypes/web_page_view_config.cpp new file mode 100644 index 000000000000..6843bf8f75c8 --- /dev/null +++ b/rerun_cpp/src/rerun/blueprint/archetypes/web_page_view_config.cpp @@ -0,0 +1,65 @@ +// DO NOT EDIT! This file was auto-generated by crates/build/re_types_builder/src/codegen/cpp/mod.rs +// Based on "crates/store/re_sdk_types/definitions/rerun/blueprint/archetypes/web_page_view_config.fbs". + +#include "web_page_view_config.hpp" + +#include "../../collection_adapter_builtins.hpp" + +namespace rerun::blueprint::archetypes { + WebPageViewConfig WebPageViewConfig::clear_fields() { + auto archetype = WebPageViewConfig(); + archetype.url = + ComponentBatch::empty(Descriptor_url) + .value_or_throw(); + archetype.show_navigation_controls = + ComponentBatch::empty( + Descriptor_show_navigation_controls + ) + .value_or_throw(); + return archetype; + } + + Collection WebPageViewConfig::columns(const Collection& lengths_) { + std::vector columns; + columns.reserve(2); + if (url.has_value()) { + columns.push_back(url.value().partitioned(lengths_).value_or_throw()); + } + if (show_navigation_controls.has_value()) { + columns.push_back(show_navigation_controls.value().partitioned(lengths_).value_or_throw( + )); + } + return columns; + } + + Collection WebPageViewConfig::columns() { + if (url.has_value()) { + return columns(std::vector(url.value().length(), 1)); + } + if (show_navigation_controls.has_value()) { + return columns(std::vector(show_navigation_controls.value().length(), 1)); + } + return Collection(); + } +} // namespace rerun::blueprint::archetypes + +namespace rerun { + + Result> + AsComponents::as_batches( + const blueprint::archetypes::WebPageViewConfig& archetype + ) { + using namespace blueprint::archetypes; + std::vector cells; + cells.reserve(2); + + if (archetype.url.has_value()) { + cells.push_back(archetype.url.value()); + } + if (archetype.show_navigation_controls.has_value()) { + cells.push_back(archetype.show_navigation_controls.value()); + } + + return rerun::take_ownership(std::move(cells)); + } +} // namespace rerun diff --git a/rerun_cpp/src/rerun/blueprint/archetypes/web_page_view_config.hpp b/rerun_cpp/src/rerun/blueprint/archetypes/web_page_view_config.hpp new file mode 100644 index 000000000000..e186a0e5cc4b --- /dev/null +++ b/rerun_cpp/src/rerun/blueprint/archetypes/web_page_view_config.hpp @@ -0,0 +1,119 @@ +// DO NOT EDIT! This file was auto-generated by crates/build/re_types_builder/src/codegen/cpp/mod.rs +// Based on "crates/store/re_sdk_types/definitions/rerun/blueprint/archetypes/web_page_view_config.fbs". + +#pragma once + +#include "../../blueprint/components/show_navigation_controls.hpp" +#include "../../blueprint/components/web_page_url.hpp" +#include "../../collection.hpp" +#include "../../component_batch.hpp" +#include "../../component_column.hpp" +#include "../../result.hpp" + +#include +#include +#include +#include + +namespace rerun::blueprint::archetypes { + /// **Archetype**: Configuration for the Web Page View. + /// + /// ⚠ **This type is _unstable_ and may change significantly in a way that the data won't be backwards compatible.** + /// + struct WebPageViewConfig { + /// The initial URL to load. + std::optional url; + + /// Whether browser navigation controls should be shown. + /// + /// Defaults to true. + std::optional show_navigation_controls; + + public: + /// The name of the archetype as used in `ComponentDescriptor`s. + static constexpr const char ArchetypeName[] = + "rerun.blueprint.archetypes.WebPageViewConfig"; + + /// `ComponentDescriptor` for the `url` field. + static constexpr auto Descriptor_url = ComponentDescriptor( + ArchetypeName, "WebPageViewConfig:url", + Loggable::ComponentType + ); + /// `ComponentDescriptor` for the `show_navigation_controls` field. + static constexpr auto Descriptor_show_navigation_controls = ComponentDescriptor( + ArchetypeName, "WebPageViewConfig:show_navigation_controls", + Loggable::ComponentType + ); + + public: + WebPageViewConfig() = default; + WebPageViewConfig(WebPageViewConfig&& other) = default; + WebPageViewConfig(const WebPageViewConfig& other) = default; + WebPageViewConfig& operator=(const WebPageViewConfig& other) = default; + WebPageViewConfig& operator=(WebPageViewConfig&& other) = default; + + explicit WebPageViewConfig(rerun::blueprint::components::WebPageUrl _url) + : url(ComponentBatch::from_loggable(std::move(_url), Descriptor_url).value_or_throw()) { + } + + /// Update only some specific fields of a `WebPageViewConfig`. + static WebPageViewConfig update_fields() { + return WebPageViewConfig(); + } + + /// Clear all the fields of a `WebPageViewConfig`. + static WebPageViewConfig clear_fields(); + + /// The initial URL to load. + WebPageViewConfig with_url(const rerun::blueprint::components::WebPageUrl& _url) && { + url = ComponentBatch::from_loggable(_url, Descriptor_url).value_or_throw(); + return std::move(*this); + } + + /// Whether browser navigation controls should be shown. + /// + /// Defaults to true. + WebPageViewConfig with_show_navigation_controls( + const rerun::blueprint::components::ShowNavigationControls& _show_navigation_controls + ) && { + show_navigation_controls = ComponentBatch::from_loggable( + _show_navigation_controls, + Descriptor_show_navigation_controls + ) + .value_or_throw(); + return std::move(*this); + } + + /// Partitions the component data into multiple sub-batches. + /// + /// Specifically, this transforms the existing `ComponentBatch` data into `ComponentColumn`s + /// instead, via `ComponentBatch::partitioned`. + /// + /// This makes it possible to use `RecordingStream::send_columns` to send columnar data directly into Rerun. + /// + /// The specified `lengths` must sum to the total length of the component batch. + Collection columns(const Collection& lengths_); + + /// Partitions the component data into unit-length sub-batches. + /// + /// This is semantically similar to calling `columns` with `std::vector(n, 1)`, + /// where `n` is automatically guessed. + Collection columns(); + }; + +} // namespace rerun::blueprint::archetypes + +namespace rerun { + /// \private + template + struct AsComponents; + + /// \private + template <> + struct AsComponents { + /// Serialize all set component batches. + static Result> as_batches( + const blueprint::archetypes::WebPageViewConfig& archetype + ); + }; +} // namespace rerun diff --git a/rerun_cpp/src/rerun/blueprint/components.hpp b/rerun_cpp/src/rerun/blueprint/components.hpp index a2c377e4c033..dd673341fb6d 100644 --- a/rerun_cpp/src/rerun/blueprint/components.hpp +++ b/rerun_cpp/src/rerun/blueprint/components.hpp @@ -38,6 +38,7 @@ #include "blueprint/components/root_container.hpp" #include "blueprint/components/row_share.hpp" #include "blueprint/components/selected_columns.hpp" +#include "blueprint/components/show_navigation_controls.hpp" #include "blueprint/components/tensor_dimension_index_slider.hpp" #include "blueprint/components/text_log_column.hpp" #include "blueprint/components/time_int.hpp" @@ -54,4 +55,5 @@ #include "blueprint/components/visualizer_component_mapping.hpp" #include "blueprint/components/visualizer_instruction_id.hpp" #include "blueprint/components/visualizer_type.hpp" +#include "blueprint/components/web_page_url.hpp" #include "blueprint/components/zoom_level.hpp" diff --git a/rerun_cpp/src/rerun/blueprint/components/.gitattributes b/rerun_cpp/src/rerun/blueprint/components/.gitattributes index d8df7ee127ad..bc05d488cf36 100644 --- a/rerun_cpp/src/rerun/blueprint/components/.gitattributes +++ b/rerun_cpp/src/rerun/blueprint/components/.gitattributes @@ -47,6 +47,7 @@ query_expression.hpp linguist-generated=true root_container.hpp linguist-generated=true row_share.hpp linguist-generated=true selected_columns.hpp linguist-generated=true +show_navigation_controls.hpp linguist-generated=true tensor_dimension_index_slider.hpp linguist-generated=true text_log_column.hpp linguist-generated=true time_int.hpp linguist-generated=true @@ -64,4 +65,5 @@ visual_bounds2d.hpp linguist-generated=true visualizer_component_mapping.hpp linguist-generated=true visualizer_instruction_id.hpp linguist-generated=true visualizer_type.hpp linguist-generated=true +web_page_url.hpp linguist-generated=true zoom_level.hpp linguist-generated=true diff --git a/rerun_cpp/src/rerun/blueprint/components/show_navigation_controls.hpp b/rerun_cpp/src/rerun/blueprint/components/show_navigation_controls.hpp new file mode 100644 index 000000000000..3744e19bea1f --- /dev/null +++ b/rerun_cpp/src/rerun/blueprint/components/show_navigation_controls.hpp @@ -0,0 +1,80 @@ +// DO NOT EDIT! This file was auto-generated by crates/build/re_types_builder/src/codegen/cpp/mod.rs +// Based on "crates/store/re_sdk_types/definitions/rerun/blueprint/components/show_navigation_controls.fbs". + +#pragma once + +#include "../../datatypes/bool.hpp" +#include "../../result.hpp" + +#include +#include + +namespace rerun::blueprint::components { + /// **Component**: Whether browser navigation controls should be shown in the Web Page View. + /// + /// ⚠ **This type is _unstable_ and may change significantly in a way that the data won't be backwards compatible.** + /// + struct ShowNavigationControls { + rerun::datatypes::Bool show_navigation_controls; + + public: + ShowNavigationControls() = default; + + ShowNavigationControls(rerun::datatypes::Bool show_navigation_controls_) + : show_navigation_controls(show_navigation_controls_) {} + + ShowNavigationControls& operator=(rerun::datatypes::Bool show_navigation_controls_) { + show_navigation_controls = show_navigation_controls_; + return *this; + } + + ShowNavigationControls(bool value_) : show_navigation_controls(value_) {} + + ShowNavigationControls& operator=(bool value_) { + show_navigation_controls = value_; + return *this; + } + + /// Cast to the underlying Bool datatype + operator rerun::datatypes::Bool() const { + return show_navigation_controls; + } + }; +} // namespace rerun::blueprint::components + +namespace rerun { + static_assert( + sizeof(rerun::datatypes::Bool) == sizeof(blueprint::components::ShowNavigationControls) + ); + + /// \private + template <> + struct Loggable { + static constexpr std::string_view ComponentType = + "rerun.blueprint.components.ShowNavigationControls"; + + /// Returns the arrow data type this type corresponds to. + static const std::shared_ptr& arrow_datatype() { + return Loggable::arrow_datatype(); + } + + /// Serializes an array of `rerun::blueprint:: components::ShowNavigationControls` into an arrow array. + static Result> to_arrow( + const blueprint::components::ShowNavigationControls* instances, size_t num_instances + ) { + if (num_instances == 0) { + return Loggable::to_arrow(nullptr, 0); + } else if (instances == nullptr) { + return rerun::Error( + ErrorCode::UnexpectedNullArgument, + "Passed array instances is null when num_elements> 0." + ); + } else { + return Loggable::to_arrow( + &instances->show_navigation_controls, + num_instances + ); + } + } + }; +} // namespace rerun diff --git a/rerun_cpp/src/rerun/blueprint/components/web_page_url.hpp b/rerun_cpp/src/rerun/blueprint/components/web_page_url.hpp new file mode 100644 index 000000000000..dad05982772e --- /dev/null +++ b/rerun_cpp/src/rerun/blueprint/components/web_page_url.hpp @@ -0,0 +1,75 @@ +// DO NOT EDIT! This file was auto-generated by crates/build/re_types_builder/src/codegen/cpp/mod.rs +// Based on "crates/store/re_sdk_types/definitions/rerun/blueprint/components/web_page_url.fbs". + +#pragma once + +#include "../../datatypes/utf8.hpp" +#include "../../result.hpp" + +#include +#include +#include +#include + +namespace rerun::blueprint::components { + /// **Component**: The initial URL to load in a Web Page View. + /// + /// ⚠ **This type is _unstable_ and may change significantly in a way that the data won't be backwards compatible.** + /// + struct WebPageUrl { + rerun::datatypes::Utf8 url; + + public: + WebPageUrl() = default; + + WebPageUrl(rerun::datatypes::Utf8 url_) : url(std::move(url_)) {} + + WebPageUrl& operator=(rerun::datatypes::Utf8 url_) { + url = std::move(url_); + return *this; + } + + WebPageUrl(std::string value_) : url(std::move(value_)) {} + + WebPageUrl& operator=(std::string value_) { + url = std::move(value_); + return *this; + } + + /// Cast to the underlying Utf8 datatype + operator rerun::datatypes::Utf8() const { + return url; + } + }; +} // namespace rerun::blueprint::components + +namespace rerun { + static_assert(sizeof(rerun::datatypes::Utf8) == sizeof(blueprint::components::WebPageUrl)); + + /// \private + template <> + struct Loggable { + static constexpr std::string_view ComponentType = "rerun.blueprint.components.WebPageUrl"; + + /// Returns the arrow data type this type corresponds to. + static const std::shared_ptr& arrow_datatype() { + return Loggable::arrow_datatype(); + } + + /// Serializes an array of `rerun::blueprint:: components::WebPageUrl` into an arrow array. + static Result> to_arrow( + const blueprint::components::WebPageUrl* instances, size_t num_instances + ) { + if (num_instances == 0) { + return Loggable::to_arrow(nullptr, 0); + } else if (instances == nullptr) { + return rerun::Error( + ErrorCode::UnexpectedNullArgument, + "Passed array instances is null when num_elements> 0." + ); + } else { + return Loggable::to_arrow(&instances->url, num_instances); + } + } + }; +} // namespace rerun diff --git a/rerun_py/rerun_sdk/rerun/blueprint/__init__.py b/rerun_py/rerun_sdk/rerun/blueprint/__init__.py index 71ea8123c1aa..9b633360af1b 100644 --- a/rerun_py/rerun_sdk/rerun/blueprint/__init__.py +++ b/rerun_py/rerun_sdk/rerun/blueprint/__init__.py @@ -44,6 +44,7 @@ TimeAxis as TimeAxis, VisibleTimeRanges as VisibleTimeRanges, VisualBounds2D as VisualBounds2D, + WebPageViewConfig as WebPageViewConfig, ) from .components import ( BackgroundKind as BackgroundKind, @@ -70,5 +71,6 @@ TextDocumentView as TextDocumentView, TextLogView as TextLogView, TimeSeriesView as TimeSeriesView, + WebPageView as WebPageView, ) from .visualizers import VisualizableArchetype as VisualizableArchetype, Visualizer as Visualizer diff --git a/rerun_py/rerun_sdk/rerun/blueprint/archetypes/.gitattributes b/rerun_py/rerun_sdk/rerun/blueprint/archetypes/.gitattributes index 73782fdeccc1..1ceac83a030b 100644 --- a/rerun_py/rerun_sdk/rerun/blueprint/archetypes/.gitattributes +++ b/rerun_py/rerun_sdk/rerun/blueprint/archetypes/.gitattributes @@ -37,3 +37,4 @@ viewport_blueprint.py linguist-generated=true visible_time_ranges.py linguist-generated=true visual_bounds2d.py linguist-generated=true visualizer_instruction.py linguist-generated=true +web_page_view_config.py linguist-generated=true diff --git a/rerun_py/rerun_sdk/rerun/blueprint/archetypes/__init__.py b/rerun_py/rerun_sdk/rerun/blueprint/archetypes/__init__.py index 7e961d129f7f..a6357d0f4f14 100644 --- a/rerun_py/rerun_sdk/rerun/blueprint/archetypes/__init__.py +++ b/rerun_py/rerun_sdk/rerun/blueprint/archetypes/__init__.py @@ -37,6 +37,7 @@ from .visible_time_ranges import VisibleTimeRanges from .visual_bounds2d import VisualBounds2D from .visualizer_instruction import VisualizerInstruction +from .web_page_view_config import WebPageViewConfig __all__ = [ "ActiveVisualizers", @@ -74,4 +75,5 @@ "VisibleTimeRanges", "VisualBounds2D", "VisualizerInstruction", + "WebPageViewConfig", ] diff --git a/rerun_py/rerun_sdk/rerun/blueprint/archetypes/web_page_view_config.py b/rerun_py/rerun_sdk/rerun/blueprint/archetypes/web_page_view_config.py new file mode 100644 index 000000000000..771405a193a2 --- /dev/null +++ b/rerun_py/rerun_sdk/rerun/blueprint/archetypes/web_page_view_config.py @@ -0,0 +1,154 @@ +# DO NOT EDIT! This file was auto-generated by crates/build/re_types_builder/src/codegen/python/mod.rs +# Based on "crates/store/re_sdk_types/definitions/rerun/blueprint/archetypes/web_page_view_config.fbs". + +# You can extend this class by creating a "WebPageViewConfigExt" class in "web_page_view_config_ext.py". + +from __future__ import annotations + +from typing import TYPE_CHECKING, Any, ClassVar + +from attrs import define, field + +from ..._baseclasses import ( + Archetype, + ComponentDescriptor, +) +from ...blueprint import components as blueprint_components +from ...error_utils import catch_and_log_exceptions + +if TYPE_CHECKING: + from ... import datatypes + +__all__ = ["WebPageViewConfig"] + + +@define(str=False, repr=False, init=False) +class WebPageViewConfig(Archetype): + """ + **Archetype**: Configuration for the Web Page View. + + ⚠️ **This type is _unstable_ and may change significantly in a way that the data won't be backwards compatible.** + """ + + NAME: ClassVar[str] = "rerun.blueprint.archetypes.WebPageViewConfig" + + def __init__( + self: Any, url: datatypes.Utf8Like, *, show_navigation_controls: datatypes.BoolLike | None = None + ) -> None: + """ + Create a new instance of the WebPageViewConfig archetype. + + Parameters + ---------- + url: + The initial URL to load. + show_navigation_controls: + Whether browser navigation controls should be shown. + + Defaults to true. + + """ + + # You can define your own __init__ function as a member of WebPageViewConfigExt in web_page_view_config_ext.py + with catch_and_log_exceptions(context=self.__class__.__name__): + self.__attrs_init__(url=url, show_navigation_controls=show_navigation_controls) + return + self.__attrs_clear__() + + def __attrs_clear__(self) -> None: + """Convenience method for calling `__attrs_init__` with all `None`s.""" + self.__attrs_init__( + url=None, + show_navigation_controls=None, + ) + + @classmethod + def _clear(cls) -> WebPageViewConfig: + """Produce an empty WebPageViewConfig, bypassing `__init__`.""" + inst = cls.__new__(cls) + inst.__attrs_clear__() + return inst + + @classmethod + def from_fields( + cls, + *, + clear_unset: bool = False, + url: datatypes.Utf8Like | None = None, + show_navigation_controls: datatypes.BoolLike | None = None, + ) -> WebPageViewConfig: + """ + Update only some specific fields of a `WebPageViewConfig`. + + Parameters + ---------- + clear_unset: + If true, all unspecified fields will be explicitly cleared. + url: + The initial URL to load. + show_navigation_controls: + Whether browser navigation controls should be shown. + + Defaults to true. + + """ + + inst = cls.__new__(cls) + with catch_and_log_exceptions(context=cls.__name__): + kwargs = { + "url": url, + "show_navigation_controls": show_navigation_controls, + } + + if clear_unset: + kwargs = {k: v if v is not None else [] for k, v in kwargs.items()} # type: ignore[misc] + + inst.__attrs_init__(**kwargs) + return inst + + inst.__attrs_clear__() + return inst + + @classmethod + def cleared(cls) -> WebPageViewConfig: + """Clear all the fields of a `WebPageViewConfig`.""" + return cls.from_fields(clear_unset=True) + + @staticmethod + def descriptor_url() -> ComponentDescriptor: + return ComponentDescriptor( + "WebPageViewConfig:url", + archetype=WebPageViewConfig.NAME, + component_type=blueprint_components.WebPageUrlBatch._COMPONENT_TYPE, + ) + + @staticmethod + def descriptor_show_navigation_controls() -> ComponentDescriptor: + return ComponentDescriptor( + "WebPageViewConfig:show_navigation_controls", + archetype=WebPageViewConfig.NAME, + component_type=blueprint_components.ShowNavigationControlsBatch._COMPONENT_TYPE, + ) + + url: blueprint_components.WebPageUrlBatch | None = field( + metadata={"component": True}, + default=None, + converter=blueprint_components.WebPageUrlBatch._converter, # type: ignore[misc] + ) + # The initial URL to load. + # + # (Docstring intentionally commented out to hide this field from the docs) + + show_navigation_controls: blueprint_components.ShowNavigationControlsBatch | None = field( + metadata={"component": True}, + default=None, + converter=blueprint_components.ShowNavigationControlsBatch._converter, # type: ignore[misc] + ) + # Whether browser navigation controls should be shown. + # + # Defaults to true. + # + # (Docstring intentionally commented out to hide this field from the docs) + + __str__ = Archetype.__str__ + __repr__ = Archetype.__repr__ # type: ignore[assignment] diff --git a/rerun_py/rerun_sdk/rerun/blueprint/components/.gitattributes b/rerun_py/rerun_sdk/rerun/blueprint/components/.gitattributes index 80919b9b1ae0..659d430f12c5 100644 --- a/rerun_py/rerun_sdk/rerun/blueprint/components/.gitattributes +++ b/rerun_py/rerun_sdk/rerun/blueprint/components/.gitattributes @@ -38,6 +38,7 @@ query_expression.py linguist-generated=true root_container.py linguist-generated=true row_share.py linguist-generated=true selected_columns.py linguist-generated=true +show_navigation_controls.py linguist-generated=true tensor_dimension_index_slider.py linguist-generated=true text_log_column.py linguist-generated=true time_int.py linguist-generated=true @@ -54,4 +55,5 @@ visual_bounds2d.py linguist-generated=true visualizer_component_mapping.py linguist-generated=true visualizer_instruction_id.py linguist-generated=true visualizer_type.py linguist-generated=true +web_page_url.py linguist-generated=true zoom_level.py linguist-generated=true diff --git a/rerun_py/rerun_sdk/rerun/blueprint/components/__init__.py b/rerun_py/rerun_sdk/rerun/blueprint/components/__init__.py index 4a999fcefc59..8ebc6f5b7cab 100644 --- a/rerun_py/rerun_sdk/rerun/blueprint/components/__init__.py +++ b/rerun_py/rerun_sdk/rerun/blueprint/components/__init__.py @@ -38,6 +38,7 @@ from .root_container import RootContainer, RootContainerBatch from .row_share import RowShare, RowShareBatch from .selected_columns import SelectedColumns, SelectedColumnsBatch +from .show_navigation_controls import ShowNavigationControls, ShowNavigationControlsBatch from .tensor_dimension_index_slider import TensorDimensionIndexSlider, TensorDimensionIndexSliderBatch from .text_log_column import TextLogColumn, TextLogColumnBatch from .time_int import TimeInt, TimeIntBatch @@ -54,6 +55,7 @@ from .visualizer_component_mapping import VisualizerComponentMapping, VisualizerComponentMappingBatch from .visualizer_instruction_id import VisualizerInstructionId, VisualizerInstructionIdBatch from .visualizer_type import VisualizerType, VisualizerTypeBatch +from .web_page_url import WebPageUrl, WebPageUrlBatch from .zoom_level import ZoomLevel, ZoomLevelBatch __all__ = [ @@ -149,6 +151,8 @@ "RowShareBatch", "SelectedColumns", "SelectedColumnsBatch", + "ShowNavigationControls", + "ShowNavigationControlsBatch", "TensorDimensionIndexSlider", "TensorDimensionIndexSliderBatch", "TextLogColumn", @@ -183,6 +187,8 @@ "VisualizerInstructionIdBatch", "VisualizerType", "VisualizerTypeBatch", + "WebPageUrl", + "WebPageUrlBatch", "ZoomLevel", "ZoomLevelBatch", ] diff --git a/rerun_py/rerun_sdk/rerun/blueprint/components/show_navigation_controls.py b/rerun_py/rerun_sdk/rerun/blueprint/components/show_navigation_controls.py new file mode 100644 index 000000000000..63a362e220cc --- /dev/null +++ b/rerun_py/rerun_sdk/rerun/blueprint/components/show_navigation_controls.py @@ -0,0 +1,35 @@ +# DO NOT EDIT! This file was auto-generated by crates/build/re_types_builder/src/codegen/python/mod.rs +# Based on "crates/store/re_sdk_types/definitions/rerun/blueprint/components/show_navigation_controls.fbs". + +# You can extend this class by creating a "ShowNavigationControlsExt" class in "show_navigation_controls_ext.py". + +from __future__ import annotations + +from ... import datatypes +from ..._baseclasses import ( + ComponentBatchMixin, + ComponentMixin, +) + +__all__ = ["ShowNavigationControls", "ShowNavigationControlsBatch"] + + +class ShowNavigationControls(datatypes.Bool, ComponentMixin): + """ + **Component**: Whether browser navigation controls should be shown in the Web Page View. + + ⚠️ **This type is _unstable_ and may change significantly in a way that the data won't be backwards compatible.** + """ + + _BATCH_TYPE = None + # You can define your own __init__ function as a member of ShowNavigationControlsExt in show_navigation_controls_ext.py + + # Note: there are no fields here because ShowNavigationControls delegates to datatypes.Bool + + +class ShowNavigationControlsBatch(datatypes.BoolBatch, ComponentBatchMixin): + _COMPONENT_TYPE: str = "rerun.blueprint.components.ShowNavigationControls" + + +# This is patched in late to avoid circular dependencies. +ShowNavigationControls._BATCH_TYPE = ShowNavigationControlsBatch # type: ignore[assignment] diff --git a/rerun_py/rerun_sdk/rerun/blueprint/components/web_page_url.py b/rerun_py/rerun_sdk/rerun/blueprint/components/web_page_url.py new file mode 100644 index 000000000000..ead0b0a8b534 --- /dev/null +++ b/rerun_py/rerun_sdk/rerun/blueprint/components/web_page_url.py @@ -0,0 +1,35 @@ +# DO NOT EDIT! This file was auto-generated by crates/build/re_types_builder/src/codegen/python/mod.rs +# Based on "crates/store/re_sdk_types/definitions/rerun/blueprint/components/web_page_url.fbs". + +# You can extend this class by creating a "WebPageUrlExt" class in "web_page_url_ext.py". + +from __future__ import annotations + +from ... import datatypes +from ..._baseclasses import ( + ComponentBatchMixin, + ComponentMixin, +) + +__all__ = ["WebPageUrl", "WebPageUrlBatch"] + + +class WebPageUrl(datatypes.Utf8, ComponentMixin): + """ + **Component**: The initial URL to load in a Web Page View. + + ⚠️ **This type is _unstable_ and may change significantly in a way that the data won't be backwards compatible.** + """ + + _BATCH_TYPE = None + # You can define your own __init__ function as a member of WebPageUrlExt in web_page_url_ext.py + + # Note: there are no fields here because WebPageUrl delegates to datatypes.Utf8 + + +class WebPageUrlBatch(datatypes.Utf8Batch, ComponentBatchMixin): + _COMPONENT_TYPE: str = "rerun.blueprint.components.WebPageUrl" + + +# This is patched in late to avoid circular dependencies. +WebPageUrl._BATCH_TYPE = WebPageUrlBatch # type: ignore[assignment] diff --git a/rerun_py/rerun_sdk/rerun/blueprint/views/.gitattributes b/rerun_py/rerun_sdk/rerun/blueprint/views/.gitattributes index 1cf2a57418bd..a95245ffcf50 100644 --- a/rerun_py/rerun_sdk/rerun/blueprint/views/.gitattributes +++ b/rerun_py/rerun_sdk/rerun/blueprint/views/.gitattributes @@ -13,3 +13,4 @@ tensor_view.py linguist-generated=true text_document_view.py linguist-generated=true text_log_view.py linguist-generated=true time_series_view.py linguist-generated=true +web_page_view.py linguist-generated=true diff --git a/rerun_py/rerun_sdk/rerun/blueprint/views/__init__.py b/rerun_py/rerun_sdk/rerun/blueprint/views/__init__.py index d800db82767f..c476baef2178 100644 --- a/rerun_py/rerun_sdk/rerun/blueprint/views/__init__.py +++ b/rerun_py/rerun_sdk/rerun/blueprint/views/__init__.py @@ -13,6 +13,7 @@ from .text_document_view import TextDocumentView from .text_log_view import TextLogView from .time_series_view import TimeSeriesView +from .web_page_view import WebPageView __all__ = [ "BarChartView", @@ -26,4 +27,5 @@ "TextDocumentView", "TextLogView", "TimeSeriesView", + "WebPageView", ] diff --git a/rerun_py/rerun_sdk/rerun/blueprint/views/web_page_view.py b/rerun_py/rerun_sdk/rerun/blueprint/views/web_page_view.py new file mode 100644 index 000000000000..ee5e4197be02 --- /dev/null +++ b/rerun_py/rerun_sdk/rerun/blueprint/views/web_page_view.py @@ -0,0 +1,106 @@ +# DO NOT EDIT! This file was auto-generated by crates/build/re_types_builder/src/codegen/python/mod.rs +# Based on "crates/store/re_sdk_types/definitions/rerun/blueprint/views/web_page.fbs". + +from __future__ import annotations + +from typing import TYPE_CHECKING + +__all__ = ["WebPageView"] + + +from .. import archetypes as blueprint_archetypes +from ..api import View, ViewContentsLike, VisualizerLike + +if TYPE_CHECKING: + from collections.abc import Iterable, Mapping + + from ... import datatypes + from ..._baseclasses import ( + AsComponents, + DescribedComponentBatch, + ) + from ...datatypes import EntityPathLike, Utf8Like + + +class WebPageView(View): + """ + **View**: A native-only view that embeds a configured web page. + + Web Page View is unsupported in the web viewer. Native support depends on the + operating-system webview runtime provided through `wry`: `WebView2` on Windows, + `WebKit` on macOS, and `WebKitGTK` on Linux. Linux child webviews may also depend + on the active display-server integration; the direct child-window path is + X11-oriented, while Wayland-capable embedding requires a GTK container path. + + ⚠️ **This type is _unstable_ and may change significantly in a way that the data won't be backwards compatible.** + """ + + def __init__( + self, + *, + origin: EntityPathLike = "/", + contents: ViewContentsLike = "$origin/**", + name: Utf8Like | None = None, + visible: datatypes.BoolLike | None = None, + defaults: Iterable[AsComponents | Iterable[DescribedComponentBatch]] | None = None, + overrides: Mapping[EntityPathLike, VisualizerLike | Iterable[VisualizerLike]] | None = None, + config: blueprint_archetypes.WebPageViewConfig | None = None, + ) -> None: + """ + Construct a blueprint for a new WebPageView view. + + Parameters + ---------- + origin: + The `EntityPath` to use as the origin of this view. + All other entities will be transformed to be displayed relative to this origin. + contents: + The contents of the view specified as a query expression. + This is either a single expression, or a list of multiple expressions. + See [rerun.blueprint.archetypes.ViewContents][]. + name: + The display name of the view. + visible: + Whether this view is visible. + + Defaults to true if not specified. + defaults: + List of archetypes or (described) component batches to add to the view. + When an archetype in the view is missing a component included in this set, + the value of default will be used instead of the normal fallback for the visualizer. + + Note that an archetype's required components typically don't have any effect. + It is recommended to use the archetype's `from_fields` method instead and only specify the fields that you need. + overrides: + Dictionary of visualizer overrides to apply to the view. The key is the path to the entity where the override + should be applied. The value is a list of visualizers which should be enabled for that entity, or a single visualizer. + + Each visualizer can be configured with arbitrary overrides and mappings. + + For any entity mentioned in this map, visualizers are no longer added automatically based on the entity's components. + + Important note: the path must be a fully qualified entity path starting at the root. The override paths + do not yet support `$origin` relative paths or glob expressions. + This will be addressed in . + + config: + Configuration for the web page to embed. + + """ + + properties: dict[str, AsComponents] = {} + if config is not None: + if not isinstance(config, blueprint_archetypes.WebPageViewConfig): + config = blueprint_archetypes.WebPageViewConfig(config) + properties["WebPageViewConfig"] = config + + super().__init__( + class_identifier="WebPage", + origin=origin, + contents=contents, + name=name, + visible=visible, + properties=properties, + defaults=defaults, + overrides=overrides, + ) From 530d1cf29c2fa1eaf3c8b1ffec4d7a8b909a6641 Mon Sep 17 00:00:00 2001 From: cc Date: Sat, 13 Jun 2026 11:46:23 -0700 Subject: [PATCH 02/12] feat: add dimos web page command --- Cargo.lock | 2 + crates/top/rerun/src/commands/mod.rs | 4 +- crates/top/rerun/src/lib.rs | 5 +- crates/viewer/re_view_web_page/src/backend.rs | 15 +- .../viewer/re_view_web_page/src/lifecycle.rs | 6 + .../re_view_web_page/src/native_backend.rs | 152 ++++++++- .../viewer/re_view_web_page/src/view_class.rs | 76 ++++- .../re_view_web_page/tests/web_page_view.rs | 42 ++- crates/viewer/re_viewer/src/app.rs | 38 +++ crates/viewer/re_viewer/src/app_state.rs | 80 ++++- crates/viewer/re_viewer/src/lib.rs | 2 +- dimos/Cargo.toml | 2 + dimos/src/interaction/handle.rs | 10 +- dimos/src/interaction/keyboard.rs | 77 +++-- dimos/src/interaction/mod.rs | 2 +- dimos/src/interaction/ws.rs | 301 +++++++++++++++++- dimos/src/viewer.rs | 80 ++++- .../add-dimos-web-page-command/.openspec.yaml | 2 + .../add-dimos-web-page-command/design.md | 58 ++++ .../add-dimos-web-page-command/pr-notes.md | 48 +++ .../add-dimos-web-page-command/proposal.md | 28 ++ .../specs/dimos-web-page-command/spec.md | 61 ++++ .../specs/native-web-page-view/spec.md | 20 ++ .../add-dimos-web-page-command/tasks.md | 38 +++ .../.openspec.yaml | 0 .../design.md | 0 .../proposal.md | 0 .../specs/native-web-page-view/spec.md | 0 .../tasks.md | 0 openspec/specs/native-web-page-view/spec.md | 144 +++++++++ 30 files changed, 1197 insertions(+), 96 deletions(-) create mode 100644 openspec/changes/add-dimos-web-page-command/.openspec.yaml create mode 100644 openspec/changes/add-dimos-web-page-command/design.md create mode 100644 openspec/changes/add-dimos-web-page-command/pr-notes.md create mode 100644 openspec/changes/add-dimos-web-page-command/proposal.md create mode 100644 openspec/changes/add-dimos-web-page-command/specs/dimos-web-page-command/spec.md create mode 100644 openspec/changes/add-dimos-web-page-command/specs/native-web-page-view/spec.md create mode 100644 openspec/changes/add-dimos-web-page-command/tasks.md rename openspec/changes/{add-native-web-page-view => archive/2026-06-12-add-native-web-page-view}/.openspec.yaml (100%) rename openspec/changes/{add-native-web-page-view => archive/2026-06-12-add-native-web-page-view}/design.md (100%) rename openspec/changes/{add-native-web-page-view => archive/2026-06-12-add-native-web-page-view}/proposal.md (100%) rename openspec/changes/{add-native-web-page-view => archive/2026-06-12-add-native-web-page-view}/specs/native-web-page-view/spec.md (100%) rename openspec/changes/{add-native-web-page-view => archive/2026-06-12-add-native-web-page-view}/tasks.md (100%) create mode 100644 openspec/specs/native-web-page-view/spec.md diff --git a/Cargo.lock b/Cargo.lock index b0fe43b0ab0f..dd3d20185f39 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3166,11 +3166,13 @@ dependencies = [ "clap", "futures-util", "mimalloc", + "parking_lot", "rerun", "serde", "serde_json", "tokio", "tokio-tungstenite", + "url", ] [[package]] diff --git a/crates/top/rerun/src/commands/mod.rs b/crates/top/rerun/src/commands/mod.rs index de14da278940..8675c73d5fee 100644 --- a/crates/top/rerun/src/commands/mod.rs +++ b/crates/top/rerun/src/commands/mod.rs @@ -38,8 +38,8 @@ mod analytics; pub(crate) use self::analytics::AnalyticsCommands; pub use self::download::DownloadCommand; pub use self::entrypoint::{ - run, run_with_app_wrapper, AppWrapper, StartupOptionsPatch, Args as RerunArgs, - native_startup_options_from_args, + AppWrapper, Args as RerunArgs, StartupOptionsPatch, native_startup_options_from_args, run, + run_with_app_wrapper, }; #[cfg(feature = "importers")] pub use self::mcap::McapCommands; diff --git a/crates/top/rerun/src/lib.rs b/crates/top/rerun/src/lib.rs index 272133e1ef25..0904bcf7f090 100644 --- a/crates/top/rerun/src/lib.rs +++ b/crates/top/rerun/src/lib.rs @@ -124,7 +124,10 @@ pub mod demo_util; pub mod log_integration; #[cfg(feature = "run")] -pub use commands::{CallSource, run, run_with_app_wrapper, AppWrapper, StartupOptionsPatch, RerunArgs, native_startup_options_from_args}; +pub use commands::{ + AppWrapper, CallSource, RerunArgs, StartupOptionsPatch, native_startup_options_from_args, run, + run_with_app_wrapper, +}; #[cfg(feature = "log")] pub use log_integration::Logger; #[cfg(feature = "log")] diff --git a/crates/viewer/re_view_web_page/src/backend.rs b/crates/viewer/re_view_web_page/src/backend.rs index 2cb87e67a8f8..8c7441e19ef3 100644 --- a/crates/viewer/re_view_web_page/src/backend.rs +++ b/crates/viewer/re_view_web_page/src/backend.rs @@ -74,6 +74,17 @@ impl WebViewInstance { } } + pub(crate) fn set_visible(&self, visible: bool) { + #[cfg(not(all(not(target_arch = "wasm32"), feature = "native_webview")))] + let _ = self; + let _ = visible; + + #[cfg(all(not(target_arch = "wasm32"), feature = "native_webview"))] + if self.has_native_webview { + crate::native_backend::set_visible(self.view_id, visible); + } + } + pub(crate) fn go_back(&self) { #[cfg(debug_assertions)] if let Some(fake_backend) = &self.fake_backend { @@ -154,8 +165,8 @@ impl WebViewBounds { let min = rect.min * pixels_per_point; let size = rect.size() * pixels_per_point; Self { - min: [min.x, min.y], - size: [size.x, size.y], + min: [min.x.round(), min.y.round()], + size: [size.x.round(), size.y.round()], } } } diff --git a/crates/viewer/re_view_web_page/src/lifecycle.rs b/crates/viewer/re_view_web_page/src/lifecycle.rs index 66570dd1f5a9..133e384af4af 100644 --- a/crates/viewer/re_view_web_page/src/lifecycle.rs +++ b/crates/viewer/re_view_web_page/src/lifecycle.rs @@ -52,6 +52,12 @@ impl WebViewLifecycle { self.last_bounds = Some(bounds); } + pub(crate) fn set_visible(&mut self, visible: bool) { + if let Some(webview) = &mut self.webview { + webview.set_visible(visible); + } + } + pub(crate) fn go_back(&mut self) { if let Some(webview) = &mut self.webview { webview.go_back(); diff --git a/crates/viewer/re_view_web_page/src/native_backend.rs b/crates/viewer/re_view_web_page/src/native_backend.rs index b1801d90b643..4aa9794901c9 100644 --- a/crates/viewer/re_view_web_page/src/native_backend.rs +++ b/crates/viewer/re_view_web_page/src/native_backend.rs @@ -10,6 +10,10 @@ use crate::backend::WebViewBounds; thread_local! { static NATIVE_WEBVIEWS: std::cell::RefCell> = std::cell::RefCell::new(ahash::HashMap::default()); + static VISIBLE_THIS_FRAME: std::cell::RefCell> = + std::cell::RefCell::new(ahash::HashSet::default()); + static OVERLAY_CLIP_BOUNDS: std::cell::Cell> = + const { std::cell::Cell::new(None) }; } scoped_tls::scoped_thread_local!(static NATIVE_PARENT_WINDOW: eframe::Frame); @@ -19,12 +23,15 @@ pub struct NativeWebViewBackend; pub struct NativeWebView { webview: wry::WebView, + visible: bool, + bounds: WebViewBounds, } #[derive(Debug)] pub enum NativeWebViewError { MissingParentWindow, Wry(wry::Error), + Clip(String), } impl std::fmt::Display for NativeWebViewError { @@ -32,6 +39,7 @@ impl std::fmt::Display for NativeWebViewError { match self { Self::MissingParentWindow => f.write_str("missing parent native window"), Self::Wry(err) => write!(f, "failed to create native webview: {err}"), + Self::Clip(err) => write!(f, "failed to clip native webview: {err}"), } } } @@ -61,14 +69,26 @@ impl NativeWebViewBackend { let _ = self; NATIVE_PARENT_WINDOW.with(|parent_window| { let webview = platform::create_child(parent_window, url, bounds)?; - Ok(NativeWebView { webview }) + Ok(NativeWebView { + webview, + visible: true, + bounds, + }) }) } } +pub fn set_overlay_clip_rect(rect: egui::Rect, pixels_per_point: f32) { + OVERLAY_CLIP_BOUNDS.with(|overlay_clip_bounds| { + overlay_clip_bounds.set(Some(WebViewBounds::from_egui_rect(rect, pixels_per_point))); + }); +} + pub fn with_native_parent_window(frame: &eframe::Frame, f: impl FnOnce() -> R) -> R { NATIVE_PARENT_WINDOW.set(frame, || { + begin_frame(); let result = f(); + hide_webviews_not_drawn_this_frame(); platform::pump_events(); result }) @@ -79,8 +99,30 @@ pub(crate) fn has_native_parent_window() -> bool { } impl NativeWebView { - pub(crate) fn set_bounds(&self, bounds: WebViewBounds) -> Result<(), NativeWebViewError> { - self.webview.set_bounds(bounds.into()).map_err(Into::into) + pub(crate) fn set_bounds(&mut self, bounds: WebViewBounds) -> Result<(), NativeWebViewError> { + self.webview + .set_bounds(bounds.into()) + .map_err(NativeWebViewError::from)?; + self.bounds = bounds; + self.apply_overlay_clip() + } + + fn apply_overlay_clip(&self) -> Result<(), NativeWebViewError> { + let overlay_clip_bounds = + OVERLAY_CLIP_BOUNDS.with(|overlay_clip_bounds| overlay_clip_bounds.get()); + platform::apply_overlay_clip(&self.webview, self.bounds, overlay_clip_bounds) + } + + pub(crate) fn set_visible(&mut self, visible: bool) -> Result<(), NativeWebViewError> { + if self.visible == visible { + return Ok(()); + } + + self.webview + .set_visible(visible) + .map_err(NativeWebViewError::from)?; + self.visible = visible; + Ok(()) } pub(crate) fn go_back(&self) -> Result<(), NativeWebViewError> { @@ -120,25 +162,38 @@ pub(crate) fn set_bounds(view_id: ViewId, bounds: WebViewBounds) { with_webview(view_id, |webview| webview.set_bounds(bounds)); } +pub(crate) fn set_visible(view_id: ViewId, visible: bool) { + if visible { + VISIBLE_THIS_FRAME.with_borrow_mut(|visible_this_frame| { + visible_this_frame.insert(view_id); + }); + } + + with_webview(view_id, |webview| webview.set_visible(visible)); +} + pub(crate) fn go_back(view_id: ViewId) { - with_webview(view_id, NativeWebView::go_back); + with_webview(view_id, |webview| webview.go_back()); } pub(crate) fn go_forward(view_id: ViewId) { - with_webview(view_id, NativeWebView::go_forward); + with_webview(view_id, |webview| webview.go_forward()); } pub(crate) fn reload(view_id: ViewId) { - with_webview(view_id, NativeWebView::reload); + with_webview(view_id, |webview| webview.reload()); } pub(crate) fn navigate_to(view_id: ViewId, url: &str) { with_webview(view_id, |webview| webview.navigate_to(url)); } -fn with_webview(view_id: ViewId, f: impl FnOnce(&NativeWebView) -> Result<(), NativeWebViewError>) { - NATIVE_WEBVIEWS.with_borrow(|webviews| { - if let Some(webview) = webviews.get(&view_id) { +fn with_webview( + view_id: ViewId, + f: impl FnOnce(&mut NativeWebView) -> Result<(), NativeWebViewError>, +) { + NATIVE_WEBVIEWS.with_borrow_mut(|webviews| { + if let Some(webview) = webviews.get_mut(&view_id) { match f(webview) { Ok(()) | Err(_) => {} } @@ -146,6 +201,26 @@ fn with_webview(view_id: ViewId, f: impl FnOnce(&NativeWebView) -> Result<(), Na }); } +fn begin_frame() { + VISIBLE_THIS_FRAME.with_borrow_mut(|visible_this_frame| { + visible_this_frame.clear(); + }); +} + +fn hide_webviews_not_drawn_this_frame() { + VISIBLE_THIS_FRAME.with_borrow(|visible_this_frame| { + NATIVE_WEBVIEWS.with_borrow_mut(|webviews| { + for (view_id, webview) in webviews { + if !visible_this_frame.contains(view_id) { + match webview.set_visible(false) { + Ok(()) | Err(_) => {} + } + } + } + }); + }); +} + impl From for wry::Rect { fn from(bounds: WebViewBounds) -> Self { let min_x = bounds.min[0].max(0.0).round() as u32; @@ -162,11 +237,20 @@ impl From for wry::Rect { #[cfg(target_os = "linux")] mod platform { + use gtk::prelude::WidgetExt; use raw_window_handle::HasWindowHandle; + use wry::WebViewExtUnix as _; + + use super::NativeWebViewError; pub(super) fn pump_events() { if gtk::is_initialized_main_thread() { - while gtk::events_pending() { + // Keep WebKitGTK responsive without letting its event queue monopolize an egui frame. + // Further events will be drained on subsequent frames. + for _ in 0..16 { + if !gtk::events_pending() { + break; + } gtk::main_iteration_do(false); } } @@ -187,6 +271,46 @@ mod platform { .with_url(url) .build_as_child(parent_window) } + + pub(super) fn apply_overlay_clip( + webview: &wry::WebView, + webview_bounds: crate::backend::WebViewBounds, + overlay_bounds: Option, + ) -> Result<(), NativeWebViewError> { + let width = webview_bounds.size[0].max(1.0).round() as i32; + let height = webview_bounds.size[1].max(1.0).round() as i32; + + let region = gtk::cairo::Region::create_rectangle(>k::cairo::RectangleInt::new( + 0, 0, width, height, + )); + + if let Some(overlay_bounds) = overlay_bounds { + let left = webview_bounds.min[0].max(overlay_bounds.min[0]); + let top = webview_bounds.min[1].max(overlay_bounds.min[1]); + let right = (webview_bounds.min[0] + webview_bounds.size[0]) + .min(overlay_bounds.min[0] + overlay_bounds.size[0]); + let bottom = (webview_bounds.min[1] + webview_bounds.size[1]) + .min(overlay_bounds.min[1] + overlay_bounds.size[1]); + + if right > left && bottom > top { + region + .subtract_rectangle(>k::cairo::RectangleInt::new( + (left - webview_bounds.min[0]).round() as i32, + (top - webview_bounds.min[1]).round() as i32, + (right - left).round().max(1.0) as i32, + (bottom - top).round().max(1.0) as i32, + )) + .map_err(|err| NativeWebViewError::Clip(err.to_string()))?; + } + } + + if let Some(window) = webview.webview().window() { + window.shape_combine_region(Some(®ion), 0, 0); + window.input_shape_combine_region(®ion, 0, 0); + } + + Ok(()) + } } #[cfg(not(target_os = "linux"))] @@ -205,4 +329,12 @@ mod platform { .with_url(url) .build_as_child(parent_window) } + + pub(super) fn apply_overlay_clip( + _webview: &wry::WebView, + _webview_bounds: crate::backend::WebViewBounds, + _overlay_bounds: Option, + ) -> Result<(), super::NativeWebViewError> { + Ok(()) + } } diff --git a/crates/viewer/re_view_web_page/src/view_class.rs b/crates/viewer/re_view_web_page/src/view_class.rs index 5afbbe934a09..069e627bb59b 100644 --- a/crates/viewer/re_view_web_page/src/view_class.rs +++ b/crates/viewer/re_view_web_page/src/view_class.rs @@ -4,7 +4,7 @@ use re_sdk_types::blueprint::{ components::{ShowNavigationControls, WebPageUrl}, }; use re_sdk_types::{View as _, ViewClassIdentifier}; -use re_ui::{Help, icons}; +use re_ui::{Help, UiExt as _, icons}; use re_viewer_context::{ ViewClass, ViewClassLayoutPriority, ViewClassRegistryError, ViewId, ViewQuery, ViewSpawnHeuristics, ViewState, ViewSystemExecutionError, ViewerContext, @@ -18,6 +18,10 @@ use crate::url_policy::{UrlPolicyResult, validate_url}; #[derive(Default)] struct WebPageViewState { lifecycle: WebViewLifecycle, + address_bar_url: String, + address_bar_home_url: Option, + address_bar_error: Option, + pending_navigation_command: Option, } impl ViewState for WebPageViewState { @@ -160,6 +164,13 @@ impl ViewClass for WebPageView { .as_any_mut() .downcast_mut::() .ok_or(ViewSystemExecutionError::StateCastError("WebPageViewState"))?; + + if state.address_bar_home_url.as_deref() != Some(url.as_str()) { + state.address_bar_url.clone_from(&url); + state.address_bar_home_url = Some(url.clone()); + state.address_bar_error = None; + } + if config.show_navigation_controls { let mut navigation_command = None; ui.horizontal(|ui| { @@ -175,19 +186,54 @@ impl ViewClass for WebPageView { if ui.button("Home").clicked() { navigation_command = Some(NavigationCommand::Home); } - ui.label(&url); + let label = ui.label("Address"); + let go_button_width = + ui.spacing().interact_size.x + ui.spacing().button_padding.x * 2.0; + let address_width = + (ui.available_width() - go_button_width - ui.spacing().item_spacing.x) + .max(80.0); + let response = ui + .add( + egui::TextEdit::singleline(&mut state.address_bar_url) + .desired_width(address_width) + .hint_text("Address"), + ) + .labelled_by(label.id); + let go_clicked = ui.button("Go").clicked(); + if go_clicked + || response.has_focus() && ui.input(|input| input.key_pressed(egui::Key::Enter)) + { + match validate_url(&state.address_bar_url) { + UrlPolicyResult::Accepted(address_bar_url) => { + state.address_bar_url.clone_from(&address_bar_url); + state.address_bar_error = None; + navigation_command = + Some(NavigationCommand::NavigateTo(address_bar_url)); + } + UrlPolicyResult::Invalid => { + state.address_bar_error = Some("Invalid URL".to_owned()); + } + UrlPolicyResult::UnsupportedScheme(scheme) => { + state.address_bar_error = + Some(format!("Unsupported URL scheme: {scheme}")); + } + } + } }); + if let Some(error) = &state.address_bar_error { + ui.error_label(error); + } ui.separator(); - match navigation_command { - Some(NavigationCommand::Back) => state.lifecycle.go_back(), - Some(NavigationCommand::Forward) => state.lifecycle.go_forward(), - Some(NavigationCommand::Reload) => state.lifecycle.reload(), - Some(NavigationCommand::Home) => state.lifecycle.navigate_to(&url), - None => {} + if matches!(navigation_command, Some(NavigationCommand::Home)) { + state.address_bar_url.clone_from(&url); + state.address_bar_error = None; } + + state.pending_navigation_command = navigation_command; } else { ui.label(&url); + state.pending_navigation_command = None; } let webview_rect = ui.available_rect_before_wrap(); @@ -201,6 +247,19 @@ impl ViewClass for WebPageView { match lifecycle_status { WebViewLifecycleStatus::Ready => { state.lifecycle.update_bounds(query.view_id, webview_bounds); + state.lifecycle.set_visible(true); + match state.pending_navigation_command.take() { + Some(NavigationCommand::Back) => state.lifecycle.go_back(), + Some(NavigationCommand::Forward) => state.lifecycle.go_forward(), + Some(NavigationCommand::Reload) => state.lifecycle.reload(), + Some(NavigationCommand::Home) => { + state.lifecycle.navigate_to(&url); + } + Some(NavigationCommand::NavigateTo(address_bar_url)) => { + state.lifecycle.navigate_to(&address_bar_url); + } + None => {} + } ui.allocate_rect(webview_rect, egui::Sense::hover()); } WebViewLifecycleStatus::Unavailable => { @@ -227,4 +286,5 @@ enum NavigationCommand { Forward, Reload, Home, + NavigateTo(String), } diff --git a/crates/viewer/re_view_web_page/tests/web_page_view.rs b/crates/viewer/re_view_web_page/tests/web_page_view.rs index b8e5577252cf..68969c49c65b 100644 --- a/crates/viewer/re_view_web_page/tests/web_page_view.rs +++ b/crates/viewer/re_view_web_page/tests/web_page_view.rs @@ -78,10 +78,12 @@ fn https_url_is_accepted() { harness.run(); - assert!( + assert_eq!( harness - .query_by_label_contains("https://example.com") - .is_some() + .get_by_role_and_label(egui::accesskit::Role::TextInput, "Address") + .value() + .as_deref(), + Some("https://example.com") ); assert!(harness.query_by_label_contains("Invalid URL").is_none()); assert!( @@ -104,10 +106,12 @@ fn localhost_http_url_is_accepted() { harness.run(); - assert!( + assert_eq!( harness - .query_by_label_contains("http://localhost:3000") - .is_some() + .get_by_role_and_label(egui::accesskit::Role::TextInput, "Address") + .value() + .as_deref(), + Some("http://localhost:3000") ); assert!(harness.query_by_label_contains("Invalid URL").is_none()); assert!( @@ -182,7 +186,31 @@ fn home_navigation_does_not_mutate_configured_url() { assert_eq!(navigation_requests.len(), 1); assert_eq!(navigation_requests[0].view_id, view_id); assert_eq!(navigation_requests[0].url, configured_url); - assert!(harness.query_by_label_contains(configured_url).is_some()); + assert_eq!( + harness + .get_by_role_and_label(egui::accesskit::Role::TextInput, "Address") + .value() + .as_deref(), + Some(configured_url) + ); +} + +#[test] +fn navigation_controls_include_editable_address_bar() { + let configured_url = "https://example.com/home"; + let mut test_context = TestContext::new_with_view_class::(); + let view_id = setup_configured_web_page_view(&mut test_context, configured_url, true); + + let mut harness = test_context + .setup_kittest_for_rendering_ui([700.0, 250.0]) + .build_ui(|ui| { + test_context.run_with_single_view(ui, view_id); + }); + harness.run(); + + let address_bar = harness.get_by_role_and_label(egui::accesskit::Role::TextInput, "Address"); + assert_eq!(address_bar.value().as_deref(), Some(configured_url)); + assert!(harness.query_by_label("Go").is_some()); } #[test] diff --git a/crates/viewer/re_viewer/src/app.rs b/crates/viewer/re_viewer/src/app.rs index 3ce8c0c52722..49813c49aae9 100644 --- a/crates/viewer/re_viewer/src/app.rs +++ b/crates/viewer/re_viewer/src/app.rs @@ -158,6 +158,19 @@ pub struct App { async_runtime: AsyncRuntimeHandle, } +/// Request to open or update a Web Page View in the active viewport. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct WebPageViewRequest { + /// Caller-owned stable identifier used to update the same panel later. + pub panel_id: String, + /// Human-readable panel title. + pub title: String, + /// Configured page URL. + pub url: String, + /// Whether browser-like controls should be visible. + pub show_navigation_controls: bool, +} + impl App { pub fn new( main_thread_token: MainThreadToken, @@ -488,6 +501,31 @@ impl App { &self.connection_registry } + /// Queue a Web Page View panel request for the active viewport. + /// + /// This is intended for thin custom viewer wrappers such as `DimOS`. The request is translated + /// into normal blueprint state on the next viewer frame. + pub fn open_or_update_web_page_view(&mut self, request: WebPageViewRequest) { + self.state.queue_web_page_view_request(request); + self.egui_ctx.request_repaint(); + } + + /// Clip native Web Page View surfaces around an egui overlay rectangle. + /// + /// Native webviews are OS child surfaces and can cover egui foreground overlays. This keeps the + /// overlay appearance unchanged by punching an overlay-shaped hole in the native surface where + /// supported. Platforms that do not need/support this ignore the request. + pub fn set_web_page_overlay_clip_rect(&self, rect: egui::Rect) { + #[cfg(all(not(target_arch = "wasm32"), feature = "native_webview"))] + re_view_web_page::native_backend::set_overlay_clip_rect( + rect, + self.egui_ctx.pixels_per_point(), + ); + + #[cfg(not(all(not(target_arch = "wasm32"), feature = "native_webview")))] + let _ = (self, rect); + } + pub fn set_examples_manifest_url(&mut self, url: String) { re_log::info!("Using manifest_url={url:?}"); self.state.set_examples_manifest_url(&self.egui_ctx, url); diff --git a/crates/viewer/re_viewer/src/app_state.rs b/crates/viewer/re_viewer/src/app_state.rs index 83d8f267ac65..f085a230085c 100644 --- a/crates/viewer/re_viewer/src/app_state.rs +++ b/crates/viewer/re_viewer/src/app_state.rs @@ -11,7 +11,9 @@ use re_log_channel::{LogReceiverSet, LogSource, RecordingOpenBehavior}; use re_log_types::{AbsoluteTimeRangeF, StoreId, TableId}; use re_redap_browser::RedapServers; use re_redap_client::ConnectionRegistryHandle; +use re_sdk_types::blueprint::archetypes::{self as blueprint_archetypes, WebPageViewConfig}; use re_sdk_types::blueprint::components::{PanelState, PlayState}; +use re_sdk_types::components::Name; use re_ui::{ContextExt as _, UiExt as _}; use re_viewer_context::open_url::{self, ViewerOpenUrl}; use re_viewer_context::{ @@ -19,13 +21,14 @@ use re_viewer_context::{ AsyncRuntimeHandle, AuthContext, BlueprintContext, BlueprintUndoState, CommandSender, ComponentUiRegistry, DragAndDropManager, FallbackProviderRegistry, FocusTarget, Item, Route, SelectionChange, StorageContext, StoreHub, StoreViewContext, SystemCommand, - SystemCommandSender as _, TableStore, TimeControl, TimeControlCommand, ViewClassRegistry, - ViewStates, ViewerContext, blueprint_timeline, + SystemCommandSender as _, TableStore, TimeControl, TimeControlCommand, ViewClass as _, + ViewClassRegistry, ViewId, ViewStates, ViewerContext, blueprint_timeline, }; use re_viewport::ViewportUi; -use re_viewport_blueprint::ViewportBlueprint; use re_viewport_blueprint::ui::add_view_or_container_modal_ui; +use re_viewport_blueprint::{ViewBlueprint, ViewProperty, ViewportBlueprint}; +use crate::app::WebPageViewRequest; use crate::app_blueprint::AppBlueprint; use crate::navigation::Navigation; use crate::open_url_description::ViewerOpenUrlDescription; @@ -121,6 +124,14 @@ pub struct AppState { /// Are we logged in? #[serde(skip)] pub(crate) auth_state: Option, + + /// Queued external requests to create or update Web Page View panels. + #[serde(skip)] + pending_web_page_view_requests: Vec, + + /// Runtime-only mapping from external panel ids to viewer view ids. + #[serde(skip)] + web_page_panel_ids: HashMap, } impl Default for AppState { @@ -146,6 +157,8 @@ impl Default for AppState { selection_state: Default::default(), focused_item: Default::default(), auth_state: Default::default(), + pending_web_page_view_requests: Default::default(), + web_page_panel_ids: Default::default(), #[cfg(feature = "testing")] test_hook: None, @@ -177,6 +190,10 @@ impl AppState { self.welcome_screen.set_examples_manifest_url(egui_ctx, url); } + pub fn queue_web_page_view_request(&mut self, request: WebPageViewRequest) { + self.pending_web_page_view_requests.push(request); + } + pub fn app_options(&self) -> &AppOptions { &self.app_options } @@ -301,6 +318,8 @@ impl AppState { selection_state, focused_item, auth_state, + pending_web_page_view_requests, + web_page_panel_ids, .. } = self; @@ -474,6 +493,13 @@ impl AppState { egui_ctx.request_repaint(); } + apply_pending_web_page_view_requests( + &ctx, + &viewport_ui.blueprint, + pending_web_page_view_requests, + web_page_panel_ids, + ); + // Update the viewport. May spawn new views and handle queued requests (like screenshots). viewport_ui.on_frame_start(&ctx); @@ -1025,3 +1051,51 @@ impl re_byte_size::MemUsageTreeCapture for AppState { tree.into_tree() } } + +fn apply_pending_web_page_view_requests( + ctx: &ViewerContext<'_>, + viewport_blueprint: &ViewportBlueprint, + pending_requests: &mut Vec, + panel_ids: &mut HashMap, +) { + for request in pending_requests.drain(..) { + let view_id = panel_ids + .get(&request.panel_id) + .copied() + .filter(|view_id| viewport_blueprint.view(view_id).is_some()) + .unwrap_or_else(|| { + let mut view = ViewBlueprint::new_with_root_wildcard( + re_view_web_page::WebPageView::identifier(), + ); + view.display_name = Some(request.title.clone()); + + let view_id = viewport_blueprint.add_view_at_root(view); + panel_ids.insert(request.panel_id.clone(), view_id); + view_id + }); + + save_web_page_view_request(ctx, view_id, &request); + viewport_blueprint.focus_tab(view_id); + } +} + +fn save_web_page_view_request( + ctx: &ViewerContext<'_>, + view_id: ViewId, + request: &WebPageViewRequest, +) { + ctx.save_blueprint_component( + view_id.as_entity_path(), + &blueprint_archetypes::ViewBlueprint::descriptor_display_name(), + &Name::from(request.title.clone()), + ); + + let config_property = ViewProperty::from_archetype::( + ctx.blueprint_db(), + ctx.blueprint_query, + view_id, + ); + let config = WebPageViewConfig::new(request.url.clone()) + .with_show_navigation_controls(request.show_navigation_controls); + ctx.save_blueprint_archetype(config_property.blueprint_store_path, &config); +} diff --git a/crates/viewer/re_viewer/src/lib.rs b/crates/viewer/re_viewer/src/lib.rs index 1dacec0b394a..a209285b6c25 100644 --- a/crates/viewer/re_viewer/src/lib.rs +++ b/crates/viewer/re_viewer/src/lib.rs @@ -63,7 +63,7 @@ mod loading; /// Unstable. Used for the ongoing blueprint experimentations. pub mod blueprint; -pub use app::App; +pub use app::{App, WebPageViewRequest}; pub(crate) use app_state::AppState; pub use event::{SelectionChangeItem, ViewerEvent, ViewerEventKind}; pub use re_capabilities::MainThreadToken; diff --git a/dimos/Cargo.toml b/dimos/Cargo.toml index db8b34bf5e43..2e6bd33cf613 100644 --- a/dimos/Cargo.toml +++ b/dimos/Cargo.toml @@ -30,6 +30,7 @@ bincode.workspace = true clap.workspace = true futures-util.workspace = true mimalloc.workspace = true +parking_lot.workspace = true serde = { workspace = true, features = ["derive"] } serde_json.workspace = true tokio = { workspace = true, features = [ @@ -42,6 +43,7 @@ tokio = { workspace = true, features = [ "time", ] } tokio-tungstenite = "0.28.0" +url.workspace = true [lints] workspace = true diff --git a/dimos/src/interaction/handle.rs b/dimos/src/interaction/handle.rs index 0f71a6f11fd6..b29fd1ea41a0 100644 --- a/dimos/src/interaction/handle.rs +++ b/dimos/src/interaction/handle.rs @@ -1,5 +1,5 @@ -use tokio::sync::mpsc; use super::protocol::ViewerEvent; +use tokio::sync::mpsc; /// Handle for sending interaction events from the viewer to the application. /// @@ -58,7 +58,13 @@ mod tests { let event = rx.try_recv().unwrap(); match event { - ViewerEvent::Click { position, entity_path, view_id, is_2d, .. } => { + ViewerEvent::Click { + position, + entity_path, + view_id, + is_2d, + .. + } => { assert_eq!(position, [1.0, 2.0, 3.0]); assert_eq!(entity_path, Some("world/robot".to_string())); assert_eq!(view_id, "view_123"); diff --git a/dimos/src/interaction/keyboard.rs b/dimos/src/interaction/keyboard.rs index 291586df7024..c7f531d0cdac 100644 --- a/dimos/src/interaction/keyboard.rs +++ b/dimos/src/interaction/keyboard.rs @@ -10,9 +10,9 @@ use super::ws::WsPublisher; use rerun::external::{egui, re_log}; /// Base speeds for keyboard control -const BASE_LINEAR_SPEED: f64 = 0.5; // m/s -const BASE_ANGULAR_SPEED: f64 = 0.8; // rad/s -const FAST_MULTIPLIER: f64 = 2.0; // Shift modifier +const BASE_LINEAR_SPEED: f64 = 0.5; // m/s +const BASE_ANGULAR_SPEED: f64 = 0.8; // rad/s +const FAST_MULTIPLIER: f64 = 2.0; // Shift modifier /// Overlay styling const OVERLAY_PADDING: f32 = 10.0; @@ -29,13 +29,13 @@ const ESTOP_ACTIVE_BG: egui::Color32 = egui::Color32::from_rgb(220, 50, 50); /// Tracks which movement keys are currently held down. #[derive(Debug, Clone, Default)] struct KeyState { - forward: bool, // W or Up - backward: bool, // S or Down - left: bool, // A or Left - right: bool, // D or Right - strafe_l: bool, // Q - strafe_r: bool, // E - fast: bool, // Shift held + forward: bool, // W or Up + backward: bool, // S or Down + left: bool, // A or Left + right: bool, // D or Right + strafe_l: bool, // Q + strafe_r: bool, // E + fast: bool, // Shift held } impl KeyState { @@ -66,8 +66,8 @@ pub struct KeyboardHandler { ws: WsPublisher, state: KeyState, was_active: bool, - estop_flash: bool, // true briefly after space pressed - engaged: bool, // true when user has clicked the overlay to activate + estop_flash: bool, // true briefly after space pressed + engaged: bool, // true when user has clicked the overlay to activate } impl KeyboardHandler { @@ -133,7 +133,7 @@ impl KeyboardHandler { /// Draw keyboard overlay HUD anchored to the bottom-right of the viewport. /// Clickable: clicking the overlay toggles engaged state. - pub fn draw_overlay(&mut self, ctx: &egui::Context) { + pub fn draw_overlay(&mut self, ctx: &egui::Context) -> egui::Rect { let area_response = egui::Area::new("dimos_keyboard_hud_br".into()) .anchor(egui::Align2::RIGHT_BOTTOM, egui::vec2(-5.0, -5.0)) .order(egui::Order::Foreground) @@ -197,11 +197,17 @@ impl KeyboardHandler { self.state.reset(); self.was_active = false; } + + area_response.interact_rect } fn draw_hud_content(&self, ui: &mut egui::Ui) { // Title - ui.label(egui::RichText::new("Keyboard Teleop").color(LABEL_COLOR).size(13.0)); + ui.label( + egui::RichText::new("Keyboard Teleop") + .color(LABEL_COLOR) + .size(13.0), + ); ui.add_space(4.0); // Key grid: [Q] [W] [E] @@ -236,16 +242,19 @@ impl KeyboardHandler { // Space bar (e-stop) let space_width = KEY_SIZE * 3.0 + KEY_GAP * 2.0; - let space_rect = ui.allocate_exact_size( - egui::vec2(space_width, KEY_SIZE * 0.7), - egui::Sense::hover(), - ).0; + let space_rect = ui + .allocate_exact_size( + egui::vec2(space_width, KEY_SIZE * 0.7), + egui::Sense::hover(), + ) + .0; let space_bg = if self.estop_flash { ESTOP_ACTIVE_BG } else { KEY_INACTIVE_BG }; - ui.painter().rect_filled(space_rect, egui::CornerRadius::same(4), space_bg); + ui.painter() + .rect_filled(space_rect, egui::CornerRadius::same(4), space_bg); ui.painter().text( space_rect.center(), egui::Align2::CENTER_CENTER, @@ -257,22 +266,33 @@ impl KeyboardHandler { ui.add_space(4.0); // Speed indicator - let speed_label = if self.state.fast { "⇧ FAST" } else { "⇧ shift=fast" }; + let speed_label = if self.state.fast { + "⇧ FAST" + } else { + "⇧ shift=fast" + }; let speed_color = if self.state.fast { egui::Color32::from_rgb(255, 200, 50) } else { LABEL_COLOR }; - ui.label(egui::RichText::new(speed_label).color(speed_color).size(10.0)); + ui.label( + egui::RichText::new(speed_label) + .color(speed_color) + .size(10.0), + ); } fn draw_key(&self, ui: &mut egui::Ui, label: &str, pressed: bool) { - let (rect, _) = ui.allocate_exact_size( - egui::vec2(KEY_SIZE, KEY_SIZE), - egui::Sense::hover(), - ); - let bg = if pressed { KEY_ACTIVE_BG } else { KEY_INACTIVE_BG }; - ui.painter().rect_filled(rect, egui::CornerRadius::same(4), bg); + let (rect, _) = + ui.allocate_exact_size(egui::vec2(KEY_SIZE, KEY_SIZE), egui::Sense::hover()); + let bg = if pressed { + KEY_ACTIVE_BG + } else { + KEY_INACTIVE_BG + }; + ui.painter() + .rect_filled(rect, egui::CornerRadius::same(4), bg); ui.painter().text( rect.center(), egui::Align2::CENTER_CENTER, @@ -298,7 +318,8 @@ impl KeyboardHandler { /// Convert current KeyState to Twist and publish via WebSocket. fn publish_twist(&mut self) -> Result<(), super::ws::SendError> { let (lin_x, lin_y, lin_z, ang_x, ang_y, ang_z) = self.compute_twist(); - self.ws.send_twist(lin_x, lin_y, lin_z, ang_x, ang_y, ang_z)?; + self.ws + .send_twist(lin_x, lin_y, lin_z, ang_x, ang_y, ang_z)?; if std::env::var("DIMOS_DEBUG").is_ok_and(|v| v == "1") { eprintln!( diff --git a/dimos/src/interaction/mod.rs b/dimos/src/interaction/mod.rs index 76ab58df72d3..78fe38e7b79f 100644 --- a/dimos/src/interaction/mod.rs +++ b/dimos/src/interaction/mod.rs @@ -6,4 +6,4 @@ pub mod ws; pub use handle::InteractionHandle; pub use keyboard::KeyboardHandler; pub use protocol::ViewerEvent; -pub use ws::{SendError, WsPublisher}; +pub use ws::{SendError, WsCommand, WsPublisher}; diff --git a/dimos/src/interaction/ws.rs b/dimos/src/interaction/ws.rs index a05636b5488b..caee89539798 100644 --- a/dimos/src/interaction/ws.rs +++ b/dimos/src/interaction/ws.rs @@ -13,10 +13,10 @@ //! {"type":"stop"} //! ``` -use std::time::Duration; +use std::{sync::Arc, time::Duration}; -use rerun::external::re_log; -use serde::Serialize; +use parking_lot::Mutex; +use serde::{Deserialize, Serialize}; use tokio::sync::mpsc; /// Error returned when a WebSocket event cannot be sent. @@ -59,6 +59,70 @@ pub enum WsEvent { Stop, } +/// JSON message variants received from the WebSocket server. +#[derive(Debug, Clone, PartialEq, Eq, Deserialize)] +#[serde(tag = "type", rename_all = "snake_case", deny_unknown_fields)] +pub enum WsCommand { + /// Request that DimOS opens or updates a Web Page View panel. + OpenWebPageView { + /// Caller-owned stable identifier used to update the same panel later. + panel_id: String, + /// Human-readable panel title. + title: String, + /// Configured page URL. + url: String, + /// Whether browser-like controls should be visible. + show_navigation_controls: bool, + }, +} + +/// Error returned when an inbound WebSocket command is not safe to apply. +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum WsCommandValidationError { + /// URL text could not be parsed as an absolute URL. + InvalidUrl, + /// URL uses a scheme that Web Page View does not allow. + UnsupportedUrlScheme(String), +} + +impl std::fmt::Display for WsCommandValidationError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::InvalidUrl => write!(f, "invalid URL"), + Self::UnsupportedUrlScheme(scheme) => write!(f, "unsupported URL scheme: {scheme}"), + } + } +} + +impl WsCommand { + /// Validate command fields that affect viewer state. + pub fn validate(&self) -> Result<(), WsCommandValidationError> { + match self { + Self::OpenWebPageView { url, .. } => validate_web_page_url(url), + } + } +} + +fn validate_web_page_url(url: &str) -> Result<(), WsCommandValidationError> { + let parsed = match url::Url::parse(url) { + Ok(parsed) => parsed, + Err(err) => { + let _err = err; + return Err(WsCommandValidationError::InvalidUrl); + } + }; + match parsed.scheme() { + "http" | "https" => Ok(()), + scheme => Err(WsCommandValidationError::UnsupportedUrlScheme( + scheme.to_owned(), + )), + } +} + +fn parse_command(text: &str) -> Result { + serde_json::from_str(text) +} + /// Sends `WsEvent`s (serialized to JSON) to a remote WebSocket server. /// /// Maintains a persistent connection with automatic reconnection. The @@ -67,6 +131,7 @@ pub enum WsEvent { #[derive(Clone)] pub struct WsPublisher { tx: mpsc::Sender, + command_rx: Arc>>, } impl WsPublisher { @@ -79,34 +144,50 @@ impl WsPublisher { /// so it works even when called from a non-async context (like the eframe UI). pub fn connect(url: String) -> Self { let (tx, rx) = mpsc::channel::(256); + let (command_tx, command_rx) = mpsc::channel::(256); // Spawn a dedicated thread with its own tokio runtime. // This allows WsPublisher to work from the eframe UI thread which // doesn't have a tokio runtime. std::thread::Builder::new() - .name("ws-publisher".to_string()) + .name("ws-publisher".to_owned()) .spawn(move || { let rt = tokio::runtime::Builder::new_current_thread() .enable_all() .build() .expect("failed to create tokio runtime for WsPublisher"); - rt.block_on(run_client(url, rx)); + rt.block_on(run_client(url, rx, command_tx)); }) .expect("failed to spawn WsPublisher thread"); - Self { tx } + Self { + tx, + command_rx: Arc::new(Mutex::new(command_rx)), + } + } + + /// Try to receive one inbound command without blocking the UI thread. + pub fn try_recv_command(&self) -> Option { + self.command_rx.lock().try_recv().ok() } /// Publish a click event. - pub fn send_click(&self, x: f64, y: f64, z: f64, entity_path: &str, timestamp_ms: u64) -> Result<(), SendError> { + pub fn send_click( + &self, + x: f64, + y: f64, + z: f64, + entity_path: &str, + timestamp_ms: u64, + ) -> Result<(), SendError> { let event = WsEvent::Click { x, y, z, - entity_path: entity_path.to_string(), + entity_path: entity_path.to_owned(), timestamp_ms, }; - self.broadcast(event) + self.broadcast(&event) } /// Publish a twist (velocity) command. @@ -127,18 +208,22 @@ impl WsPublisher { angular_y, angular_z, }; - self.broadcast(event) + self.broadcast(&event) } /// Publish a stop command. pub fn send_stop(&self) -> Result<(), SendError> { - self.broadcast(WsEvent::Stop) + self.broadcast(&WsEvent::Stop) } - fn broadcast(&self, event: WsEvent) -> Result<(), SendError> { - let json = serde_json::to_string(&event).map_err(|e| SendError::Serialize(e.to_string()))?; + fn broadcast(&self, event: &WsEvent) -> Result<(), SendError> { + let json = + serde_json::to_string(&event).map_err(|e| SendError::Serialize(e.to_string()))?; // Non-blocking: error if the channel is full rather than block the UI thread. - self.tx.try_send(json).map_err(|_| SendError::QueueFull) + self.tx.try_send(json).map_err(|err| { + let _err = err; + SendError::QueueFull + }) } } @@ -148,8 +233,12 @@ fn is_debug() -> bool { } /// Background task: connect → send → reconnect loop. -async fn run_client(url: String, mut rx: mpsc::Receiver) { - use futures_util::{SinkExt, StreamExt}; +async fn run_client( + url: String, + mut rx: mpsc::Receiver, + command_tx: mpsc::Sender, +) { + use futures_util::{SinkExt as _, StreamExt as _}; use tokio_tungstenite::{connect_async, tungstenite::Message}; let debug = is_debug(); @@ -171,9 +260,28 @@ async fn run_client(url: String, mut rx: mpsc::Receiver) { // server's keepalive pings get answered and the connection stays // alive. Exits when the server closes or an error occurs. let debug_read = debug; + let command_tx_read = command_tx.clone(); let mut read_handle = tokio::spawn(async move { while let Some(frame) = reader.next().await { match frame { + Ok(Message::Text(text)) => match parse_command(&text) { + Ok(command) => { + if let Err(err) = command_tx_read.try_send(command) + && debug_read + { + eprintln!( + "[DIMOS_DEBUG] WsPublisher: inbound command dropped: {err}" + ); + } + } + Err(err) => { + if debug_read { + eprintln!( + "[DIMOS_DEBUG] WsPublisher: ignoring inbound message: {err}" + ); + } + } + }, Ok(Message::Close(_)) => { if debug_read { eprintln!("[DIMOS_DEBUG] WsPublisher: server sent close frame"); @@ -226,7 +334,9 @@ async fn run_client(url: String, mut rx: mpsc::Receiver) { } Err(err) => { if debug { - eprintln!("[DIMOS_DEBUG] WsPublisher: connection failed: {err} — retrying in 1s"); + eprintln!( + "[DIMOS_DEBUG] WsPublisher: connection failed: {err} — retrying in 1s" + ); } } } @@ -238,3 +348,160 @@ async fn run_client(url: String, mut rx: mpsc::Receiver) { tokio::time::sleep(Duration::from_secs(1)).await; } } + +#[cfg(test)] +mod tests { + use super::*; + + fn test_publisher_with_command_rx(command_rx: mpsc::Receiver) -> WsPublisher { + let (tx, _rx) = mpsc::channel::(1); + WsPublisher { + tx, + command_rx: Arc::new(Mutex::new(command_rx)), + } + } + + #[test] + fn parses_open_web_page_view_command() { + let command: WsCommand = serde_json::from_str( + r#"{ + "type": "open_web_page_view", + "panel_id": "viser", + "title": "Viser", + "url": "http://127.0.0.1:8095/", + "show_navigation_controls": true + }"#, + ) + .expect("valid open_web_page_view command should parse"); + + assert_eq!( + command, + WsCommand::OpenWebPageView { + panel_id: "viser".to_owned(), + title: "Viser".to_owned(), + url: "http://127.0.0.1:8095/".to_owned(), + show_navigation_controls: true, + } + ); + } + + #[test] + fn outbound_events_keep_existing_wire_format() { + let click = serde_json::to_value(WsEvent::Click { + x: 1.0, + y: 2.0, + z: 3.0, + entity_path: "/world".to_owned(), + timestamp_ms: 1234567890, + }) + .expect("click event should serialize"); + assert_eq!( + click, + serde_json::json!({ + "type": "click", + "x": 1.0, + "y": 2.0, + "z": 3.0, + "entity_path": "/world", + "timestamp_ms": 1234567890_u64, + }) + ); + + let twist = serde_json::to_value(WsEvent::Twist { + linear_x: 0.5, + linear_y: 0.0, + linear_z: 0.0, + angular_x: 0.0, + angular_y: 0.0, + angular_z: 0.8, + }) + .expect("twist event should serialize"); + assert_eq!( + twist, + serde_json::json!({ + "type": "twist", + "linear_x": 0.5, + "linear_y": 0.0, + "linear_z": 0.0, + "angular_x": 0.0, + "angular_y": 0.0, + "angular_z": 0.8, + }) + ); + + let stop = serde_json::to_value(WsEvent::Stop).expect("stop event should serialize"); + assert_eq!(stop, serde_json::json!({ "type": "stop" })); + } + + #[test] + fn try_recv_command_drains_inbound_queue_without_blocking() { + let (command_tx, command_rx) = mpsc::channel::(1); + command_tx + .try_send(WsCommand::OpenWebPageView { + panel_id: "viser".to_owned(), + title: "Viser".to_owned(), + url: "http://127.0.0.1:8095/".to_owned(), + show_navigation_controls: true, + }) + .expect("test command queue should have capacity"); + + let publisher = test_publisher_with_command_rx(command_rx); + + assert_eq!( + publisher.try_recv_command(), + Some(WsCommand::OpenWebPageView { + panel_id: "viser".to_owned(), + title: "Viser".to_owned(), + url: "http://127.0.0.1:8095/".to_owned(), + show_navigation_controls: true, + }) + ); + assert_eq!(publisher.try_recv_command(), None); + } + + #[test] + fn unknown_or_malformed_inbound_messages_are_rejected_by_parser() { + assert!(parse_command(r#"{"type":"unknown"}"#).is_err()); + assert!(parse_command(r#"{"type":"open_web_page_view","panel_id":"viser"}"#).is_err()); + assert!(parse_command("not json").is_err()); + } + + #[test] + fn layout_placement_fields_are_rejected_by_parser() { + assert!( + parse_command( + r#"{ + "type": "open_web_page_view", + "panel_id": "viser", + "title": "Viser", + "url": "http://127.0.0.1:8095/", + "show_navigation_controls": true, + "split_direction": "right" + }"#, + ) + .is_err() + ); + } + + #[test] + fn web_page_command_url_policy_matches_web_page_view() { + assert_eq!(validate_web_page_url("https://rerun.io"), Ok(())); + assert_eq!(validate_web_page_url("http://127.0.0.1:8095/"), Ok(())); + assert_eq!( + validate_web_page_url("file:///tmp/report.html"), + Err(WsCommandValidationError::UnsupportedUrlScheme( + "file".to_owned() + )) + ); + assert_eq!( + validate_web_page_url("javascript:alert(1)"), + Err(WsCommandValidationError::UnsupportedUrlScheme( + "javascript".to_owned() + )) + ); + assert_eq!( + validate_web_page_url("not a url"), + Err(WsCommandValidationError::InvalidUrl) + ); + } +} diff --git a/dimos/src/viewer.rs b/dimos/src/viewer.rs index 729ecbc9f694..457dbe48389c 100644 --- a/dimos/src/viewer.rs +++ b/dimos/src/viewer.rs @@ -4,11 +4,11 @@ //! - Click-to-navigate: click any entity with a 3D position → sends click event via WebSocket //! - WASD keyboard teleop: click overlay to engage, then WASD publishes Twist via WebSocket -use std::rc::Rc; use std::cell::RefCell; +use std::rc::Rc; use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH}; -use dimos_viewer::interaction::{KeyboardHandler, WsPublisher}; +use dimos_viewer::interaction::{KeyboardHandler, WsCommand, WsPublisher}; use rerun::external::{eframe, egui, re_log, re_memory, re_viewer}; #[global_allocator] @@ -28,6 +28,35 @@ const RAPID_CLICK_THRESHOLD: usize = 5; struct DimosApp { inner: re_viewer::App, keyboard: KeyboardHandler, + ws_publisher: WsPublisher, +} + +impl DimosApp { + fn handle_ws_commands(&mut self) { + while let Some(command) = self.ws_publisher.try_recv_command() { + if let Err(err) = command.validate() { + re_log::warn!("Ignoring invalid websocket command: {err}"); + continue; + } + + match command { + WsCommand::OpenWebPageView { + panel_id, + title, + url, + show_navigation_controls, + } => { + self.inner + .open_or_update_web_page_view(re_viewer::WebPageViewRequest { + panel_id, + title, + url, + show_navigation_controls, + }); + } + } + } + } } impl eframe::App for DimosApp { @@ -40,17 +69,28 @@ impl eframe::App for DimosApp { fn ui(&mut self, ui: &mut egui::Ui, frame: &mut eframe::Frame) { self.keyboard.process(ui.ctx()); - self.keyboard.draw_overlay(ui.ctx()); + let keyboard_overlay_rect = self.keyboard.draw_overlay(ui.ctx()); + self.inner + .set_web_page_overlay_clip_rect(keyboard_overlay_rect); + self.handle_ws_commands(); self.inner.ui(ui, frame); } - fn save(&mut self, storage: &mut dyn eframe::Storage) { self.inner.save(storage); } + fn save(&mut self, storage: &mut dyn eframe::Storage) { + self.inner.save(storage); + } - fn clear_color(&self, visuals: &egui::Visuals) -> [f32; 4] { self.inner.clear_color(visuals) } + fn clear_color(&self, visuals: &egui::Visuals) -> [f32; 4] { + self.inner.clear_color(visuals) + } - fn persist_egui_memory(&self) -> bool { self.inner.persist_egui_memory() } + fn persist_egui_memory(&self) -> bool { + self.inner.persist_egui_memory() + } - fn auto_save_interval(&self) -> Duration { self.inner.auto_save_interval() } + fn auto_save_interval(&self) -> Duration { + self.inner.auto_save_interval() + } fn raw_input_hook(&mut self, ctx: &egui::Context, raw_input: &mut egui::RawInput) { self.inner.raw_input_hook(ctx, raw_input); @@ -87,10 +127,9 @@ fn main() -> Result<(), Box> { } let keyboard_handler_ws = ws_publisher.clone(); + let command_ws = ws_publisher.clone(); - let last_click_time = Rc::new(RefCell::new( - Instant::now() - Duration::from_secs(10) - )); + let last_click_time = Rc::new(RefCell::new(Instant::now() - Duration::from_secs(10))); let rapid_click_count = Rc::new(RefCell::new(0usize)); // Plain click (no Ctrl required) fires nav goal on any entity with a 3D position @@ -142,7 +181,10 @@ fn main() -> Result<(), Box> { } re_log::debug!( "Click event published: entity={}, pos=({:.2}, {:.2}, {:.2})", - entity_path, pos.x, pos.y, pos.z + entity_path, + pos.x, + pos.y, + pos.z ); } re_viewer::SelectionChangeItem::Entity { position: None, .. } => { @@ -165,16 +207,26 @@ fn main() -> Result<(), Box> { if let Some(ref connect) = parsed.connect { match connect.as_deref() { Some(url) => eprintln!("[DIMOS_DEBUG] gRPC connecting to: {url}"), - None => eprintln!("[DIMOS_DEBUG] gRPC connecting to default (port {})", parsed.port), + None => eprintln!( + "[DIMOS_DEBUG] gRPC connecting to default (port {})", + parsed.port + ), } } else { - eprintln!("[DIMOS_DEBUG] gRPC: starting local server on port {}", parsed.port); + eprintln!( + "[DIMOS_DEBUG] gRPC: starting local server on port {}", + parsed.port + ); } } let wrapper: rerun::AppWrapper = Box::new(move |app| { let keyboard = KeyboardHandler::new(keyboard_handler_ws.clone()); - Ok(Box::new(DimosApp { inner: app, keyboard })) + Ok(Box::new(DimosApp { + inner: app, + keyboard, + ws_publisher: command_ws.clone(), + })) }); let exit_code = rerun::run_with_app_wrapper( diff --git a/openspec/changes/add-dimos-web-page-command/.openspec.yaml b/openspec/changes/add-dimos-web-page-command/.openspec.yaml new file mode 100644 index 000000000000..64b47c4313b2 --- /dev/null +++ b/openspec/changes/add-dimos-web-page-command/.openspec.yaml @@ -0,0 +1,2 @@ +schema: spec-driven +created: 2026-06-13 diff --git a/openspec/changes/add-dimos-web-page-command/design.md b/openspec/changes/add-dimos-web-page-command/design.md new file mode 100644 index 000000000000..35d8d8019429 --- /dev/null +++ b/openspec/changes/add-dimos-web-page-command/design.md @@ -0,0 +1,58 @@ +## Context + +The native Web Page View implementation already exposes the canonical Rerun path for creating a web panel: users send blueprint state through the generated Rerun SDK, e.g. `rr.send_blueprint(rrb.WebPageView(config=rrb.WebPageViewConfig(url=...)))`. That path is official, typed, and transported through Rerun's existing log/gRPC channel. + +DimOS also has an existing websocket control path in `dimos/src/interaction/ws.rs`. Today it is outbound from the viewer to the DimOS server for click, twist, and stop events; incoming frames are consumed only to keep the connection healthy. The DimOS viewer wrapper in `dimos/src/viewer.rs` wraps `re_viewer::App` for keyboard and selection behavior. + +This change adds a small DimOS-only inbound command lane for requesting Web Page View panels while keeping Rerun blueprint state as the source of truth. + +## Goals / Non-Goals + +**Goals:** + +- Add an experimental DimOS websocket command for opening/updating/focusing a Web Page View. +- Keep the command a thin convenience wrapper around Web Page View blueprint state. +- Keep `panel_id -> ViewId` state runtime-only inside the DimOS viewer wrapper. +- Keep the implementation localized to `dimos/` unless a narrowly-scoped core viewer API is required to apply blueprint updates cleanly. +- Make the DimOS command easy to remove if reviewers prefer the canonical Python/blueprint API only. + +**Non-Goals:** + +- Do not add new Rerun SDK schema/codegen for this DimOS command. +- Do not make the DimOS websocket a stable Rerun layout API. +- Do not expose arbitrary layout placement, split ratios, or viewport-tree editing in v1. +- Do not bypass the Web Page View URL policy or native backend lifecycle. +- Do not support old stock `rerun-sdk` Python packages as a typed Web Page View API; they can use the DimOS command as a pragmatic fallback. + +## Decisions + +1. **Canonical API remains Rerun blueprint/Python.** + - Use `rr.send_blueprint(rrb.WebPageView(...))` as the official API story. + - The DimOS websocket command is a convenience path, not a second source of truth. + - Alternative considered: make DimOS websocket the primary panel API. Rejected because it duplicates Rerun's viewer-layout channel and would be harder for Rerun reviewers to accept. + +2. **Inbound websocket command is caller-owned and idempotent by `panel_id`.** + - Command shape includes `panel_id`, `title`, `url`, and `show_navigation_controls`. + - Repeating the same `panel_id` updates/focuses the existing panel instead of creating duplicates. + - Alternative considered: viewer-generated IDs. Rejected because it would require a response/lookup protocol before callers could update or close the panel. + +3. **`panel_id -> ViewId` mapping is runtime-only.** + - The DimOS wrapper stores the mapping in memory. + - Restarting the viewer loses the mapping, which is acceptable for an experimental command. + - Alternative considered: persist `panel_id` in blueprint metadata. Rejected because it would add schema/API surface solely for a DimOS convenience wrapper. + +4. **Focus/raise on open/update if feasible.** + - Existing `ViewportBlueprint::focus_tab(view_id)` supports focusing a view's tab through `ViewportCommand::FocusTab`. + - `open_web_page_view` should make the requested panel visible to the user when possible. + +5. **Keep layout placement out of scope.** + - The command requests a logical panel, not a full remote layout edit. + - If placement is needed later, it should be designed as a separate layout-control capability. + +## Risks / Trade-offs + +- **Risk: Applying blueprint changes from `DimosApp` may require a core viewer API.** → First inspect for an existing clean path. If unavailable, add the smallest focused viewer method/command needed rather than poking internal viewport state. +- **Risk: Two apparent APIs can confuse reviewers.** → PR text must explicitly label Python/blueprint as canonical and DimOS websocket as experimental/removable. +- **Risk: Runtime-only mapping loses identity across restart.** → Accept for v1; callers can resend commands after reconnect. +- **Risk: DimOS command bypasses typed Python validation.** → Reuse or mirror the same `http`/`https` URL policy before creating/updating the panel. +- **Risk: Scope creep into layout management.** → Keep v1 to create/update/focus only. diff --git a/openspec/changes/add-dimos-web-page-command/pr-notes.md b/openspec/changes/add-dimos-web-page-command/pr-notes.md new file mode 100644 index 000000000000..ec24359954f2 --- /dev/null +++ b/openspec/changes/add-dimos-web-page-command/pr-notes.md @@ -0,0 +1,48 @@ +# PR Notes: DimOS Web Page View command + +## Canonical API + +The preferred, stable way to create a Web Page View remains the Rerun blueprint API: + +```python +import rerun as rr +import rerun.blueprint as rrb + +rr.send_blueprint( + rrb.Blueprint( + rrb.WebPageView( + name="Viser", + config=rrb.WebPageViewConfig( + url="http://127.0.0.1:8095/", + show_navigation_controls=True, + ), + ) + ) +) +``` + +## DimOS websocket command + +The DimOS websocket command is experimental, DimOS-only, and removable. It is a convenience wrapper for environments that already speak to the DimOS viewer websocket and cannot rely on a generated Rerun Python SDK from this branch. + +```json +{ + "type": "open_web_page_view", + "panel_id": "viser", + "title": "Viser", + "url": "http://127.0.0.1:8095/", + "show_navigation_controls": true +} +``` + +The command translates to normal Web Page View blueprint state. It does not mutate native webview internals directly and does not expose arbitrary viewport layout control. + +## File-scope justification + +The large generated-file blast radius belongs to the core Web Page View feature. This follow-up command stays intentionally small: + +- `dimos/src/interaction/ws.rs` parses inbound commands and preserves existing outbound event JSON. +- `dimos/src/viewer.rs` drains validated commands from the websocket queue. +- `crates/viewer/re_viewer/src/app.rs` and `crates/viewer/re_viewer/src/app_state.rs` expose and implement a tiny public wrapper API that translates a request into existing blueprint/view/focus operations. + +No new SDK schema, generated API, or native webview backend behavior is introduced by this command. diff --git a/openspec/changes/add-dimos-web-page-command/proposal.md b/openspec/changes/add-dimos-web-page-command/proposal.md new file mode 100644 index 000000000000..c6bc61e09227 --- /dev/null +++ b/openspec/changes/add-dimos-web-page-command/proposal.md @@ -0,0 +1,28 @@ +## Why + +DimOS needs a pragmatic way to request a Web Page View panel from its existing websocket control path while preserving Rerun's canonical blueprint/Python API as the source of truth for viewer layout. This lets reviewers evaluate the DimOS convenience command independently from the core native Web Page View implementation. + +## What Changes + +- Add a DimOS-only websocket command, `open_web_page_view`, that requests a Web Page View panel by caller-owned `panel_id`. +- Translate the websocket command into Web Page View blueprint state instead of directly mutating native webview internals. +- Keep a runtime-only `panel_id -> ViewId` mapping in the DimOS viewer wrapper so repeated commands update/focus the same panel. +- Keep layout placement intentionally minimal for v1: create/update/focus the logical panel, but do not expose split direction, size ratios, tab placement, or arbitrary viewport-tree control. +- Preserve the canonical API story: updated Rerun SDK users should use `rr.send_blueprint(rrb.WebPageView(...))`; the DimOS websocket command is an experimental/removable convenience path. + +## Capabilities + +### New Capabilities + +- `dimos-web-page-command`: DimOS websocket command for opening/updating/focusing native Web Page View panels. + +### Modified Capabilities + +- `native-web-page-view`: Clarifies that Web Page View remains blueprint-owned and can be requested by a DimOS websocket convenience wrapper without changing the canonical Rerun blueprint API. + +## Impact + +- Affected DimOS code: websocket protocol handling in `dimos/src/interaction/ws.rs` and viewer wrapper command handling in `dimos/src/viewer.rs`. +- Possible small helper/export changes under `dimos/src/interaction/` if needed to keep parsing and idempotency logic isolated. +- Should not require new generated Rerun SDK/schema/codegen changes, native webview backend changes, or broader viewer core changes unless blueprint application from the DimOS wrapper proves blocked. +- PR narrative should explicitly separate the existing core Web Page View implementation from this DimOS-only convenience command. diff --git a/openspec/changes/add-dimos-web-page-command/specs/dimos-web-page-command/spec.md b/openspec/changes/add-dimos-web-page-command/specs/dimos-web-page-command/spec.md new file mode 100644 index 000000000000..46a05b55d02a --- /dev/null +++ b/openspec/changes/add-dimos-web-page-command/specs/dimos-web-page-command/spec.md @@ -0,0 +1,61 @@ +## ADDED Requirements + +### Requirement: DimOS websocket can request a Web Page View + +The system SHALL accept a DimOS websocket command named `open_web_page_view` that requests a native Web Page View panel. + +#### Scenario: Command creates a web page panel + +- **WHEN** the DimOS viewer receives `open_web_page_view` with `panel_id`, `title`, `url`, and `show_navigation_controls` +- **THEN** the viewer creates a Web Page View configured with that title, URL, and navigation-control preference + +### Requirement: Web page commands are idempotent by panel identifier + +The system SHALL treat `panel_id` as a caller-owned stable identifier for DimOS web page panel commands. + +#### Scenario: Repeated command updates existing panel + +- **WHEN** the DimOS viewer receives `open_web_page_view` for a `panel_id` that already has a Web Page View in the current viewer session +- **THEN** the viewer updates that existing panel rather than creating a duplicate panel + +#### Scenario: First command creates runtime mapping + +- **WHEN** the DimOS viewer receives `open_web_page_view` for a new `panel_id` +- **THEN** the viewer records a runtime-only mapping from that `panel_id` to the created Rerun view identity + +### Requirement: Web page commands focus requested panels + +The system SHALL focus or raise the requested Web Page View panel when handling an `open_web_page_view` command. + +#### Scenario: Existing panel is focused after update + +- **WHEN** the DimOS viewer updates an existing Web Page View for an `open_web_page_view` command +- **THEN** the viewer focuses the tab containing that Web Page View when the layout supports tab focus + +#### Scenario: Newly created panel is focused + +- **WHEN** the DimOS viewer creates a Web Page View for an `open_web_page_view` command +- **THEN** the viewer focuses the newly created panel when the layout supports tab focus + +### Requirement: DimOS command scope remains minimal + +The system SHALL NOT expose arbitrary viewport tree editing through the `open_web_page_view` command. + +#### Scenario: Layout placement fields are ignored or rejected + +- **WHEN** an `open_web_page_view` command includes placement fields such as split direction, width ratio, or tab group +- **THEN** the DimOS viewer does not treat those fields as authoritative layout-edit instructions + +### Requirement: Invalid web page command URLs are rejected safely + +The system SHALL validate URLs from `open_web_page_view` commands using the Web Page View HTTP URL policy before creating or updating panels. + +#### Scenario: Unsupported scheme is rejected + +- **WHEN** the DimOS viewer receives `open_web_page_view` with a `file://` URL +- **THEN** the viewer rejects the command without creating or updating a Web Page View panel + +#### Scenario: HTTP URL is accepted + +- **WHEN** the DimOS viewer receives `open_web_page_view` with an `http://` or `https://` URL +- **THEN** the viewer may create or update the requested Web Page View panel diff --git a/openspec/changes/add-dimos-web-page-command/specs/native-web-page-view/spec.md b/openspec/changes/add-dimos-web-page-command/specs/native-web-page-view/spec.md new file mode 100644 index 000000000000..03f67c017802 --- /dev/null +++ b/openspec/changes/add-dimos-web-page-command/specs/native-web-page-view/spec.md @@ -0,0 +1,20 @@ +## MODIFIED Requirements + +### Requirement: Blueprint-owned configuration + +The system SHALL store Web Page View configuration as blueprint/view state with a required URL and a `show_navigation_controls` setting. + +#### Scenario: Blueprint preconfigures a Web Page View + +- **WHEN** blueprint state contains a Web Page View with a valid URL and navigation controls setting +- **THEN** the viewer creates the view with those configured values + +#### Scenario: Manual creation starts unconfigured + +- **WHEN** a user manually adds a Web Page View without setting a URL +- **THEN** the viewer displays a Rerun-side status explaining that no URL is configured + +#### Scenario: DimOS command translates to blueprint state + +- **WHEN** the DimOS viewer handles an `open_web_page_view` websocket command +- **THEN** the resulting Web Page View URL and navigation-control setting are represented as Web Page View blueprint configuration rather than native-webview-only state diff --git a/openspec/changes/add-dimos-web-page-command/tasks.md b/openspec/changes/add-dimos-web-page-command/tasks.md new file mode 100644 index 000000000000..efbe14f045eb --- /dev/null +++ b/openspec/changes/add-dimos-web-page-command/tasks.md @@ -0,0 +1,38 @@ +## 1. Protocol and parsing + +- [x] 1.1 Add a failing test or focused assertion for parsing an inbound `open_web_page_view` websocket command. +- [x] 1.2 Add a `WsCommand::OpenWebPageView` protocol type with `panel_id`, `title`, `url`, and `show_navigation_controls` fields. +- [x] 1.3 Keep existing outbound `WsEvent` messages (`click`, `twist`, `stop`) wire-compatible. + +## 2. Bidirectional websocket plumbing + +- [x] 2.1 Add a non-blocking incoming command queue from the websocket read loop to the DimOS viewer wrapper. +- [x] 2.2 Ensure ping/pong and reconnect behavior still works while inbound command frames are parsed. +- [x] 2.3 Log and ignore malformed or unknown inbound websocket messages without crashing the viewer. + +## 3. Blueprint command application + +- [x] 3.1 Inspect and choose the least invasive path for applying Web Page View blueprint updates from `DimosApp`. +- [x] 3.2 Add a helper that creates a Web Page View blueprint entry and saves `WebPageViewConfig` for a valid command. +- [x] 3.3 Add runtime-only `panel_id -> ViewId` tracking in `DimosApp`. +- [x] 3.4 Implement idempotent command handling: create for new `panel_id`, update existing panel for repeated `panel_id`. +- [x] 3.5 Focus the created or updated panel through existing viewport focus behavior when feasible. + +## 4. Validation and scope control + +- [x] 4.1 Validate command URLs with the same `http`/`https` policy used by Web Page View. +- [x] 4.2 Verify unsupported schemes do not create or update panels. +- [x] 4.3 Verify v1 ignores or rejects layout-placement fields rather than treating them as viewport-tree commands. +- [x] 4.4 Keep implementation localized to `dimos/`; pause for design review if core viewer APIs must be changed. + +## 5. PR formalization + +- [x] 5.1 Update PR notes or documentation text with the canonical Python/blueprint API example. +- [x] 5.2 Explicitly label the DimOS websocket command as experimental, DimOS-only, and removable. +- [x] 5.3 Document final file-scope justification: generated Web Page View blast radius belongs to the core feature; DimOS command changes stay separate and small. + +## 6. Checks + +- [x] 6.1 Run focused Rust tests for DimOS websocket command parsing/handling. +- [x] 6.2 Run relevant formatting/check commands for touched Rust files. +- [x] 6.3 Run `openspec validate "add-dimos-web-page-command" --strict --json`. diff --git a/openspec/changes/add-native-web-page-view/.openspec.yaml b/openspec/changes/archive/2026-06-12-add-native-web-page-view/.openspec.yaml similarity index 100% rename from openspec/changes/add-native-web-page-view/.openspec.yaml rename to openspec/changes/archive/2026-06-12-add-native-web-page-view/.openspec.yaml diff --git a/openspec/changes/add-native-web-page-view/design.md b/openspec/changes/archive/2026-06-12-add-native-web-page-view/design.md similarity index 100% rename from openspec/changes/add-native-web-page-view/design.md rename to openspec/changes/archive/2026-06-12-add-native-web-page-view/design.md diff --git a/openspec/changes/add-native-web-page-view/proposal.md b/openspec/changes/archive/2026-06-12-add-native-web-page-view/proposal.md similarity index 100% rename from openspec/changes/add-native-web-page-view/proposal.md rename to openspec/changes/archive/2026-06-12-add-native-web-page-view/proposal.md diff --git a/openspec/changes/add-native-web-page-view/specs/native-web-page-view/spec.md b/openspec/changes/archive/2026-06-12-add-native-web-page-view/specs/native-web-page-view/spec.md similarity index 100% rename from openspec/changes/add-native-web-page-view/specs/native-web-page-view/spec.md rename to openspec/changes/archive/2026-06-12-add-native-web-page-view/specs/native-web-page-view/spec.md diff --git a/openspec/changes/add-native-web-page-view/tasks.md b/openspec/changes/archive/2026-06-12-add-native-web-page-view/tasks.md similarity index 100% rename from openspec/changes/add-native-web-page-view/tasks.md rename to openspec/changes/archive/2026-06-12-add-native-web-page-view/tasks.md diff --git a/openspec/specs/native-web-page-view/spec.md b/openspec/specs/native-web-page-view/spec.md new file mode 100644 index 000000000000..ba978b47cec5 --- /dev/null +++ b/openspec/specs/native-web-page-view/spec.md @@ -0,0 +1,144 @@ +## Purpose + +Native Web Page View lets native Rerun viewer layouts embed configured `http(s)` webpages inline alongside other view types. + +## Requirements + +### Requirement: Configured native Web Page View + +The system SHALL provide a native-only Web Page View that displays a configured webpage inline in the viewer layout. + +#### Scenario: Native view displays configured page + +- **WHEN** a native viewer layout contains a Web Page View configured with `https://example.com` +- **THEN** the viewer displays that page inline in the view area + +#### Scenario: Web viewer reports unsupported view + +- **WHEN** the web viewer renders a layout containing a Web Page View +- **THEN** the viewer displays a clear unsupported status for that view + +### Requirement: Blueprint-owned configuration + +The system SHALL store Web Page View configuration as blueprint/view state with a required URL and a `show_navigation_controls` setting. + +#### Scenario: Blueprint preconfigures a Web Page View + +- **WHEN** blueprint state contains a Web Page View with a valid URL and navigation controls setting +- **THEN** the viewer creates the view with those configured values + +#### Scenario: Manual creation starts unconfigured + +- **WHEN** a user manually adds a Web Page View without setting a URL +- **THEN** the viewer displays a Rerun-side status explaining that no URL is configured + +### Requirement: No data-driven spawning + +The system SHALL make Web Page Views manually creatable and blueprint-preconfigurable without auto-spawning them from logged data. + +#### Scenario: Logged entities do not suggest Web Page View + +- **WHEN** the viewer computes data-driven view suggestions from logged entities +- **THEN** the suggestion set excludes Web Page View unless it was explicitly created or configured + +### Requirement: HTTP URL policy + +The system SHALL validate configured Web Page View URLs and load only `http://` and `https://` schemes. + +#### Scenario: HTTPS URL loads + +- **WHEN** a Web Page View is configured with `https://example.com` +- **THEN** the viewer attempts to load the URL in the native webview + +#### Scenario: Local HTTP URL loads + +- **WHEN** a Web Page View is configured with `http://localhost:3000` +- **THEN** the viewer attempts to load the URL in the native webview + +#### Scenario: Unsupported scheme is rejected + +- **WHEN** a Web Page View is configured with `file:///tmp/report.html` +- **THEN** the viewer displays a Rerun-side status explaining that only `http` and `https` URLs are supported + +### Requirement: Automatic loading + +The system SHALL automatically load a valid configured Web Page View URL without requiring an additional confirmation action. + +#### Scenario: Valid URL autoloads + +- **WHEN** a Web Page View becomes visible with a valid configured URL +- **THEN** the native webview starts loading that URL automatically + +### Requirement: Per-view webview instances + +The system SHALL create and manage one native webview instance for each Web Page View instance. + +#### Scenario: Multiple views render independently + +- **WHEN** a layout contains two Web Page Views with different valid URLs +- **THEN** the viewer maintains two independent native webview instances and displays both pages in their respective view areas + +### Requirement: Webview lifecycle preserves hidden view state + +The system SHALL keep a Web Page View's native webview instance alive while the Rerun view exists, including when temporarily hidden. + +#### Scenario: Hidden view keeps page state + +- **WHEN** a Web Page View is hidden behind a tab and later shown again +- **THEN** the viewer reuses the existing webview instance rather than reloading the configured URL from scratch + +#### Scenario: Removed view destroys instance + +- **WHEN** a Web Page View is removed from the layout +- **THEN** the viewer destroys the corresponding native webview instance + +### Requirement: Browser-like navigation + +The system SHALL allow runtime browser navigation inside the Web Page View while keeping the configured URL unchanged. + +#### Scenario: Link navigation does not change blueprint URL + +- **WHEN** a user clicks a link inside a Web Page View and the embedded page navigates to a different URL +- **THEN** the blueprint-configured URL remains the original configured URL + +#### Scenario: Home returns to configured URL + +- **WHEN** navigation controls are visible and the user activates Home after navigating away +- **THEN** the webview navigates back to the configured URL + +### Requirement: Optional navigation controls + +The system SHALL provide lightweight navigation controls for Web Page Views and allow them to be hidden by configuration. + +#### Scenario: Navigation controls visible by default + +- **WHEN** a Web Page View is configured without specifying `show_navigation_controls` +- **THEN** the viewer displays back, forward, reload, home, and URL display controls + +#### Scenario: Navigation controls hidden + +- **WHEN** a Web Page View is configured with `show_navigation_controls` set to false +- **THEN** the viewer hides the navigation controls and gives the webview the available view area + +### Requirement: Shared embedded browser session + +The system SHALL use a shared embedded browser session/profile for Web Page Views by default. + +#### Scenario: Session state shared across views + +- **WHEN** two Web Page Views load pages from the same origin +- **THEN** the embedded browser backend uses the shared default session/profile for both views + +### Requirement: Rerun-side failure reporting + +The system SHALL report configuration and backend failures using Rerun-side status UI outside the embedded webpage. + +#### Scenario: Backend creation fails + +- **WHEN** the native webview backend cannot create a webview instance +- **THEN** the Web Page View displays a clear Rerun-side failure message instead of crashing the viewer + +#### Scenario: Invalid URL is configured + +- **WHEN** the configured URL cannot be parsed as a URL +- **THEN** the Web Page View displays a clear Rerun-side invalid URL message From eebbe1a6a2e688965121224ae7addec31e50e583 Mon Sep 17 00:00:00 2001 From: cc Date: Sat, 13 Jun 2026 14:25:28 -0700 Subject: [PATCH 03/12] fix: ui --- .../re_view_web_page/src/native_backend.rs | 78 +------------------ .../viewer/re_view_web_page/src/view_class.rs | 1 - .../re_view_web_page/tests/web_page_view.rs | 22 ++++-- crates/viewer/re_viewer/src/app.rs | 16 ---- dimos/src/interaction/keyboard.rs | 4 +- dimos/src/viewer.rs | 4 +- 6 files changed, 20 insertions(+), 105 deletions(-) diff --git a/crates/viewer/re_view_web_page/src/native_backend.rs b/crates/viewer/re_view_web_page/src/native_backend.rs index 4aa9794901c9..d5b721d31027 100644 --- a/crates/viewer/re_view_web_page/src/native_backend.rs +++ b/crates/viewer/re_view_web_page/src/native_backend.rs @@ -12,8 +12,6 @@ thread_local! { std::cell::RefCell::new(ahash::HashMap::default()); static VISIBLE_THIS_FRAME: std::cell::RefCell> = std::cell::RefCell::new(ahash::HashSet::default()); - static OVERLAY_CLIP_BOUNDS: std::cell::Cell> = - const { std::cell::Cell::new(None) }; } scoped_tls::scoped_thread_local!(static NATIVE_PARENT_WINDOW: eframe::Frame); @@ -24,14 +22,12 @@ pub struct NativeWebViewBackend; pub struct NativeWebView { webview: wry::WebView, visible: bool, - bounds: WebViewBounds, } #[derive(Debug)] pub enum NativeWebViewError { MissingParentWindow, Wry(wry::Error), - Clip(String), } impl std::fmt::Display for NativeWebViewError { @@ -39,7 +35,6 @@ impl std::fmt::Display for NativeWebViewError { match self { Self::MissingParentWindow => f.write_str("missing parent native window"), Self::Wry(err) => write!(f, "failed to create native webview: {err}"), - Self::Clip(err) => write!(f, "failed to clip native webview: {err}"), } } } @@ -72,18 +67,11 @@ impl NativeWebViewBackend { Ok(NativeWebView { webview, visible: true, - bounds, }) }) } } -pub fn set_overlay_clip_rect(rect: egui::Rect, pixels_per_point: f32) { - OVERLAY_CLIP_BOUNDS.with(|overlay_clip_bounds| { - overlay_clip_bounds.set(Some(WebViewBounds::from_egui_rect(rect, pixels_per_point))); - }); -} - pub fn with_native_parent_window(frame: &eframe::Frame, f: impl FnOnce() -> R) -> R { NATIVE_PARENT_WINDOW.set(frame, || { begin_frame(); @@ -99,18 +87,8 @@ pub(crate) fn has_native_parent_window() -> bool { } impl NativeWebView { - pub(crate) fn set_bounds(&mut self, bounds: WebViewBounds) -> Result<(), NativeWebViewError> { - self.webview - .set_bounds(bounds.into()) - .map_err(NativeWebViewError::from)?; - self.bounds = bounds; - self.apply_overlay_clip() - } - - fn apply_overlay_clip(&self) -> Result<(), NativeWebViewError> { - let overlay_clip_bounds = - OVERLAY_CLIP_BOUNDS.with(|overlay_clip_bounds| overlay_clip_bounds.get()); - platform::apply_overlay_clip(&self.webview, self.bounds, overlay_clip_bounds) + pub(crate) fn set_bounds(&self, bounds: WebViewBounds) -> Result<(), NativeWebViewError> { + self.webview.set_bounds(bounds.into()).map_err(Into::into) } pub(crate) fn set_visible(&mut self, visible: bool) -> Result<(), NativeWebViewError> { @@ -237,11 +215,7 @@ impl From for wry::Rect { #[cfg(target_os = "linux")] mod platform { - use gtk::prelude::WidgetExt; use raw_window_handle::HasWindowHandle; - use wry::WebViewExtUnix as _; - - use super::NativeWebViewError; pub(super) fn pump_events() { if gtk::is_initialized_main_thread() { @@ -271,46 +245,6 @@ mod platform { .with_url(url) .build_as_child(parent_window) } - - pub(super) fn apply_overlay_clip( - webview: &wry::WebView, - webview_bounds: crate::backend::WebViewBounds, - overlay_bounds: Option, - ) -> Result<(), NativeWebViewError> { - let width = webview_bounds.size[0].max(1.0).round() as i32; - let height = webview_bounds.size[1].max(1.0).round() as i32; - - let region = gtk::cairo::Region::create_rectangle(>k::cairo::RectangleInt::new( - 0, 0, width, height, - )); - - if let Some(overlay_bounds) = overlay_bounds { - let left = webview_bounds.min[0].max(overlay_bounds.min[0]); - let top = webview_bounds.min[1].max(overlay_bounds.min[1]); - let right = (webview_bounds.min[0] + webview_bounds.size[0]) - .min(overlay_bounds.min[0] + overlay_bounds.size[0]); - let bottom = (webview_bounds.min[1] + webview_bounds.size[1]) - .min(overlay_bounds.min[1] + overlay_bounds.size[1]); - - if right > left && bottom > top { - region - .subtract_rectangle(>k::cairo::RectangleInt::new( - (left - webview_bounds.min[0]).round() as i32, - (top - webview_bounds.min[1]).round() as i32, - (right - left).round().max(1.0) as i32, - (bottom - top).round().max(1.0) as i32, - )) - .map_err(|err| NativeWebViewError::Clip(err.to_string()))?; - } - } - - if let Some(window) = webview.webview().window() { - window.shape_combine_region(Some(®ion), 0, 0); - window.input_shape_combine_region(®ion, 0, 0); - } - - Ok(()) - } } #[cfg(not(target_os = "linux"))] @@ -329,12 +263,4 @@ mod platform { .with_url(url) .build_as_child(parent_window) } - - pub(super) fn apply_overlay_clip( - _webview: &wry::WebView, - _webview_bounds: crate::backend::WebViewBounds, - _overlay_bounds: Option, - ) -> Result<(), super::NativeWebViewError> { - Ok(()) - } } diff --git a/crates/viewer/re_view_web_page/src/view_class.rs b/crates/viewer/re_view_web_page/src/view_class.rs index 069e627bb59b..299a659869a1 100644 --- a/crates/viewer/re_view_web_page/src/view_class.rs +++ b/crates/viewer/re_view_web_page/src/view_class.rs @@ -232,7 +232,6 @@ impl ViewClass for WebPageView { state.pending_navigation_command = navigation_command; } else { - ui.label(&url); state.pending_navigation_command = None; } diff --git a/crates/viewer/re_view_web_page/tests/web_page_view.rs b/crates/viewer/re_view_web_page/tests/web_page_view.rs index 68969c49c65b..68a5d4a61eb9 100644 --- a/crates/viewer/re_view_web_page/tests/web_page_view.rs +++ b/crates/viewer/re_view_web_page/tests/web_page_view.rs @@ -47,12 +47,6 @@ fn blueprint_configured_web_page_view_reads_url_and_navigation_preference_withou harness.run(); - assert!( - harness - .query_by_label_contains("https://example.com") - .is_some(), - "expected Web Page View to read and display the configured URL" - ); assert!( harness .query_by_label_contains("No URL configured") @@ -63,6 +57,16 @@ fn blueprint_configured_web_page_view_reads_url_and_navigation_preference_withou harness.query_by_label_contains("Back").is_none(), "expected navigation controls to be hidden when show_navigation_controls is false" ); + assert!( + harness.query_by_label_contains("Address").is_none(), + "expected address label to be hidden when show_navigation_controls is false" + ); + assert!( + harness + .query_by_label_contains("https://example.com") + .is_none(), + "expected configured URL label to be hidden when show_navigation_controls is false" + ); } #[test] @@ -160,6 +164,12 @@ fn navigation_controls_can_be_hidden() { assert!(harness.query_by_label_contains("Forward").is_none()); assert!(harness.query_by_label_contains("Reload").is_none()); assert!(harness.query_by_label_contains("Home").is_none()); + assert!(harness.query_by_label_contains("Address").is_none()); + assert!( + harness + .query_by_label_contains("https://example.com") + .is_none() + ); } #[test] diff --git a/crates/viewer/re_viewer/src/app.rs b/crates/viewer/re_viewer/src/app.rs index 49813c49aae9..82fe7201fa27 100644 --- a/crates/viewer/re_viewer/src/app.rs +++ b/crates/viewer/re_viewer/src/app.rs @@ -510,22 +510,6 @@ impl App { self.egui_ctx.request_repaint(); } - /// Clip native Web Page View surfaces around an egui overlay rectangle. - /// - /// Native webviews are OS child surfaces and can cover egui foreground overlays. This keeps the - /// overlay appearance unchanged by punching an overlay-shaped hole in the native surface where - /// supported. Platforms that do not need/support this ignore the request. - pub fn set_web_page_overlay_clip_rect(&self, rect: egui::Rect) { - #[cfg(all(not(target_arch = "wasm32"), feature = "native_webview"))] - re_view_web_page::native_backend::set_overlay_clip_rect( - rect, - self.egui_ctx.pixels_per_point(), - ); - - #[cfg(not(all(not(target_arch = "wasm32"), feature = "native_webview")))] - let _ = (self, rect); - } - pub fn set_examples_manifest_url(&mut self, url: String) { re_log::info!("Using manifest_url={url:?}"); self.state.set_examples_manifest_url(&self.egui_ctx, url); diff --git a/dimos/src/interaction/keyboard.rs b/dimos/src/interaction/keyboard.rs index c7f531d0cdac..a13a741df8a2 100644 --- a/dimos/src/interaction/keyboard.rs +++ b/dimos/src/interaction/keyboard.rs @@ -133,7 +133,7 @@ impl KeyboardHandler { /// Draw keyboard overlay HUD anchored to the bottom-right of the viewport. /// Clickable: clicking the overlay toggles engaged state. - pub fn draw_overlay(&mut self, ctx: &egui::Context) -> egui::Rect { + pub fn draw_overlay(&mut self, ctx: &egui::Context) { let area_response = egui::Area::new("dimos_keyboard_hud_br".into()) .anchor(egui::Align2::RIGHT_BOTTOM, egui::vec2(-5.0, -5.0)) .order(egui::Order::Foreground) @@ -197,8 +197,6 @@ impl KeyboardHandler { self.state.reset(); self.was_active = false; } - - area_response.interact_rect } fn draw_hud_content(&self, ui: &mut egui::Ui) { diff --git a/dimos/src/viewer.rs b/dimos/src/viewer.rs index 457dbe48389c..868c8efa30c8 100644 --- a/dimos/src/viewer.rs +++ b/dimos/src/viewer.rs @@ -69,9 +69,7 @@ impl eframe::App for DimosApp { fn ui(&mut self, ui: &mut egui::Ui, frame: &mut eframe::Frame) { self.keyboard.process(ui.ctx()); - let keyboard_overlay_rect = self.keyboard.draw_overlay(ui.ctx()); - self.inner - .set_web_page_overlay_clip_rect(keyboard_overlay_rect); + self.keyboard.draw_overlay(ui.ctx()); self.handle_ws_commands(); self.inner.ui(ui, frame); } From 6e70d1a126ffbecefb9d9152cf081ef85a68275e Mon Sep 17 00:00:00 2001 From: cc Date: Sat, 13 Jun 2026 15:01:58 -0700 Subject: [PATCH 04/12] fix: improve web page video refresh --- crates/viewer/re_view_web_page/src/native_backend.rs | 3 ++- crates/viewer/re_view_web_page/src/view_class.rs | 4 ++++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/crates/viewer/re_view_web_page/src/native_backend.rs b/crates/viewer/re_view_web_page/src/native_backend.rs index d5b721d31027..b277b95ecd85 100644 --- a/crates/viewer/re_view_web_page/src/native_backend.rs +++ b/crates/viewer/re_view_web_page/src/native_backend.rs @@ -221,7 +221,8 @@ mod platform { if gtk::is_initialized_main_thread() { // Keep WebKitGTK responsive without letting its event queue monopolize an egui frame. // Further events will be drained on subsequent frames. - for _ in 0..16 { + let deadline = std::time::Instant::now() + std::time::Duration::from_millis(4); + while std::time::Instant::now() < deadline { if !gtk::events_pending() { break; } diff --git a/crates/viewer/re_view_web_page/src/view_class.rs b/crates/viewer/re_view_web_page/src/view_class.rs index 299a659869a1..ec111d7c0088 100644 --- a/crates/viewer/re_view_web_page/src/view_class.rs +++ b/crates/viewer/re_view_web_page/src/view_class.rs @@ -247,6 +247,10 @@ impl ViewClass for WebPageView { WebViewLifecycleStatus::Ready => { state.lifecycle.update_bounds(query.view_id, webview_bounds); state.lifecycle.set_visible(true); + if !ctx.app_ctx.is_test { + ui.ctx() + .request_repaint_after(std::time::Duration::from_millis(16)); + } match state.pending_navigation_command.take() { Some(NavigationCommand::Back) => state.lifecycle.go_back(), Some(NavigationCommand::Forward) => state.lifecycle.go_forward(), From 3c2c1f7063bd53dd697bc9d3eb6adeca6edef7b7 Mon Sep 17 00:00:00 2001 From: cc Date: Sat, 13 Jun 2026 15:10:14 -0700 Subject: [PATCH 05/12] test: add web page view smoke scripts --- scripts/web_page_view_smoke/README.md | 46 ++++++++++ scripts/web_page_view_smoke/launch_viewer.sh | 40 +++++++++ .../serve_dimos_ws_command.py | 90 +++++++++++++++++++ scripts/web_page_view_smoke/show_two_pages.sh | 39 ++++++++ scripts/web_page_view_smoke/show_youtube.sh | 30 +++++++ 5 files changed, 245 insertions(+) create mode 100644 scripts/web_page_view_smoke/README.md create mode 100755 scripts/web_page_view_smoke/launch_viewer.sh create mode 100755 scripts/web_page_view_smoke/serve_dimos_ws_command.py create mode 100755 scripts/web_page_view_smoke/show_two_pages.sh create mode 100755 scripts/web_page_view_smoke/show_youtube.sh diff --git a/scripts/web_page_view_smoke/README.md b/scripts/web_page_view_smoke/README.md new file mode 100644 index 000000000000..3cf1db203520 --- /dev/null +++ b/scripts/web_page_view_smoke/README.md @@ -0,0 +1,46 @@ +# Web Page View smoke scripts + +These scripts manually smoke-test the experimental native Web Page View. + +## Launch the native viewer + +```bash +scripts/web_page_view_smoke/launch_viewer.sh +``` + +The script forces the native X11 path on Linux because the current `wry` backend uses child-window embedding. + +## Send two side-by-side pages through the Python blueprint API + +```bash +scripts/web_page_view_smoke/show_two_pages.sh +``` + +Expected result: + +- left panel: `https://rerun.io` with Web Page View controls visible +- right panel: `https://example.com` with Web Page View controls hidden + +## Send a YouTube page through the Python blueprint API + +```bash +scripts/web_page_view_smoke/show_youtube.sh +``` + +Linux video playback depends on WebKitGTK/GStreamer codecs. If YouTube says the browser cannot play the video, install the system GStreamer codec plugins, then relaunch the viewer. + +## Test the DimOS websocket command path + +Start the one-shot command server in one terminal: + +```bash +python3 scripts/web_page_view_smoke/serve_dimos_ws_command.py +``` + +Launch the viewer with the websocket URL in another terminal: + +```bash +scripts/web_page_view_smoke/launch_viewer.sh --ws-url ws://127.0.0.1:3032/ws +``` + +Expected result: the DimOS websocket command creates two Web Page View panels using the same blueprint-backed Web Page View state as the Python API path. diff --git a/scripts/web_page_view_smoke/launch_viewer.sh b/scripts/web_page_view_smoke/launch_viewer.sh new file mode 100755 index 000000000000..17ba3d6a70c8 --- /dev/null +++ b/scripts/web_page_view_smoke/launch_viewer.sh @@ -0,0 +1,40 @@ +#!/usr/bin/env bash +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +REPO_ROOT="$(cd "${SCRIPT_DIR}/../.." && pwd)" +LOG_PATH="${WEB_PAGE_VIEW_SMOKE_LOG:-/tmp/rerun_web_page_view_smoke.log}" + +cd "${REPO_ROOT}" + +pkill -x dimos-viewer 2>/dev/null || true +pkill -x rerun 2>/dev/null || true + +nohup env -u WAYLAND_DISPLAY \ + WINIT_UNIX_BACKEND=x11 \ + cargo run -p dimos-viewer \ + --features rerun/native_webview \ + -- \ + --new \ + "$@" \ + > "${LOG_PATH}" 2>&1 < /dev/null & + +python3 - <<'PY' +import socket +import time + +for _ in range(240): + sock = socket.socket() + try: + sock.connect(("127.0.0.1", 9876)) + print("viewer ready: rerun+http://127.0.0.1:9876/proxy") + break + except OSError: + time.sleep(0.5) + finally: + sock.close() +else: + raise SystemExit("viewer did not open gRPC port 9876") +PY + +echo "viewer log: ${LOG_PATH}" diff --git a/scripts/web_page_view_smoke/serve_dimos_ws_command.py b/scripts/web_page_view_smoke/serve_dimos_ws_command.py new file mode 100755 index 000000000000..7cabeeba38e6 --- /dev/null +++ b/scripts/web_page_view_smoke/serve_dimos_ws_command.py @@ -0,0 +1,90 @@ +#!/usr/bin/env python3 +"""One-shot websocket command server for the DimOS Web Page View smoke test.""" + +from __future__ import annotations + +import base64 +import hashlib +import json +import socket +import struct +import time + + +HOST = "127.0.0.1" +PORT = 3032 +WEBSOCKET_GUID = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11" + + +COMMANDS = [ + { + "type": "open_web_page_view", + "panel_id": "webpage-rerun", + "title": "Rerun — DimOS WS", + "url": "https://rerun.io", + "show_navigation_controls": True, + }, + { + "type": "open_web_page_view", + "panel_id": "webpage-example", + "title": "Example — DimOS WS no controls", + "url": "https://example.com", + "show_navigation_controls": False, + }, +] + + +def websocket_accept_key(headers: str) -> str: + for line in headers.splitlines(): + if line.lower().startswith("sec-websocket-key:"): + key = line.split(":", 1)[1].strip() + digest = hashlib.sha1((key + WEBSOCKET_GUID).encode("ascii")).digest() + return base64.b64encode(digest).decode("ascii") + raise RuntimeError("missing Sec-WebSocket-Key") + + +def send_text_frame(conn: socket.socket, text: str) -> None: + payload = text.encode("utf-8") + header = bytearray([0x81]) + if len(payload) < 126: + header.append(len(payload)) + elif len(payload) < 65536: + header.extend([126]) + header.extend(struct.pack("!H", len(payload))) + else: + header.extend([127]) + header.extend(struct.pack("!Q", len(payload))) + conn.sendall(bytes(header) + payload) + + +def main() -> None: + with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as server: + server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + server.bind((HOST, PORT)) + server.listen(1) + print(f"listening on ws://{HOST}:{PORT}/ws") + + conn, addr = server.accept() + with conn: + request = conn.recv(4096).decode("utf-8", errors="replace") + accept_key = websocket_accept_key(request) + response = ( + "HTTP/1.1 101 Switching Protocols\r\n" + "Upgrade: websocket\r\n" + "Connection: Upgrade\r\n" + f"Sec-WebSocket-Accept: {accept_key}\r\n" + "\r\n" + ) + conn.sendall(response.encode("ascii")) + print(f"client connected from {addr[0]}:{addr[1]}") + + for command in COMMANDS: + send_text_frame(conn, json.dumps(command)) + print(f"sent command: {command['panel_id']}") + time.sleep(0.25) + + time.sleep(2.0) + + +if __name__ == "__main__": + main() diff --git a/scripts/web_page_view_smoke/show_two_pages.sh b/scripts/web_page_view_smoke/show_two_pages.sh new file mode 100755 index 000000000000..532dec5dd8f0 --- /dev/null +++ b/scripts/web_page_view_smoke/show_two_pages.sh @@ -0,0 +1,39 @@ +#!/usr/bin/env bash +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +REPO_ROOT="$(cd "${SCRIPT_DIR}/../.." && pwd)" + +cd "${REPO_ROOT}" + +pixi run uvpy -c ' +import time +import rerun as rr +import rerun.blueprint as rrb + +rr.init("web_page_manual_smoke") +rr.connect_grpc("rerun+http://127.0.0.1:9876/proxy") +rr.send_blueprint( + rrb.Blueprint( + rrb.Horizontal( + rrb.WebPageView( + name="Rerun — with controls", + config=rrb.WebPageViewConfig( + url="https://rerun.io", + show_navigation_controls=True, + ), + ), + rrb.WebPageView( + name="Example — no controls", + config=rrb.WebPageViewConfig( + url="https://example.com", + show_navigation_controls=False, + ), + ), + ), + collapse_panels=True, + ) +) +time.sleep(2) +print("sent two side-by-side Web Page Views") +' diff --git a/scripts/web_page_view_smoke/show_youtube.sh b/scripts/web_page_view_smoke/show_youtube.sh new file mode 100755 index 000000000000..56b6b856ba15 --- /dev/null +++ b/scripts/web_page_view_smoke/show_youtube.sh @@ -0,0 +1,30 @@ +#!/usr/bin/env bash +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +REPO_ROOT="$(cd "${SCRIPT_DIR}/../.." && pwd)" + +cd "${REPO_ROOT}" + +pixi run uvpy -c ' +import time +import rerun as rr +import rerun.blueprint as rrb + +rr.init("web_page_youtube_smoke") +rr.connect_grpc("rerun+http://127.0.0.1:9876/proxy") +rr.send_blueprint( + rrb.Blueprint( + rrb.WebPageView( + name="YouTube", + config=rrb.WebPageViewConfig( + url="https://www.youtube.com/watch?v=aqz-KE-bpKQ", + show_navigation_controls=True, + ), + ), + collapse_panels=True, + ) +) +time.sleep(2) +print("sent YouTube smoke page") +' From 065b340d48f2a6aee2eced8cf4cec4db854f794e Mon Sep 17 00:00:00 2001 From: Jeff Hykin Date: Sun, 14 Jun 2026 11:02:11 +0800 Subject: [PATCH 06/12] is at least rendering on macos now --- crates/viewer/re_view_web_page/Cargo.toml | 7 ++++++- crates/viewer/re_view_web_page/src/native_backend.rs | 8 ++++++-- crates/viewer/re_viewer/src/app_state.rs | 6 ++++++ 3 files changed, 18 insertions(+), 3 deletions(-) diff --git a/crates/viewer/re_view_web_page/Cargo.toml b/crates/viewer/re_view_web_page/Cargo.toml index de258cd4871c..277fdd092ca2 100644 --- a/crates/viewer/re_view_web_page/Cargo.toml +++ b/crates/viewer/re_view_web_page/Cargo.toml @@ -31,7 +31,6 @@ native_webview = [ [dependencies] ahash.workspace = true egui.workspace = true -gtk = { workspace = true, optional = true } parking_lot.workspace = true re_log_types.workspace = true re_sdk_types.workspace = true @@ -50,6 +49,12 @@ wry = { workspace = true, optional = true, default-features = false, features = "x11", ] } +# GTK/WebKitGTK is only used on Linux (see the `cfg(target_os = "linux")` platform +# module in `native_backend.rs`). Gating it here keeps the macOS/Windows builds from +# pulling in GTK system libraries. +[target.'cfg(target_os = "linux")'.dependencies] +gtk = { workspace = true, optional = true } + [dev-dependencies] egui_kittest.workspace = true re_test_context.workspace = true diff --git a/crates/viewer/re_view_web_page/src/native_backend.rs b/crates/viewer/re_view_web_page/src/native_backend.rs index b277b95ecd85..82a4cc52da95 100644 --- a/crates/viewer/re_view_web_page/src/native_backend.rs +++ b/crates/viewer/re_view_web_page/src/native_backend.rs @@ -206,9 +206,13 @@ impl From for wry::Rect { let width = bounds.size[0].max(1.0).round() as u32; let height = bounds.size[1].max(1.0).round() as u32; + // `WebViewBounds` is already in physical pixels (`from_egui_rect` multiplies the + // egui rect by `pixels_per_point`), so it must be handed to wry as a *physical* + // rect. Passing it as logical double-counts the scale factor on HiDPI/Retina + // displays, shifting and oversizing the webview off its tile. Self { - position: wry::dpi::LogicalPosition::new(min_x, min_y).into(), - size: wry::dpi::LogicalSize::new(width, height).into(), + position: wry::dpi::PhysicalPosition::new(min_x, min_y).into(), + size: wry::dpi::PhysicalSize::new(width, height).into(), } } } diff --git a/crates/viewer/re_viewer/src/app_state.rs b/crates/viewer/re_viewer/src/app_state.rs index f085a230085c..22ba89e1dd51 100644 --- a/crates/viewer/re_viewer/src/app_state.rs +++ b/crates/viewer/re_viewer/src/app_state.rs @@ -1070,6 +1070,12 @@ fn apply_pending_web_page_view_requests( view.display_name = Some(request.title.clone()); let view_id = viewport_blueprint.add_view_at_root(view); + + // Adding a view via the DimOS command counts as a user edit: otherwise + // auto-layout heuristics re-derive the layout from logged data every frame + // and drop the (data-less) Web Page View before it can be shown. + viewport_blueprint.mark_user_interaction(ctx); + panel_ids.insert(request.panel_id.clone(), view_id); view_id }); From b96b01679854548e83387b8672f9ad4490f66c94 Mon Sep 17 00:00:00 2001 From: Jeff Hykin Date: Sun, 14 Jun 2026 11:14:38 +0800 Subject: [PATCH 07/12] feat: macOS Edit menu for webview copy/paste + keep Linux on logical webview bounds - Install a minimal macOS Edit menu (Cut/Copy/Paste/Select All) so the native Web Page View's WKWebView receives cmd+C/cmd+V via the responder chain. eframe/winit doesn't install one, so the shortcuts never reached the webview. - Gate the webview bounds coordinate space: physical on macOS/Windows (fixes the Retina double-scaling that pushed the page off its tile), logical on Linux to leave the author's tested X11 child-window path unchanged. --- Cargo.lock | 1 + .../re_view_web_page/src/native_backend.rs | 30 +++++-- dimos/Cargo.toml | 5 ++ dimos/src/lib.rs | 1 + dimos/src/macos_menu.rs | 81 +++++++++++++++++++ dimos/src/viewer.rs | 3 + 6 files changed, 115 insertions(+), 6 deletions(-) create mode 100644 dimos/src/macos_menu.rs diff --git a/Cargo.lock b/Cargo.lock index dd3d20185f39..9f6270baca56 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3166,6 +3166,7 @@ dependencies = [ "clap", "futures-util", "mimalloc", + "objc2 0.6.4", "parking_lot", "rerun", "serde", diff --git a/crates/viewer/re_view_web_page/src/native_backend.rs b/crates/viewer/re_view_web_page/src/native_backend.rs index 82a4cc52da95..1423580ca4ef 100644 --- a/crates/viewer/re_view_web_page/src/native_backend.rs +++ b/crates/viewer/re_view_web_page/src/native_backend.rs @@ -207,12 +207,30 @@ impl From for wry::Rect { let height = bounds.size[1].max(1.0).round() as u32; // `WebViewBounds` is already in physical pixels (`from_egui_rect` multiplies the - // egui rect by `pixels_per_point`), so it must be handed to wry as a *physical* - // rect. Passing it as logical double-counts the scale factor on HiDPI/Retina - // displays, shifting and oversizing the webview off its tile. - Self { - position: wry::dpi::PhysicalPosition::new(min_x, min_y).into(), - size: wry::dpi::PhysicalSize::new(width, height).into(), + // egui rect by `pixels_per_point`). + // + // On macOS/Windows the webview is a direct native child surface positioned in + // physical pixels, so it must be handed to wry as a *physical* rect — passing it + // as logical double-counts the scale factor on Retina/HiDPI displays, shifting and + // oversizing the webview off its tile. + // + // On Linux the X11 child-window path is positioned in GTK logical coordinates and + // only the scale-factor-1.0 case is supported (Wayland/HiDPI needs the GTK + // container path — see the `platform` module). Keep the original logical behavior + // there so the supported X11 path is unchanged. + #[cfg(target_os = "linux")] + { + Self { + position: wry::dpi::LogicalPosition::new(min_x, min_y).into(), + size: wry::dpi::LogicalSize::new(width, height).into(), + } + } + #[cfg(not(target_os = "linux"))] + { + Self { + position: wry::dpi::PhysicalPosition::new(min_x, min_y).into(), + size: wry::dpi::PhysicalSize::new(width, height).into(), + } } } } diff --git a/dimos/Cargo.toml b/dimos/Cargo.toml index 2e6bd33cf613..c24ee3b539a3 100644 --- a/dimos/Cargo.toml +++ b/dimos/Cargo.toml @@ -45,5 +45,10 @@ tokio = { workspace = true, features = [ tokio-tungstenite = "0.28.0" url.workspace = true +# Used to install a macOS Edit menu so the native Web Page View can receive +# cmd+C / cmd+V via the responder chain (see `macos_menu.rs`). +[target.'cfg(target_os = "macos")'.dependencies] +objc2 = "0.6" + [lints] workspace = true diff --git a/dimos/src/lib.rs b/dimos/src/lib.rs index a397390f3710..179cc9bceee4 100644 --- a/dimos/src/lib.rs +++ b/dimos/src/lib.rs @@ -1 +1,2 @@ pub mod interaction; +pub mod macos_menu; diff --git a/dimos/src/macos_menu.rs b/dimos/src/macos_menu.rs new file mode 100644 index 000000000000..0905f479b892 --- /dev/null +++ b/dimos/src/macos_menu.rs @@ -0,0 +1,81 @@ +//! macOS application menu setup. +//! +//! eframe/winit apps don't install an **Edit** menu, so the standard +//! Cut/Copy/Paste/Select-All key equivalents are never delivered down the +//! responder chain. That doesn't matter for pure-egui content (egui handles its +//! own clipboard), but the native Web Page View embeds a `WKWebView` child whose +//! copy/paste rely on macOS routing `cmd+C`/`cmd+V` to it via `performKeyEquivalent:`. +//! +//! Installing an Edit menu whose items target `nil` (the default) makes the +//! responder chain deliver `copy:`/`paste:`/etc. to whichever view is first +//! responder — i.e. the focused webview — so selecting text and pressing +//! `cmd+C` works. +#![allow(unsafe_code)] // Thin Objective-C bridge to build an NSMenu; see module docs. + +#[cfg(target_os = "macos")] +pub fn install_edit_menu() { + use std::sync::Once; + static ONCE: Once = Once::new(); + ONCE.call_once(|| unsafe { install_edit_menu_impl() }); +} + +#[cfg(not(target_os = "macos"))] +pub fn install_edit_menu() {} + +#[cfg(target_os = "macos")] +unsafe fn install_edit_menu_impl() { + use objc2::runtime::{AnyObject, Sel}; + use objc2::{class, msg_send, sel}; + use std::ffi::CString; + + // All objects below are added to the app's main menu, which lives for the whole + // process, so the menu retains them; we intentionally don't balance the +1 from + // alloc/init. NSStrings are autoreleased and only need to outlive the calls here. + unsafe fn ns_string(s: &str) -> *mut AnyObject { + let cstr = CString::new(s).unwrap_or_default(); + unsafe { msg_send![class!(NSString), stringWithUTF8String: cstr.as_ptr()] } + } + + unsafe fn add_item(menu: *mut AnyObject, title: &str, action: Sel, key: &str) { + unsafe { + let alloc: *mut AnyObject = msg_send![class!(NSMenuItem), alloc]; + let item: *mut AnyObject = msg_send![ + alloc, + initWithTitle: ns_string(title), + action: action, + keyEquivalent: ns_string(key) + ]; + let _: () = msg_send![menu, addItem: item]; + } + } + + unsafe { + // The app (and main thread) already exist by the time the first frame runs. + let app: *mut AnyObject = msg_send![class!(NSApplication), sharedApplication]; + if app.is_null() { + return; + } + + let mut main_menu: *mut AnyObject = msg_send![app, mainMenu]; + if main_menu.is_null() { + let alloc: *mut AnyObject = msg_send![class!(NSMenu), alloc]; + main_menu = msg_send![alloc, init]; + let _: () = msg_send![app, setMainMenu: main_menu]; + } + + let edit_alloc: *mut AnyObject = msg_send![class!(NSMenu), alloc]; + let edit_menu: *mut AnyObject = msg_send![edit_alloc, initWithTitle: ns_string("Edit")]; + + add_item(edit_menu, "Undo", sel!(undo:), "z"); + add_item(edit_menu, "Redo", sel!(redo:), "Z"); + add_item(edit_menu, "Cut", sel!(cut:), "x"); + add_item(edit_menu, "Copy", sel!(copy:), "c"); + add_item(edit_menu, "Paste", sel!(paste:), "v"); + add_item(edit_menu, "Select All", sel!(selectAll:), "a"); + + let item_alloc: *mut AnyObject = msg_send![class!(NSMenuItem), alloc]; + let edit_item: *mut AnyObject = msg_send![item_alloc, init]; + let _: () = msg_send![edit_item, setSubmenu: edit_menu]; + let _: () = msg_send![main_menu, addItem: edit_item]; + } +} diff --git a/dimos/src/viewer.rs b/dimos/src/viewer.rs index 868c8efa30c8..9bf1d17f34bf 100644 --- a/dimos/src/viewer.rs +++ b/dimos/src/viewer.rs @@ -64,6 +64,9 @@ impl eframe::App for DimosApp { /// re_viewer::App drains log_receivers / ingests messages here, so we MUST /// forward — otherwise the viewer's data pipeline stalls. fn logic(&mut self, ctx: &egui::Context, frame: &mut eframe::Frame) { + // Idempotent; installs the macOS Edit menu on the first frame so that the + // native Web Page View can receive cmd+C / cmd+V via the responder chain. + dimos_viewer::macos_menu::install_edit_menu(); self.inner.logic(ctx, frame); } From 4d3fbfbaee2fa5bbc2a946d181fe026481600176 Mon Sep 17 00:00:00 2001 From: cc Date: Mon, 15 Jun 2026 21:57:36 -0700 Subject: [PATCH 08/12] fix: address web page CI lints --- ARCHITECTURE.md | 1 + crates/viewer/re_view_web_page/Cargo.toml | 8 +-- crates/viewer/re_viewer/src/app.rs | 3 + dimos/pyproject.toml | 22 +++---- dimos/src/interaction/handle.rs | 4 +- dimos/src/interaction/keyboard.rs | 23 ++++---- dimos/src/interaction/ws.rs | 7 ++- docs/websockets.md | 2 +- .../add-dimos-web-page-command/design.md | 4 +- .../add-dimos-web-page-command/pr-notes.md | 2 +- .../add-dimos-web-page-command/proposal.md | 6 +- .../specs/dimos-web-page-command/spec.md | 24 ++++---- .../specs/native-web-page-view/spec.md | 8 +-- .../add-dimos-web-page-command/tasks.md | 2 +- .../design.md | 4 +- .../proposal.md | 6 +- .../specs/native-web-page-view/spec.md | 58 +++++++++---------- .../tasks.md | 18 +++--- openspec/specs/native-web-page-view/spec.md | 56 +++++++++--------- scripts/web_page_view_smoke/README.md | 2 +- .../serve_dimos_ws_command.py | 1 - scripts/web_page_view_smoke/show_two_pages.sh | 2 +- scripts/web_page_view_smoke/show_youtube.sh | 2 +- 23 files changed, 133 insertions(+), 132 deletions(-) diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index 326cba7e2779..23867f59cef1 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -158,6 +158,7 @@ Update instructions: | re_view_text_document | A simple View that shows a single text box. | | re_view_text_log | A View that shows text entries in a table and scrolls with the active time. | | re_view_time_series | A View that shows plots over Rerun timelines. | +| re_view_web_page | A native-only experimental View that embeds a configured web page. | | re_viewer | The Rerun Viewer | | re_viewport | The central viewport panel of the Rerun viewer. | diff --git a/crates/viewer/re_view_web_page/Cargo.toml b/crates/viewer/re_view_web_page/Cargo.toml index de258cd4871c..dcb80c9ea310 100644 --- a/crates/viewer/re_view_web_page/Cargo.toml +++ b/crates/viewer/re_view_web_page/Cargo.toml @@ -20,13 +20,7 @@ all-features = true [features] default = [] -native_webview = [ - "dep:eframe", - "dep:gtk", - "dep:raw-window-handle", - "dep:scoped-tls", - "dep:wry", -] +native_webview = ["dep:eframe", "dep:gtk", "dep:raw-window-handle", "dep:scoped-tls", "dep:wry"] [dependencies] ahash.workspace = true diff --git a/crates/viewer/re_viewer/src/app.rs b/crates/viewer/re_viewer/src/app.rs index 82fe7201fa27..5cccccb4068b 100644 --- a/crates/viewer/re_viewer/src/app.rs +++ b/crates/viewer/re_viewer/src/app.rs @@ -163,10 +163,13 @@ pub struct App { pub struct WebPageViewRequest { /// Caller-owned stable identifier used to update the same panel later. pub panel_id: String, + /// Human-readable panel title. pub title: String, + /// Configured page URL. pub url: String, + /// Whether browser-like controls should be visible. pub show_navigation_controls: bool, } diff --git a/dimos/pyproject.toml b/dimos/pyproject.toml index 81fa2e3e4d4f..cec8ca1413c2 100644 --- a/dimos/pyproject.toml +++ b/dimos/pyproject.toml @@ -8,18 +8,18 @@ version = "0.32.0a1" description = "Interactive Rerun viewer for DimOS with click-to-navigate support" readme = "README.md" requires-python = ">=3.10" -license = {text = "MIT OR Apache-2.0"} -authors = [{name = "Dimensional Inc.", email = "engineering@dimensional.com"}] +license = { text = "MIT OR Apache-2.0" } +authors = [{ name = "Dimensional Inc.", email = "engineering@dimensional.com" }] classifiers = [ - "Development Status :: 4 - Beta", - "Intended Audience :: Developers", - "Topic :: Scientific/Engineering", - "Topic :: Scientific/Engineering :: Visualization", - "Programming Language :: Rust", - "License :: OSI Approved :: MIT License", - "License :: OSI Approved :: Apache Software License", - "Operating System :: POSIX :: Linux", - "Operating System :: MacOS", + "Development Status :: 4 - Beta", + "Intended Audience :: Developers", + "Topic :: Scientific/Engineering", + "Topic :: Scientific/Engineering :: Visualization", + "Programming Language :: Rust", + "License :: OSI Approved :: MIT License", + "License :: OSI Approved :: Apache Software License", + "Operating System :: POSIX :: Linux", + "Operating System :: MacOS", ] [project.urls] diff --git a/dimos/src/interaction/handle.rs b/dimos/src/interaction/handle.rs index b29fd1ea41a0..8b986c9d1e8d 100644 --- a/dimos/src/interaction/handle.rs +++ b/dimos/src/interaction/handle.rs @@ -34,8 +34,8 @@ impl InteractionHandle { is_2d, }; - if let Err(e) = self.tx.send(event) { - eprintln!("Failed to send click event: {}", e); + if let Err(err) = self.tx.send(event) { + eprintln!("Failed to send click event: {err}"); } } } diff --git a/dimos/src/interaction/keyboard.rs b/dimos/src/interaction/keyboard.rs index a13a741df8a2..a72f95942e6e 100644 --- a/dimos/src/interaction/keyboard.rs +++ b/dimos/src/interaction/keyboard.rs @@ -93,8 +93,8 @@ impl KeyboardHandler { // If not engaged, don't capture any keys if !self.engaged { if self.was_active { - if let Err(e) = self.publish_stop() { - re_log::warn!("Failed to send stop on disengage: {e}"); + if let Err(err) = self.publish_stop() { + re_log::warn!("Failed to send stop on disengage: {err}"); } self.was_active = false; } @@ -107,8 +107,8 @@ impl KeyboardHandler { // Check for emergency stop (Space key pressed - one-shot action) if ctx.input(|i| i.key_pressed(egui::Key::Space)) { self.state.reset(); - if let Err(e) = self.publish_stop() { - re_log::warn!("Failed to send emergency stop: {e}"); + if let Err(err) = self.publish_stop() { + re_log::warn!("Failed to send emergency stop: {err}"); } self.was_active = false; self.estop_flash = true; @@ -117,13 +117,13 @@ impl KeyboardHandler { // Publish twist command if keys are active, or stop if just released if self.state.any_active() { - if let Err(e) = self.publish_twist() { - re_log::warn!("Failed to publish twist command: {e}"); + if let Err(err) = self.publish_twist() { + re_log::warn!("Failed to publish twist command: {err}"); } self.was_active = true; } else if self.was_active { - if let Err(e) = self.publish_stop() { - re_log::warn!("Failed to send stop on key release: {e}"); + if let Err(err) = self.publish_stop() { + re_log::warn!("Failed to send stop on key release: {err}"); } self.was_active = false; } @@ -314,22 +314,21 @@ impl KeyboardHandler { } /// Convert current KeyState to Twist and publish via WebSocket. - fn publish_twist(&mut self) -> Result<(), super::ws::SendError> { + fn publish_twist(&self) -> Result<(), super::ws::SendError> { let (lin_x, lin_y, lin_z, ang_x, ang_y, ang_z) = self.compute_twist(); self.ws .send_twist(lin_x, lin_y, lin_z, ang_x, ang_y, ang_z)?; if std::env::var("DIMOS_DEBUG").is_ok_and(|v| v == "1") { eprintln!( - "[DIMOS_DEBUG] Published twist: lin=({:.2},{:.2},{:.2}) ang=({:.2},{:.2},{:.2})", - lin_x, lin_y, lin_z, ang_x, ang_y, ang_z + "[DIMOS_DEBUG] Published twist: lin=({lin_x:.2},{lin_y:.2},{lin_z:.2}) ang=({ang_x:.2},{ang_y:.2},{ang_z:.2})" ); } Ok(()) } /// Publish all-zero twist (stop command) via WebSocket. - fn publish_stop(&mut self) -> Result<(), super::ws::SendError> { + fn publish_stop(&self) -> Result<(), super::ws::SendError> { self.ws.send_stop()?; if std::env::var("DIMOS_DEBUG").is_ok_and(|v| v == "1") { eprintln!("[DIMOS_DEBUG] Published stop command"); diff --git a/dimos/src/interaction/ws.rs b/dimos/src/interaction/ws.rs index caee89539798..002ea3544868 100644 --- a/dimos/src/interaction/ws.rs +++ b/dimos/src/interaction/ws.rs @@ -24,6 +24,7 @@ use tokio::sync::mpsc; pub enum SendError { /// The send queue is full; the event was dropped. QueueFull, + /// Failed to serialize the event to JSON. Serialize(String), } @@ -67,10 +68,13 @@ pub enum WsCommand { OpenWebPageView { /// Caller-owned stable identifier used to update the same panel later. panel_id: String, + /// Human-readable panel title. title: String, + /// Configured page URL. url: String, + /// Whether browser-like controls should be visible. show_navigation_controls: bool, }, @@ -81,6 +85,7 @@ pub enum WsCommand { pub enum WsCommandValidationError { /// URL text could not be parsed as an absolute URL. InvalidUrl, + /// URL uses a scheme that Web Page View does not allow. UnsupportedUrlScheme(String), } @@ -218,7 +223,7 @@ impl WsPublisher { fn broadcast(&self, event: &WsEvent) -> Result<(), SendError> { let json = - serde_json::to_string(&event).map_err(|e| SendError::Serialize(e.to_string()))?; + serde_json::to_string(&event).map_err(|err| SendError::Serialize(err.to_string()))?; // Non-blocking: error if the channel is full rather than block the UI thread. self.tx.try_send(json).map_err(|err| { let _err = err; diff --git a/docs/websockets.md b/docs/websockets.md index ee0c5e7f5edc..840a76e1ff3e 100644 --- a/docs/websockets.md +++ b/docs/websockets.md @@ -1,4 +1,4 @@ -# dimos-viewer WebSocket Event Stream +# Dimos-viewer WebSocket event stream When `dimos-viewer` is started with `--connect`, LCM multicast is not available (LCM uses UDP multicast which is limited to the local machine or subnet). Instead, diff --git a/openspec/changes/add-dimos-web-page-command/design.md b/openspec/changes/add-dimos-web-page-command/design.md index 35d8d8019429..dc958de9add0 100644 --- a/openspec/changes/add-dimos-web-page-command/design.md +++ b/openspec/changes/add-dimos-web-page-command/design.md @@ -1,6 +1,6 @@ ## Context -The native Web Page View implementation already exposes the canonical Rerun path for creating a web panel: users send blueprint state through the generated Rerun SDK, e.g. `rr.send_blueprint(rrb.WebPageView(config=rrb.WebPageViewConfig(url=...)))`. That path is official, typed, and transported through Rerun's existing log/gRPC channel. +The native Web Page View implementation already exposes the canonical Rerun path for creating a web panel: users send blueprint state through the generated Rerun SDK, e.g. `rr.send_blueprint(rrb.WebPageView(config=rrb.WebPageViewConfig(url=…)))`. That path is official, typed, and transported through Rerun's existing log/gRPC channel. DimOS also has an existing websocket control path in `dimos/src/interaction/ws.rs`. Today it is outbound from the viewer to the DimOS server for click, twist, and stop events; incoming frames are consumed only to keep the connection healthy. The DimOS viewer wrapper in `dimos/src/viewer.rs` wraps `re_viewer::App` for keyboard and selection behavior. @@ -49,7 +49,7 @@ This change adds a small DimOS-only inbound command lane for requesting Web Page - The command requests a logical panel, not a full remote layout edit. - If placement is needed later, it should be designed as a separate layout-control capability. -## Risks / Trade-offs +## Risks / trade-offs - **Risk: Applying blueprint changes from `DimosApp` may require a core viewer API.** → First inspect for an existing clean path. If unavailable, add the smallest focused viewer method/command needed rather than poking internal viewport state. - **Risk: Two apparent APIs can confuse reviewers.** → PR text must explicitly label Python/blueprint as canonical and DimOS websocket as experimental/removable. diff --git a/openspec/changes/add-dimos-web-page-command/pr-notes.md b/openspec/changes/add-dimos-web-page-command/pr-notes.md index ec24359954f2..efec2682c5da 100644 --- a/openspec/changes/add-dimos-web-page-command/pr-notes.md +++ b/openspec/changes/add-dimos-web-page-command/pr-notes.md @@ -1,4 +1,4 @@ -# PR Notes: DimOS Web Page View command +# PR notes: DimOS web page view command ## Canonical API diff --git a/openspec/changes/add-dimos-web-page-command/proposal.md b/openspec/changes/add-dimos-web-page-command/proposal.md index c6bc61e09227..80a504a62263 100644 --- a/openspec/changes/add-dimos-web-page-command/proposal.md +++ b/openspec/changes/add-dimos-web-page-command/proposal.md @@ -2,7 +2,7 @@ DimOS needs a pragmatic way to request a Web Page View panel from its existing websocket control path while preserving Rerun's canonical blueprint/Python API as the source of truth for viewer layout. This lets reviewers evaluate the DimOS convenience command independently from the core native Web Page View implementation. -## What Changes +## What changes - Add a DimOS-only websocket command, `open_web_page_view`, that requests a Web Page View panel by caller-owned `panel_id`. - Translate the websocket command into Web Page View blueprint state instead of directly mutating native webview internals. @@ -12,11 +12,11 @@ DimOS needs a pragmatic way to request a Web Page View panel from its existing w ## Capabilities -### New Capabilities +### New capabilities - `dimos-web-page-command`: DimOS websocket command for opening/updating/focusing native Web Page View panels. -### Modified Capabilities +### Modified capabilities - `native-web-page-view`: Clarifies that Web Page View remains blueprint-owned and can be requested by a DimOS websocket convenience wrapper without changing the canonical Rerun blueprint API. diff --git a/openspec/changes/add-dimos-web-page-command/specs/dimos-web-page-command/spec.md b/openspec/changes/add-dimos-web-page-command/specs/dimos-web-page-command/spec.md index 46a05b55d02a..ac48b2c16ecd 100644 --- a/openspec/changes/add-dimos-web-page-command/specs/dimos-web-page-command/spec.md +++ b/openspec/changes/add-dimos-web-page-command/specs/dimos-web-page-command/spec.md @@ -1,38 +1,38 @@ -## ADDED Requirements +## ADDED requirements -### Requirement: DimOS websocket can request a Web Page View +### Requirement: DimOS websocket can request a web page view The system SHALL accept a DimOS websocket command named `open_web_page_view` that requests a native Web Page View panel. -#### Scenario: Command creates a web page panel +#### Scenario: command creates a web page panel - **WHEN** the DimOS viewer receives `open_web_page_view` with `panel_id`, `title`, `url`, and `show_navigation_controls` - **THEN** the viewer creates a Web Page View configured with that title, URL, and navigation-control preference -### Requirement: Web page commands are idempotent by panel identifier +### Requirement: web page commands are idempotent by panel identifier The system SHALL treat `panel_id` as a caller-owned stable identifier for DimOS web page panel commands. -#### Scenario: Repeated command updates existing panel +#### Scenario: repeated command updates existing panel - **WHEN** the DimOS viewer receives `open_web_page_view` for a `panel_id` that already has a Web Page View in the current viewer session - **THEN** the viewer updates that existing panel rather than creating a duplicate panel -#### Scenario: First command creates runtime mapping +#### Scenario: first command creates runtime mapping - **WHEN** the DimOS viewer receives `open_web_page_view` for a new `panel_id` - **THEN** the viewer records a runtime-only mapping from that `panel_id` to the created Rerun view identity -### Requirement: Web page commands focus requested panels +### Requirement: web page commands focus requested panels The system SHALL focus or raise the requested Web Page View panel when handling an `open_web_page_view` command. -#### Scenario: Existing panel is focused after update +#### Scenario: existing panel is focused after update - **WHEN** the DimOS viewer updates an existing Web Page View for an `open_web_page_view` command - **THEN** the viewer focuses the tab containing that Web Page View when the layout supports tab focus -#### Scenario: Newly created panel is focused +#### Scenario: newly created panel is focused - **WHEN** the DimOS viewer creates a Web Page View for an `open_web_page_view` command - **THEN** the viewer focuses the newly created panel when the layout supports tab focus @@ -41,16 +41,16 @@ The system SHALL focus or raise the requested Web Page View panel when handling The system SHALL NOT expose arbitrary viewport tree editing through the `open_web_page_view` command. -#### Scenario: Layout placement fields are ignored or rejected +#### Scenario: layout placement fields are ignored or rejected - **WHEN** an `open_web_page_view` command includes placement fields such as split direction, width ratio, or tab group - **THEN** the DimOS viewer does not treat those fields as authoritative layout-edit instructions -### Requirement: Invalid web page command URLs are rejected safely +### Requirement: invalid web page command URLs are rejected safely The system SHALL validate URLs from `open_web_page_view` commands using the Web Page View HTTP URL policy before creating or updating panels. -#### Scenario: Unsupported scheme is rejected +#### Scenario: unsupported scheme is rejected - **WHEN** the DimOS viewer receives `open_web_page_view` with a `file://` URL - **THEN** the viewer rejects the command without creating or updating a Web Page View panel diff --git a/openspec/changes/add-dimos-web-page-command/specs/native-web-page-view/spec.md b/openspec/changes/add-dimos-web-page-command/specs/native-web-page-view/spec.md index 03f67c017802..5799694f76ed 100644 --- a/openspec/changes/add-dimos-web-page-command/specs/native-web-page-view/spec.md +++ b/openspec/changes/add-dimos-web-page-command/specs/native-web-page-view/spec.md @@ -1,15 +1,15 @@ -## MODIFIED Requirements +## MODIFIED requirements -### Requirement: Blueprint-owned configuration +### Requirement: blueprint-owned configuration The system SHALL store Web Page View configuration as blueprint/view state with a required URL and a `show_navigation_controls` setting. -#### Scenario: Blueprint preconfigures a Web Page View +#### Scenario: blueprint preconfigures a web page view - **WHEN** blueprint state contains a Web Page View with a valid URL and navigation controls setting - **THEN** the viewer creates the view with those configured values -#### Scenario: Manual creation starts unconfigured +#### Scenario: manual creation starts unconfigured - **WHEN** a user manually adds a Web Page View without setting a URL - **THEN** the viewer displays a Rerun-side status explaining that no URL is configured diff --git a/openspec/changes/add-dimos-web-page-command/tasks.md b/openspec/changes/add-dimos-web-page-command/tasks.md index efbe14f045eb..99412b3a0ba0 100644 --- a/openspec/changes/add-dimos-web-page-command/tasks.md +++ b/openspec/changes/add-dimos-web-page-command/tasks.md @@ -25,7 +25,7 @@ - [x] 4.3 Verify v1 ignores or rejects layout-placement fields rather than treating them as viewport-tree commands. - [x] 4.4 Keep implementation localized to `dimos/`; pause for design review if core viewer APIs must be changed. -## 5. PR formalization +## 5. Pr formalization - [x] 5.1 Update PR notes or documentation text with the canonical Python/blueprint API example. - [x] 5.2 Explicitly label the DimOS websocket command as experimental, DimOS-only, and removable. diff --git a/openspec/changes/archive/2026-06-12-add-native-web-page-view/design.md b/openspec/changes/archive/2026-06-12-add-native-web-page-view/design.md index 4b0ddbc75621..1afc66a3fd37 100644 --- a/openspec/changes/archive/2026-06-12-add-native-web-page-view/design.md +++ b/openspec/changes/archive/2026-06-12-add-native-web-page-view/design.md @@ -28,7 +28,7 @@ The Web Page View introduces a native-only view that displays a configured `http ## Decisions -### Native-only Web Page View +### Native-only web page view The Web Page View is available only in native viewer builds. The web viewer should render a clear unsupported status instead of attempting iframe support. @@ -82,7 +82,7 @@ Web Page Views share the default embedded browser session/profile in the initial **Alternatives considered:** - Isolate each view by default. Rejected because common dashboard use cases would require repeated logins and duplicate session setup. -## Risks / Trade-offs +## Risks / trade-offs - Native webview surfaces may not clip, stack, or resize exactly like egui widgets → Keep the webview integration behind a narrow module boundary, update bounds from the egui view rectangle, and test split panels/tabs/resizing. - Linux support depends on WebKitGTK/display-server details → Treat support as wry-supported native platforms, document dependencies, and render backend errors clearly. diff --git a/openspec/changes/archive/2026-06-12-add-native-web-page-view/proposal.md b/openspec/changes/archive/2026-06-12-add-native-web-page-view/proposal.md index 451ab1ccb470..405248b2bdd8 100644 --- a/openspec/changes/archive/2026-06-12-add-native-web-page-view/proposal.md +++ b/openspec/changes/archive/2026-06-12-add-native-web-page-view/proposal.md @@ -2,7 +2,7 @@ Rerun users can compose rich native viewer layouts for logged data, but cannot place a live webpage alongside 3D, image, and other views. A native inline Web Page View enables dashboards, local robot control panels, documentation, and other http(s) pages to appear inside the Rerun viewer layout instead of requiring a separate browser window. -## What Changes +## What changes - Add a native-only **Web Page View** that displays a configured webpage inline in the viewer layout. - Store the view's URL and lightweight browser chrome preference as blueprint/view configuration, not logged timeline data. @@ -16,10 +16,10 @@ Rerun users can compose rich native viewer layouts for logged data, but cannot p ## Capabilities -### New Capabilities +### New capabilities - `native-web-page-view`: Defines native Web Page View behavior, configuration, platform support, URL policy, lifecycle, navigation, session handling, and error reporting. -### Modified Capabilities +### Modified capabilities None. diff --git a/openspec/changes/archive/2026-06-12-add-native-web-page-view/specs/native-web-page-view/spec.md b/openspec/changes/archive/2026-06-12-add-native-web-page-view/specs/native-web-page-view/spec.md index 8d7a0770a010..d4c64178deea 100644 --- a/openspec/changes/archive/2026-06-12-add-native-web-page-view/specs/native-web-page-view/spec.md +++ b/openspec/changes/archive/2026-06-12-add-native-web-page-view/specs/native-web-page-view/spec.md @@ -1,31 +1,31 @@ -## ADDED Requirements +## ADDED requirements -### Requirement: Configured native Web Page View +### Requirement: configured native web page view The system SHALL provide a native-only Web Page View that displays a configured webpage inline in the viewer layout. -#### Scenario: Native view displays configured page +#### Scenario: native view displays configured page - **WHEN** a native viewer layout contains a Web Page View configured with `https://example.com` - **THEN** the viewer displays that page inline in the view area -#### Scenario: Web viewer reports unsupported view +#### Scenario: web viewer reports unsupported view - **WHEN** the web viewer renders a layout containing a Web Page View - **THEN** the viewer displays a clear unsupported status for that view -### Requirement: Blueprint-owned configuration +### Requirement: blueprint-owned configuration The system SHALL store Web Page View configuration as blueprint/view state with a required URL and a `show_navigation_controls` setting. -#### Scenario: Blueprint preconfigures a Web Page View +#### Scenario: blueprint preconfigures a web page view - **WHEN** blueprint state contains a Web Page View with a valid URL and navigation controls setting - **THEN** the viewer creates the view with those configured values -#### Scenario: Manual creation starts unconfigured +#### Scenario: manual creation starts unconfigured - **WHEN** a user manually adds a Web Page View without setting a URL - **THEN** the viewer displays a Rerun-side status explaining that no URL is configured -### Requirement: No data-driven spawning +### Requirement: no data-driven spawning The system SHALL make Web Page Views manually creatable and blueprint-preconfigurable without auto-spawning them from logged data. -#### Scenario: Logged entities do not suggest Web Page View +#### Scenario: logged entities do not suggest web page view - **WHEN** the viewer computes data-driven view suggestions from logged entities - **THEN** the suggestion set excludes Web Page View unless it was explicitly created or configured @@ -36,75 +36,75 @@ The system SHALL validate configured Web Page View URLs and load only `http://` - **WHEN** a Web Page View is configured with `https://example.com` - **THEN** the viewer attempts to load the URL in the native webview -#### Scenario: Local HTTP URL loads +#### Scenario: local HTTP URL loads - **WHEN** a Web Page View is configured with `http://localhost:3000` - **THEN** the viewer attempts to load the URL in the native webview -#### Scenario: Unsupported scheme is rejected +#### Scenario: unsupported scheme is rejected - **WHEN** a Web Page View is configured with `file:///tmp/report.html` - **THEN** the viewer displays a Rerun-side status explaining that only `http` and `https` URLs are supported -### Requirement: Automatic loading +### Requirement: automatic loading The system SHALL automatically load a valid configured Web Page View URL without requiring an additional confirmation action. -#### Scenario: Valid URL autoloads +#### Scenario: valid URL autoloads - **WHEN** a Web Page View becomes visible with a valid configured URL - **THEN** the native webview starts loading that URL automatically -### Requirement: Per-view webview instances +### Requirement: per-view webview instances The system SHALL create and manage one native webview instance for each Web Page View instance. -#### Scenario: Multiple views render independently +#### Scenario: multiple views render independently - **WHEN** a layout contains two Web Page Views with different valid URLs - **THEN** the viewer maintains two independent native webview instances and displays both pages in their respective view areas -### Requirement: Webview lifecycle preserves hidden view state +### Requirement: webview lifecycle preserves hidden view state The system SHALL keep a Web Page View's native webview instance alive while the Rerun view exists, including when temporarily hidden. -#### Scenario: Hidden view keeps page state +#### Scenario: hidden view keeps page state - **WHEN** a Web Page View is hidden behind a tab and later shown again - **THEN** the viewer reuses the existing webview instance rather than reloading the configured URL from scratch -#### Scenario: Removed view destroys instance +#### Scenario: removed view destroys instance - **WHEN** a Web Page View is removed from the layout - **THEN** the viewer destroys the corresponding native webview instance -### Requirement: Browser-like navigation +### Requirement: browser-like navigation The system SHALL allow runtime browser navigation inside the Web Page View while keeping the configured URL unchanged. -#### Scenario: Link navigation does not change blueprint URL +#### Scenario: link navigation does not change blueprint URL - **WHEN** a user clicks a link inside a Web Page View and the embedded page navigates to a different URL - **THEN** the blueprint-configured URL remains the original configured URL -#### Scenario: Home returns to configured URL +#### Scenario: home returns to configured URL - **WHEN** navigation controls are visible and the user activates Home after navigating away - **THEN** the webview navigates back to the configured URL -### Requirement: Optional navigation controls +### Requirement: optional navigation controls The system SHALL provide lightweight navigation controls for Web Page Views and allow them to be hidden by configuration. -#### Scenario: Navigation controls visible by default +#### Scenario: navigation controls visible by default - **WHEN** a Web Page View is configured without specifying `show_navigation_controls` - **THEN** the viewer displays back, forward, reload, home, and URL display controls -#### Scenario: Navigation controls hidden +#### Scenario: navigation controls hidden - **WHEN** a Web Page View is configured with `show_navigation_controls` set to false - **THEN** the viewer hides the navigation controls and gives the webview the available view area -### Requirement: Shared embedded browser session +### Requirement: shared embedded browser session The system SHALL use a shared embedded browser session/profile for Web Page Views by default. -#### Scenario: Session state shared across views +#### Scenario: session state shared across views - **WHEN** two Web Page Views load pages from the same origin - **THEN** the embedded browser backend uses the shared default session/profile for both views -### Requirement: Rerun-side failure reporting +### Requirement: rerun-side failure reporting The system SHALL report configuration and backend failures using Rerun-side status UI outside the embedded webpage. -#### Scenario: Backend creation fails +#### Scenario: backend creation fails - **WHEN** the native webview backend cannot create a webview instance - **THEN** the Web Page View displays a clear Rerun-side failure message instead of crashing the viewer -#### Scenario: Invalid URL is configured +#### Scenario: invalid URL is configured - **WHEN** the configured URL cannot be parsed as a URL - **THEN** the Web Page View displays a clear Rerun-side invalid URL message diff --git a/openspec/changes/archive/2026-06-12-add-native-web-page-view/tasks.md b/openspec/changes/archive/2026-06-12-add-native-web-page-view/tasks.md index 3e8225359200..cff049a86e4a 100644 --- a/openspec/changes/archive/2026-06-12-add-native-web-page-view/tasks.md +++ b/openspec/changes/archive/2026-06-12-add-native-web-page-view/tasks.md @@ -1,22 +1,22 @@ -## 1. TDD Tracer Bullet: Blueprint View Skeleton +## 1. Tdd tracer bullet: blueprint view skeleton - [x] 1.1 RED: Add one behavior test that expects a manually created Web Page View with no URL to render a Rerun-side "No URL configured" status through the public view/test harness. - [x] 1.2 GREEN: Add the minimal `WebPageView` blueprint definition, generated code, `re_view_web_page` crate/module skeleton, view registration, and empty-URL status UI needed to pass 1.1. - [x] 1.3 REFACTOR: Align names with existing view conventions (`Web Page View` user-facing name, `web_page`/`WebPageView` code names as appropriate) and run `pixi run rs-fmt` plus the smallest relevant Rust check. -## 2. TDD Slice: Blueprint Configuration and Manual Creation +## 2. Tdd slice: blueprint configuration and manual creation - [x] 2.1 RED: Add a behavior test that creates a Web Page View from blueprint state containing `url` and `show_navigation_controls`, and verifies those values are read by the view without logged data. - [x] 2.2 GREEN: Implement blueprint properties for required URL and defaulted `show_navigation_controls = true`; ensure manual creation works and data-driven spawn heuristics do not suggest the view. - [x] 2.3 REFACTOR: Keep configuration access isolated behind a small typed helper so later backend code does not parse blueprint state directly. -## 3. TDD Slice: URL Policy and Status Errors +## 3. Tdd slice: URL policy and status errors - [x] 3.1 RED: Add behavior tests for accepted `https://example.com`, accepted `http://localhost:3000`, rejected `file:///tmp/report.html`, and invalid URL text. - [x] 3.2 GREEN: Implement URL parsing/validation that allows only `http` and `https`, including localhost/private-network HTTP, and renders Rerun-side status messages for invalid or unsupported URLs. - [x] 3.3 REFACTOR: Move URL policy into a backend-independent unit with focused tests so native webview code can trust validated URLs. -## 4. TDD Slice: Native Webview Backend Seam +## 4. Tdd slice: native webview backend seam - [x] 4.1 RED: Add tests using a fake backend that verify a valid configured URL causes exactly one backend webview instance to be created for one Web Page View. - [x] 4.2 GREEN: Introduce a narrow native webview backend abstraction and wire the view to it with a fake/test backend; do not depend on `wry` in behavior tests. @@ -24,13 +24,13 @@ - [x] 4.4 GREEN: Implement per-view instance ownership keyed by stable view identity. - [x] 4.5 REFACTOR: Keep egui/view logic, lifecycle manager, and platform backend responsibilities separate. -## 5. TDD Slice: Direct wry Integration +## 5. Tdd slice: direct wry integration - [x] 5.1 RED: Add a compile-gated native integration test or smoke test target that exercises construction through the real native backend boundary and reports backend creation failure instead of panicking. - [x] 5.2 GREEN: Add direct `wry` integration for native builds, including required Cargo feature/dependency wiring and platform-specific creation paths supported by wry. - [x] 5.3 REFACTOR: Encapsulate platform-specific wry details behind the backend boundary, including Linux WebKitGTK/X11/Wayland caveats and bounds-setting differences. -## 6. TDD Slice: Layout Bounds and Lifecycle +## 6. Tdd slice: layout bounds and lifecycle - [x] 6.1 RED: Add fake-backend behavior tests that verify the backend receives updated bounds when the egui view rectangle changes. - [x] 6.2 GREEN: Update native webview bounds from the allocated egui view rectangle each frame, accounting for DPI/points-to-pixels conversion. @@ -38,21 +38,21 @@ - [x] 6.4 GREEN: Implement keep-alive-while-hidden lifecycle and destroy-on-remove/app-exit cleanup. - [x] 6.5 REFACTOR: Audit focus, clipping, tab, split-panel, and resize behavior with the fake backend before manual native smoke testing. -## 7. TDD Slice: Navigation Controls and Runtime Navigation +## 7. Tdd slice: navigation controls and runtime navigation - [x] 7.1 RED: Add behavior tests that navigation controls are visible by default and hidden when `show_navigation_controls` is false. - [x] 7.2 GREEN: Implement lightweight back, forward, reload, home, and URL display controls above the embedded webview, reserving the full view area when controls are hidden. - [x] 7.3 RED: Add a behavior test that runtime navigation does not mutate the blueprint-configured URL and that Home navigates back to the configured URL. - [x] 7.4 GREEN: Wire navigation commands through the backend abstraction while keeping configured URL state immutable during runtime navigation. -## 8. TDD Slice: Platform Support, Session Defaults, and Failure UI +## 8. Tdd slice: Platform support, session defaults, and failure UI - [x] 8.1 RED: Add behavior tests that web builds or unavailable native backends render explicit unsupported/failure status UI. - [x] 8.2 GREEN: Implement unsupported-target and backend-failure reporting outside the webview surface. - [x] 8.3 RED: Add a fake-backend behavior test that multiple views use the shared default browser profile/session configuration. - [x] 8.4 GREEN: Implement shared default session/profile behavior in the backend boundary while leaving per-view isolation as a future extension point. -## 9. Verification and Documentation +## 9. Verification and documentation - [x] 9.1 Run `pixi run codegen` after blueprint definition changes and verify generated Rust/Python/C++ outputs are updated as expected. - [x] 9.2 Run `pixi run rs-fmt` after Rust changes. diff --git a/openspec/specs/native-web-page-view/spec.md b/openspec/specs/native-web-page-view/spec.md index ba978b47cec5..98c005a23914 100644 --- a/openspec/specs/native-web-page-view/spec.md +++ b/openspec/specs/native-web-page-view/spec.md @@ -4,39 +4,39 @@ Native Web Page View lets native Rerun viewer layouts embed configured `http(s)` ## Requirements -### Requirement: Configured native Web Page View +### Requirement: configured native web page view The system SHALL provide a native-only Web Page View that displays a configured webpage inline in the viewer layout. -#### Scenario: Native view displays configured page +#### Scenario: native view displays configured page - **WHEN** a native viewer layout contains a Web Page View configured with `https://example.com` - **THEN** the viewer displays that page inline in the view area -#### Scenario: Web viewer reports unsupported view +#### Scenario: web viewer reports unsupported view - **WHEN** the web viewer renders a layout containing a Web Page View - **THEN** the viewer displays a clear unsupported status for that view -### Requirement: Blueprint-owned configuration +### Requirement: blueprint-owned configuration The system SHALL store Web Page View configuration as blueprint/view state with a required URL and a `show_navigation_controls` setting. -#### Scenario: Blueprint preconfigures a Web Page View +#### Scenario: blueprint preconfigures a web page view - **WHEN** blueprint state contains a Web Page View with a valid URL and navigation controls setting - **THEN** the viewer creates the view with those configured values -#### Scenario: Manual creation starts unconfigured +#### Scenario: manual creation starts unconfigured - **WHEN** a user manually adds a Web Page View without setting a URL - **THEN** the viewer displays a Rerun-side status explaining that no URL is configured -### Requirement: No data-driven spawning +### Requirement: no data-driven spawning The system SHALL make Web Page Views manually creatable and blueprint-preconfigurable without auto-spawning them from logged data. -#### Scenario: Logged entities do not suggest Web Page View +#### Scenario: logged entities do not suggest web page view - **WHEN** the viewer computes data-driven view suggestions from logged entities - **THEN** the suggestion set excludes Web Page View unless it was explicitly created or configured @@ -50,95 +50,95 @@ The system SHALL validate configured Web Page View URLs and load only `http://` - **WHEN** a Web Page View is configured with `https://example.com` - **THEN** the viewer attempts to load the URL in the native webview -#### Scenario: Local HTTP URL loads +#### Scenario: local HTTP URL loads - **WHEN** a Web Page View is configured with `http://localhost:3000` - **THEN** the viewer attempts to load the URL in the native webview -#### Scenario: Unsupported scheme is rejected +#### Scenario: unsupported scheme is rejected - **WHEN** a Web Page View is configured with `file:///tmp/report.html` - **THEN** the viewer displays a Rerun-side status explaining that only `http` and `https` URLs are supported -### Requirement: Automatic loading +### Requirement: automatic loading The system SHALL automatically load a valid configured Web Page View URL without requiring an additional confirmation action. -#### Scenario: Valid URL autoloads +#### Scenario: valid URL autoloads - **WHEN** a Web Page View becomes visible with a valid configured URL - **THEN** the native webview starts loading that URL automatically -### Requirement: Per-view webview instances +### Requirement: per-view webview instances The system SHALL create and manage one native webview instance for each Web Page View instance. -#### Scenario: Multiple views render independently +#### Scenario: multiple views render independently - **WHEN** a layout contains two Web Page Views with different valid URLs - **THEN** the viewer maintains two independent native webview instances and displays both pages in their respective view areas -### Requirement: Webview lifecycle preserves hidden view state +### Requirement: webview lifecycle preserves hidden view state The system SHALL keep a Web Page View's native webview instance alive while the Rerun view exists, including when temporarily hidden. -#### Scenario: Hidden view keeps page state +#### Scenario: hidden view keeps page state - **WHEN** a Web Page View is hidden behind a tab and later shown again - **THEN** the viewer reuses the existing webview instance rather than reloading the configured URL from scratch -#### Scenario: Removed view destroys instance +#### Scenario: removed view destroys instance - **WHEN** a Web Page View is removed from the layout - **THEN** the viewer destroys the corresponding native webview instance -### Requirement: Browser-like navigation +### Requirement: browser-like navigation The system SHALL allow runtime browser navigation inside the Web Page View while keeping the configured URL unchanged. -#### Scenario: Link navigation does not change blueprint URL +#### Scenario: link navigation does not change blueprint URL - **WHEN** a user clicks a link inside a Web Page View and the embedded page navigates to a different URL - **THEN** the blueprint-configured URL remains the original configured URL -#### Scenario: Home returns to configured URL +#### Scenario: home returns to configured URL - **WHEN** navigation controls are visible and the user activates Home after navigating away - **THEN** the webview navigates back to the configured URL -### Requirement: Optional navigation controls +### Requirement: optional navigation controls The system SHALL provide lightweight navigation controls for Web Page Views and allow them to be hidden by configuration. -#### Scenario: Navigation controls visible by default +#### Scenario: navigation controls visible by default - **WHEN** a Web Page View is configured without specifying `show_navigation_controls` - **THEN** the viewer displays back, forward, reload, home, and URL display controls -#### Scenario: Navigation controls hidden +#### Scenario: navigation controls hidden - **WHEN** a Web Page View is configured with `show_navigation_controls` set to false - **THEN** the viewer hides the navigation controls and gives the webview the available view area -### Requirement: Shared embedded browser session +### Requirement: shared embedded browser session The system SHALL use a shared embedded browser session/profile for Web Page Views by default. -#### Scenario: Session state shared across views +#### Scenario: session state shared across views - **WHEN** two Web Page Views load pages from the same origin - **THEN** the embedded browser backend uses the shared default session/profile for both views -### Requirement: Rerun-side failure reporting +### Requirement: rerun-side failure reporting The system SHALL report configuration and backend failures using Rerun-side status UI outside the embedded webpage. -#### Scenario: Backend creation fails +#### Scenario: backend creation fails - **WHEN** the native webview backend cannot create a webview instance - **THEN** the Web Page View displays a clear Rerun-side failure message instead of crashing the viewer -#### Scenario: Invalid URL is configured +#### Scenario: invalid URL is configured - **WHEN** the configured URL cannot be parsed as a URL - **THEN** the Web Page View displays a clear Rerun-side invalid URL message diff --git a/scripts/web_page_view_smoke/README.md b/scripts/web_page_view_smoke/README.md index 3cf1db203520..250e76d2eeb0 100644 --- a/scripts/web_page_view_smoke/README.md +++ b/scripts/web_page_view_smoke/README.md @@ -1,4 +1,4 @@ -# Web Page View smoke scripts +# Web page view smoke scripts These scripts manually smoke-test the experimental native Web Page View. diff --git a/scripts/web_page_view_smoke/serve_dimos_ws_command.py b/scripts/web_page_view_smoke/serve_dimos_ws_command.py index 7cabeeba38e6..19e8f5d82a18 100755 --- a/scripts/web_page_view_smoke/serve_dimos_ws_command.py +++ b/scripts/web_page_view_smoke/serve_dimos_ws_command.py @@ -10,7 +10,6 @@ import struct import time - HOST = "127.0.0.1" PORT = 3032 WEBSOCKET_GUID = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11" diff --git a/scripts/web_page_view_smoke/show_two_pages.sh b/scripts/web_page_view_smoke/show_two_pages.sh index 532dec5dd8f0..11c7b3508e5a 100755 --- a/scripts/web_page_view_smoke/show_two_pages.sh +++ b/scripts/web_page_view_smoke/show_two_pages.sh @@ -11,7 +11,7 @@ import time import rerun as rr import rerun.blueprint as rrb -rr.init("web_page_manual_smoke") +rr.init("rerun_example_web_page_manual_smoke") rr.connect_grpc("rerun+http://127.0.0.1:9876/proxy") rr.send_blueprint( rrb.Blueprint( diff --git a/scripts/web_page_view_smoke/show_youtube.sh b/scripts/web_page_view_smoke/show_youtube.sh index 56b6b856ba15..c9e19c18b015 100755 --- a/scripts/web_page_view_smoke/show_youtube.sh +++ b/scripts/web_page_view_smoke/show_youtube.sh @@ -11,7 +11,7 @@ import time import rerun as rr import rerun.blueprint as rrb -rr.init("web_page_youtube_smoke") +rr.init("rerun_example_web_page_youtube_smoke") rr.connect_grpc("rerun+http://127.0.0.1:9876/proxy") rr.send_blueprint( rrb.Blueprint( From 4d3aa84256136a1999ee3bfc29bd08eb18dbb687 Mon Sep 17 00:00:00 2001 From: cc Date: Tue, 16 Jun 2026 09:56:07 -0700 Subject: [PATCH 09/12] openspec remove --- CONTEXT.md | 9 -- .../add-dimos-web-page-command/.openspec.yaml | 2 - .../add-dimos-web-page-command/design.md | 58 ------- .../add-dimos-web-page-command/pr-notes.md | 48 ------ .../add-dimos-web-page-command/proposal.md | 28 ---- .../specs/dimos-web-page-command/spec.md | 61 -------- .../specs/native-web-page-view/spec.md | 20 --- .../add-dimos-web-page-command/tasks.md | 38 ----- .../.openspec.yaml | 2 - .../design.md | 92 ----------- .../proposal.md | 32 ---- .../specs/native-web-page-view/spec.md | 110 ------------- .../tasks.md | 61 -------- openspec/config.yaml | 20 --- openspec/specs/native-web-page-view/spec.md | 144 ------------------ 15 files changed, 725 deletions(-) delete mode 100644 CONTEXT.md delete mode 100644 openspec/changes/add-dimos-web-page-command/.openspec.yaml delete mode 100644 openspec/changes/add-dimos-web-page-command/design.md delete mode 100644 openspec/changes/add-dimos-web-page-command/pr-notes.md delete mode 100644 openspec/changes/add-dimos-web-page-command/proposal.md delete mode 100644 openspec/changes/add-dimos-web-page-command/specs/dimos-web-page-command/spec.md delete mode 100644 openspec/changes/add-dimos-web-page-command/specs/native-web-page-view/spec.md delete mode 100644 openspec/changes/add-dimos-web-page-command/tasks.md delete mode 100644 openspec/changes/archive/2026-06-12-add-native-web-page-view/.openspec.yaml delete mode 100644 openspec/changes/archive/2026-06-12-add-native-web-page-view/design.md delete mode 100644 openspec/changes/archive/2026-06-12-add-native-web-page-view/proposal.md delete mode 100644 openspec/changes/archive/2026-06-12-add-native-web-page-view/specs/native-web-page-view/spec.md delete mode 100644 openspec/changes/archive/2026-06-12-add-native-web-page-view/tasks.md delete mode 100644 openspec/config.yaml delete mode 100644 openspec/specs/native-web-page-view/spec.md diff --git a/CONTEXT.md b/CONTEXT.md deleted file mode 100644 index 4acbe72d2923..000000000000 --- a/CONTEXT.md +++ /dev/null @@ -1,9 +0,0 @@ -# Rerun Viewer - -Rerun Viewer is the user-facing application for inspecting logged multimodal data through configurable views. - -## Language - -**Web Page View**: -A native-only Rerun view that displays a configured webpage inline as part of the viewer layout. -_Avoid_: WebView, link view, browser view diff --git a/openspec/changes/add-dimos-web-page-command/.openspec.yaml b/openspec/changes/add-dimos-web-page-command/.openspec.yaml deleted file mode 100644 index 64b47c4313b2..000000000000 --- a/openspec/changes/add-dimos-web-page-command/.openspec.yaml +++ /dev/null @@ -1,2 +0,0 @@ -schema: spec-driven -created: 2026-06-13 diff --git a/openspec/changes/add-dimos-web-page-command/design.md b/openspec/changes/add-dimos-web-page-command/design.md deleted file mode 100644 index dc958de9add0..000000000000 --- a/openspec/changes/add-dimos-web-page-command/design.md +++ /dev/null @@ -1,58 +0,0 @@ -## Context - -The native Web Page View implementation already exposes the canonical Rerun path for creating a web panel: users send blueprint state through the generated Rerun SDK, e.g. `rr.send_blueprint(rrb.WebPageView(config=rrb.WebPageViewConfig(url=…)))`. That path is official, typed, and transported through Rerun's existing log/gRPC channel. - -DimOS also has an existing websocket control path in `dimos/src/interaction/ws.rs`. Today it is outbound from the viewer to the DimOS server for click, twist, and stop events; incoming frames are consumed only to keep the connection healthy. The DimOS viewer wrapper in `dimos/src/viewer.rs` wraps `re_viewer::App` for keyboard and selection behavior. - -This change adds a small DimOS-only inbound command lane for requesting Web Page View panels while keeping Rerun blueprint state as the source of truth. - -## Goals / Non-Goals - -**Goals:** - -- Add an experimental DimOS websocket command for opening/updating/focusing a Web Page View. -- Keep the command a thin convenience wrapper around Web Page View blueprint state. -- Keep `panel_id -> ViewId` state runtime-only inside the DimOS viewer wrapper. -- Keep the implementation localized to `dimos/` unless a narrowly-scoped core viewer API is required to apply blueprint updates cleanly. -- Make the DimOS command easy to remove if reviewers prefer the canonical Python/blueprint API only. - -**Non-Goals:** - -- Do not add new Rerun SDK schema/codegen for this DimOS command. -- Do not make the DimOS websocket a stable Rerun layout API. -- Do not expose arbitrary layout placement, split ratios, or viewport-tree editing in v1. -- Do not bypass the Web Page View URL policy or native backend lifecycle. -- Do not support old stock `rerun-sdk` Python packages as a typed Web Page View API; they can use the DimOS command as a pragmatic fallback. - -## Decisions - -1. **Canonical API remains Rerun blueprint/Python.** - - Use `rr.send_blueprint(rrb.WebPageView(...))` as the official API story. - - The DimOS websocket command is a convenience path, not a second source of truth. - - Alternative considered: make DimOS websocket the primary panel API. Rejected because it duplicates Rerun's viewer-layout channel and would be harder for Rerun reviewers to accept. - -2. **Inbound websocket command is caller-owned and idempotent by `panel_id`.** - - Command shape includes `panel_id`, `title`, `url`, and `show_navigation_controls`. - - Repeating the same `panel_id` updates/focuses the existing panel instead of creating duplicates. - - Alternative considered: viewer-generated IDs. Rejected because it would require a response/lookup protocol before callers could update or close the panel. - -3. **`panel_id -> ViewId` mapping is runtime-only.** - - The DimOS wrapper stores the mapping in memory. - - Restarting the viewer loses the mapping, which is acceptable for an experimental command. - - Alternative considered: persist `panel_id` in blueprint metadata. Rejected because it would add schema/API surface solely for a DimOS convenience wrapper. - -4. **Focus/raise on open/update if feasible.** - - Existing `ViewportBlueprint::focus_tab(view_id)` supports focusing a view's tab through `ViewportCommand::FocusTab`. - - `open_web_page_view` should make the requested panel visible to the user when possible. - -5. **Keep layout placement out of scope.** - - The command requests a logical panel, not a full remote layout edit. - - If placement is needed later, it should be designed as a separate layout-control capability. - -## Risks / trade-offs - -- **Risk: Applying blueprint changes from `DimosApp` may require a core viewer API.** → First inspect for an existing clean path. If unavailable, add the smallest focused viewer method/command needed rather than poking internal viewport state. -- **Risk: Two apparent APIs can confuse reviewers.** → PR text must explicitly label Python/blueprint as canonical and DimOS websocket as experimental/removable. -- **Risk: Runtime-only mapping loses identity across restart.** → Accept for v1; callers can resend commands after reconnect. -- **Risk: DimOS command bypasses typed Python validation.** → Reuse or mirror the same `http`/`https` URL policy before creating/updating the panel. -- **Risk: Scope creep into layout management.** → Keep v1 to create/update/focus only. diff --git a/openspec/changes/add-dimos-web-page-command/pr-notes.md b/openspec/changes/add-dimos-web-page-command/pr-notes.md deleted file mode 100644 index efec2682c5da..000000000000 --- a/openspec/changes/add-dimos-web-page-command/pr-notes.md +++ /dev/null @@ -1,48 +0,0 @@ -# PR notes: DimOS web page view command - -## Canonical API - -The preferred, stable way to create a Web Page View remains the Rerun blueprint API: - -```python -import rerun as rr -import rerun.blueprint as rrb - -rr.send_blueprint( - rrb.Blueprint( - rrb.WebPageView( - name="Viser", - config=rrb.WebPageViewConfig( - url="http://127.0.0.1:8095/", - show_navigation_controls=True, - ), - ) - ) -) -``` - -## DimOS websocket command - -The DimOS websocket command is experimental, DimOS-only, and removable. It is a convenience wrapper for environments that already speak to the DimOS viewer websocket and cannot rely on a generated Rerun Python SDK from this branch. - -```json -{ - "type": "open_web_page_view", - "panel_id": "viser", - "title": "Viser", - "url": "http://127.0.0.1:8095/", - "show_navigation_controls": true -} -``` - -The command translates to normal Web Page View blueprint state. It does not mutate native webview internals directly and does not expose arbitrary viewport layout control. - -## File-scope justification - -The large generated-file blast radius belongs to the core Web Page View feature. This follow-up command stays intentionally small: - -- `dimos/src/interaction/ws.rs` parses inbound commands and preserves existing outbound event JSON. -- `dimos/src/viewer.rs` drains validated commands from the websocket queue. -- `crates/viewer/re_viewer/src/app.rs` and `crates/viewer/re_viewer/src/app_state.rs` expose and implement a tiny public wrapper API that translates a request into existing blueprint/view/focus operations. - -No new SDK schema, generated API, or native webview backend behavior is introduced by this command. diff --git a/openspec/changes/add-dimos-web-page-command/proposal.md b/openspec/changes/add-dimos-web-page-command/proposal.md deleted file mode 100644 index 80a504a62263..000000000000 --- a/openspec/changes/add-dimos-web-page-command/proposal.md +++ /dev/null @@ -1,28 +0,0 @@ -## Why - -DimOS needs a pragmatic way to request a Web Page View panel from its existing websocket control path while preserving Rerun's canonical blueprint/Python API as the source of truth for viewer layout. This lets reviewers evaluate the DimOS convenience command independently from the core native Web Page View implementation. - -## What changes - -- Add a DimOS-only websocket command, `open_web_page_view`, that requests a Web Page View panel by caller-owned `panel_id`. -- Translate the websocket command into Web Page View blueprint state instead of directly mutating native webview internals. -- Keep a runtime-only `panel_id -> ViewId` mapping in the DimOS viewer wrapper so repeated commands update/focus the same panel. -- Keep layout placement intentionally minimal for v1: create/update/focus the logical panel, but do not expose split direction, size ratios, tab placement, or arbitrary viewport-tree control. -- Preserve the canonical API story: updated Rerun SDK users should use `rr.send_blueprint(rrb.WebPageView(...))`; the DimOS websocket command is an experimental/removable convenience path. - -## Capabilities - -### New capabilities - -- `dimos-web-page-command`: DimOS websocket command for opening/updating/focusing native Web Page View panels. - -### Modified capabilities - -- `native-web-page-view`: Clarifies that Web Page View remains blueprint-owned and can be requested by a DimOS websocket convenience wrapper without changing the canonical Rerun blueprint API. - -## Impact - -- Affected DimOS code: websocket protocol handling in `dimos/src/interaction/ws.rs` and viewer wrapper command handling in `dimos/src/viewer.rs`. -- Possible small helper/export changes under `dimos/src/interaction/` if needed to keep parsing and idempotency logic isolated. -- Should not require new generated Rerun SDK/schema/codegen changes, native webview backend changes, or broader viewer core changes unless blueprint application from the DimOS wrapper proves blocked. -- PR narrative should explicitly separate the existing core Web Page View implementation from this DimOS-only convenience command. diff --git a/openspec/changes/add-dimos-web-page-command/specs/dimos-web-page-command/spec.md b/openspec/changes/add-dimos-web-page-command/specs/dimos-web-page-command/spec.md deleted file mode 100644 index ac48b2c16ecd..000000000000 --- a/openspec/changes/add-dimos-web-page-command/specs/dimos-web-page-command/spec.md +++ /dev/null @@ -1,61 +0,0 @@ -## ADDED requirements - -### Requirement: DimOS websocket can request a web page view - -The system SHALL accept a DimOS websocket command named `open_web_page_view` that requests a native Web Page View panel. - -#### Scenario: command creates a web page panel - -- **WHEN** the DimOS viewer receives `open_web_page_view` with `panel_id`, `title`, `url`, and `show_navigation_controls` -- **THEN** the viewer creates a Web Page View configured with that title, URL, and navigation-control preference - -### Requirement: web page commands are idempotent by panel identifier - -The system SHALL treat `panel_id` as a caller-owned stable identifier for DimOS web page panel commands. - -#### Scenario: repeated command updates existing panel - -- **WHEN** the DimOS viewer receives `open_web_page_view` for a `panel_id` that already has a Web Page View in the current viewer session -- **THEN** the viewer updates that existing panel rather than creating a duplicate panel - -#### Scenario: first command creates runtime mapping - -- **WHEN** the DimOS viewer receives `open_web_page_view` for a new `panel_id` -- **THEN** the viewer records a runtime-only mapping from that `panel_id` to the created Rerun view identity - -### Requirement: web page commands focus requested panels - -The system SHALL focus or raise the requested Web Page View panel when handling an `open_web_page_view` command. - -#### Scenario: existing panel is focused after update - -- **WHEN** the DimOS viewer updates an existing Web Page View for an `open_web_page_view` command -- **THEN** the viewer focuses the tab containing that Web Page View when the layout supports tab focus - -#### Scenario: newly created panel is focused - -- **WHEN** the DimOS viewer creates a Web Page View for an `open_web_page_view` command -- **THEN** the viewer focuses the newly created panel when the layout supports tab focus - -### Requirement: DimOS command scope remains minimal - -The system SHALL NOT expose arbitrary viewport tree editing through the `open_web_page_view` command. - -#### Scenario: layout placement fields are ignored or rejected - -- **WHEN** an `open_web_page_view` command includes placement fields such as split direction, width ratio, or tab group -- **THEN** the DimOS viewer does not treat those fields as authoritative layout-edit instructions - -### Requirement: invalid web page command URLs are rejected safely - -The system SHALL validate URLs from `open_web_page_view` commands using the Web Page View HTTP URL policy before creating or updating panels. - -#### Scenario: unsupported scheme is rejected - -- **WHEN** the DimOS viewer receives `open_web_page_view` with a `file://` URL -- **THEN** the viewer rejects the command without creating or updating a Web Page View panel - -#### Scenario: HTTP URL is accepted - -- **WHEN** the DimOS viewer receives `open_web_page_view` with an `http://` or `https://` URL -- **THEN** the viewer may create or update the requested Web Page View panel diff --git a/openspec/changes/add-dimos-web-page-command/specs/native-web-page-view/spec.md b/openspec/changes/add-dimos-web-page-command/specs/native-web-page-view/spec.md deleted file mode 100644 index 5799694f76ed..000000000000 --- a/openspec/changes/add-dimos-web-page-command/specs/native-web-page-view/spec.md +++ /dev/null @@ -1,20 +0,0 @@ -## MODIFIED requirements - -### Requirement: blueprint-owned configuration - -The system SHALL store Web Page View configuration as blueprint/view state with a required URL and a `show_navigation_controls` setting. - -#### Scenario: blueprint preconfigures a web page view - -- **WHEN** blueprint state contains a Web Page View with a valid URL and navigation controls setting -- **THEN** the viewer creates the view with those configured values - -#### Scenario: manual creation starts unconfigured - -- **WHEN** a user manually adds a Web Page View without setting a URL -- **THEN** the viewer displays a Rerun-side status explaining that no URL is configured - -#### Scenario: DimOS command translates to blueprint state - -- **WHEN** the DimOS viewer handles an `open_web_page_view` websocket command -- **THEN** the resulting Web Page View URL and navigation-control setting are represented as Web Page View blueprint configuration rather than native-webview-only state diff --git a/openspec/changes/add-dimos-web-page-command/tasks.md b/openspec/changes/add-dimos-web-page-command/tasks.md deleted file mode 100644 index 99412b3a0ba0..000000000000 --- a/openspec/changes/add-dimos-web-page-command/tasks.md +++ /dev/null @@ -1,38 +0,0 @@ -## 1. Protocol and parsing - -- [x] 1.1 Add a failing test or focused assertion for parsing an inbound `open_web_page_view` websocket command. -- [x] 1.2 Add a `WsCommand::OpenWebPageView` protocol type with `panel_id`, `title`, `url`, and `show_navigation_controls` fields. -- [x] 1.3 Keep existing outbound `WsEvent` messages (`click`, `twist`, `stop`) wire-compatible. - -## 2. Bidirectional websocket plumbing - -- [x] 2.1 Add a non-blocking incoming command queue from the websocket read loop to the DimOS viewer wrapper. -- [x] 2.2 Ensure ping/pong and reconnect behavior still works while inbound command frames are parsed. -- [x] 2.3 Log and ignore malformed or unknown inbound websocket messages without crashing the viewer. - -## 3. Blueprint command application - -- [x] 3.1 Inspect and choose the least invasive path for applying Web Page View blueprint updates from `DimosApp`. -- [x] 3.2 Add a helper that creates a Web Page View blueprint entry and saves `WebPageViewConfig` for a valid command. -- [x] 3.3 Add runtime-only `panel_id -> ViewId` tracking in `DimosApp`. -- [x] 3.4 Implement idempotent command handling: create for new `panel_id`, update existing panel for repeated `panel_id`. -- [x] 3.5 Focus the created or updated panel through existing viewport focus behavior when feasible. - -## 4. Validation and scope control - -- [x] 4.1 Validate command URLs with the same `http`/`https` policy used by Web Page View. -- [x] 4.2 Verify unsupported schemes do not create or update panels. -- [x] 4.3 Verify v1 ignores or rejects layout-placement fields rather than treating them as viewport-tree commands. -- [x] 4.4 Keep implementation localized to `dimos/`; pause for design review if core viewer APIs must be changed. - -## 5. Pr formalization - -- [x] 5.1 Update PR notes or documentation text with the canonical Python/blueprint API example. -- [x] 5.2 Explicitly label the DimOS websocket command as experimental, DimOS-only, and removable. -- [x] 5.3 Document final file-scope justification: generated Web Page View blast radius belongs to the core feature; DimOS command changes stay separate and small. - -## 6. Checks - -- [x] 6.1 Run focused Rust tests for DimOS websocket command parsing/handling. -- [x] 6.2 Run relevant formatting/check commands for touched Rust files. -- [x] 6.3 Run `openspec validate "add-dimos-web-page-command" --strict --json`. diff --git a/openspec/changes/archive/2026-06-12-add-native-web-page-view/.openspec.yaml b/openspec/changes/archive/2026-06-12-add-native-web-page-view/.openspec.yaml deleted file mode 100644 index 8fe205551914..000000000000 --- a/openspec/changes/archive/2026-06-12-add-native-web-page-view/.openspec.yaml +++ /dev/null @@ -1,2 +0,0 @@ -schema: spec-driven -created: 2026-06-12 diff --git a/openspec/changes/archive/2026-06-12-add-native-web-page-view/design.md b/openspec/changes/archive/2026-06-12-add-native-web-page-view/design.md deleted file mode 100644 index 1afc66a3fd37..000000000000 --- a/openspec/changes/archive/2026-06-12-add-native-web-page-view/design.md +++ /dev/null @@ -1,92 +0,0 @@ -## Context - -Rerun's native viewer supports composable view types such as spatial, image, map, text, tensor, and dataframe views. Built-in views are registered through the viewer's view class registry, and view-specific configuration is represented as blueprint state generated from blueprint type definitions. - -There is currently URL-opening plumbing for loading Rerun data sources and opening browser URLs, but no view type that owns an embedded browser surface. The closest existing pattern is the map view: it is a registered view class with generated blueprint properties and native UI behavior. - -The Web Page View introduces a native-only view that displays a configured `http(s)` page inline in the viewer layout. It is not a visualizer for logged entities and has no timeline semantics. - -## Goals / Non-Goals - -**Goals:** - -- Add a first-class Web Page View that can be manually added to a layout and preconfigured through blueprint state. -- Store the initial/home URL and navigation chrome preference as view configuration. -- Display one live embedded native webview per Web Page View on wry-supported native platforms. -- Validate URL schemes before creating/loading the webview. -- Provide clear Rerun-side status UI for missing configuration, invalid URLs, unsupported targets, and backend creation failures. -- Shape implementation tasks as TDD vertical slices: one observable behavior test, minimal implementation, repeat. - -**Non-Goals:** - -- Supporting Web Page View in the web viewer. -- Driving the displayed URL from logged timeline data. -- Auto-spawning Web Page Views from entity contents. -- Supporting `file:`, `data:`, `javascript:`, or custom URL schemes. -- Implementing per-view isolated browser profiles in the initial version. -- Adding advanced browser options such as custom user agent, devtools configuration, zoom factor, transparent background, or script injection. - -## Decisions - -### Native-only web page view - -The Web Page View is available only in native viewer builds. The web viewer should render a clear unsupported status instead of attempting iframe support. - -**Alternatives considered:** -- Support the web viewer with iframes. Rejected because the web viewer already runs inside a browser and iframe behavior depends on cross-origin policy, CSP, and `X-Frame-Options`. -- Link-only view. Rejected because the desired behavior is an inline embedded webpage, not a shortcut to an external browser. - -### Blueprint/view configuration, not logged data - -The view's `url` and `show_navigation_controls` fields are blueprint/view configuration. The view has no data visualizer and should not be suggested from logged entities. - -**Alternatives considered:** -- Introduce a logged webpage archetype. Rejected for the initial version because webpage selection is layout configuration, not time-aware data in the requested use case. -- Support both logged and blueprint URLs. Deferred to avoid conflicting ownership of the current page. - -### Direct wry integration - -Use a native integration layer backed by `wry` rather than an egui wrapper crate. The integration should translate each view's egui rectangle into native webview bounds and manage webview lifecycle separately from egui painting. - -**Alternatives considered:** -- Third-party egui webview wrappers. Rejected as the product direction because available wrappers appear experimental and would still depend on wry/platform behavior. -- Browserless rendering or screenshots. Rejected because the view must display live interactive webpages. - -### One webview instance per view - -Each Web Page View owns one native webview instance. Multiple views therefore render independently and can show multiple live pages at once. - -**Alternatives considered:** -- Share one webview instance between views. Rejected because it conflicts with Rerun's composable layout model. -- Limit to one Web Page View per app. Rejected as an artificial limitation. - -### Keep instances alive while views exist - -The initial behavior keeps a webview alive while its Rerun view exists, including when the view is temporarily hidden. The instance is destroyed when the view is removed or the viewer exits. - -**Alternatives considered:** -- Destroy on hide. Rejected because it would reload dashboards, lose scroll position, and disrupt login/application state. - -### Browser-like navigation with stable configured URL - -The configured `url` is the initial/home URL. Runtime navigation inside the page does not mutate blueprint state. If navigation controls are visible, home returns to the configured URL. - -**Alternatives considered:** -- Lock navigation to the configured URL. Rejected because normal webpages rely on links and redirects. -- Persist every navigation into blueprint state. Rejected because casual browsing should not dirty saved layout configuration. - -### Shared browser session by default - -Web Page Views share the default embedded browser session/profile in the initial version. Per-view isolation should remain possible as a future extension. - -**Alternatives considered:** -- Isolate each view by default. Rejected because common dashboard use cases would require repeated logins and duplicate session setup. - -## Risks / trade-offs - -- Native webview surfaces may not clip, stack, or resize exactly like egui widgets → Keep the webview integration behind a narrow module boundary, update bounds from the egui view rectangle, and test split panels/tabs/resizing. -- Linux support depends on WebKitGTK/display-server details → Treat support as wry-supported native platforms, document dependencies, and render backend errors clearly. -- WebView2/WebKit runtime dependencies may be missing → Detect creation failures and show Rerun-side status UI instead of crashing. -- Shared session state is convenient but less isolated → Keep per-view profile configuration out of v1 while avoiding API choices that prevent it later. -- Embedded arbitrary webpages can consume significant CPU/memory → Keep instances alive for usability in v1; consider future suspend/destroy policies if resource usage becomes a problem. -- Native child views can complicate input focus and keyboard shortcuts → Ensure focus transfer between egui and webview is tested, especially navigation controls and viewer shortcuts. diff --git a/openspec/changes/archive/2026-06-12-add-native-web-page-view/proposal.md b/openspec/changes/archive/2026-06-12-add-native-web-page-view/proposal.md deleted file mode 100644 index 405248b2bdd8..000000000000 --- a/openspec/changes/archive/2026-06-12-add-native-web-page-view/proposal.md +++ /dev/null @@ -1,32 +0,0 @@ -## Why - -Rerun users can compose rich native viewer layouts for logged data, but cannot place a live webpage alongside 3D, image, and other views. A native inline Web Page View enables dashboards, local robot control panels, documentation, and other http(s) pages to appear inside the Rerun viewer layout instead of requiring a separate browser window. - -## What changes - -- Add a native-only **Web Page View** that displays a configured webpage inline in the viewer layout. -- Store the view's URL and lightweight browser chrome preference as blueprint/view configuration, not logged timeline data. -- Allow manual creation through the viewer UI and preconfiguration through blueprint state. -- Autoload configured `http://` and `https://` URLs, including localhost and private-network URLs. -- Reject unsupported URL schemes such as `file:`, `data:`, `javascript:`, and custom schemes with clear Rerun-side status UI. -- Use direct native webview integration for wry-supported native platforms. -- Keep runtime navigation browser-like while leaving the configured URL unchanged. -- Share embedded-browser session state by default; per-view isolated profiles remain out of scope for the initial version. -- Do not support this view in the web viewer. - -## Capabilities - -### New capabilities -- `native-web-page-view`: Defines native Web Page View behavior, configuration, platform support, URL policy, lifecycle, navigation, session handling, and error reporting. - -### Modified capabilities - -None. - -## Impact - -- Viewer view registration and manual view creation UI. -- Blueprint view definitions and generated SDK/view property types. -- Native viewer platform integration and dependency graph for embedded webviews. -- Native-only runtime lifecycle for one webview instance per Web Page View. -- Documentation and tests for blueprint configuration, URL validation, unsupported targets, and native webview lifecycle behavior. diff --git a/openspec/changes/archive/2026-06-12-add-native-web-page-view/specs/native-web-page-view/spec.md b/openspec/changes/archive/2026-06-12-add-native-web-page-view/specs/native-web-page-view/spec.md deleted file mode 100644 index d4c64178deea..000000000000 --- a/openspec/changes/archive/2026-06-12-add-native-web-page-view/specs/native-web-page-view/spec.md +++ /dev/null @@ -1,110 +0,0 @@ -## ADDED requirements - -### Requirement: configured native web page view -The system SHALL provide a native-only Web Page View that displays a configured webpage inline in the viewer layout. - -#### Scenario: native view displays configured page -- **WHEN** a native viewer layout contains a Web Page View configured with `https://example.com` -- **THEN** the viewer displays that page inline in the view area - -#### Scenario: web viewer reports unsupported view -- **WHEN** the web viewer renders a layout containing a Web Page View -- **THEN** the viewer displays a clear unsupported status for that view - -### Requirement: blueprint-owned configuration -The system SHALL store Web Page View configuration as blueprint/view state with a required URL and a `show_navigation_controls` setting. - -#### Scenario: blueprint preconfigures a web page view -- **WHEN** blueprint state contains a Web Page View with a valid URL and navigation controls setting -- **THEN** the viewer creates the view with those configured values - -#### Scenario: manual creation starts unconfigured -- **WHEN** a user manually adds a Web Page View without setting a URL -- **THEN** the viewer displays a Rerun-side status explaining that no URL is configured - -### Requirement: no data-driven spawning -The system SHALL make Web Page Views manually creatable and blueprint-preconfigurable without auto-spawning them from logged data. - -#### Scenario: logged entities do not suggest web page view -- **WHEN** the viewer computes data-driven view suggestions from logged entities -- **THEN** the suggestion set excludes Web Page View unless it was explicitly created or configured - -### Requirement: HTTP URL policy -The system SHALL validate configured Web Page View URLs and load only `http://` and `https://` schemes. - -#### Scenario: HTTPS URL loads -- **WHEN** a Web Page View is configured with `https://example.com` -- **THEN** the viewer attempts to load the URL in the native webview - -#### Scenario: local HTTP URL loads -- **WHEN** a Web Page View is configured with `http://localhost:3000` -- **THEN** the viewer attempts to load the URL in the native webview - -#### Scenario: unsupported scheme is rejected -- **WHEN** a Web Page View is configured with `file:///tmp/report.html` -- **THEN** the viewer displays a Rerun-side status explaining that only `http` and `https` URLs are supported - -### Requirement: automatic loading -The system SHALL automatically load a valid configured Web Page View URL without requiring an additional confirmation action. - -#### Scenario: valid URL autoloads -- **WHEN** a Web Page View becomes visible with a valid configured URL -- **THEN** the native webview starts loading that URL automatically - -### Requirement: per-view webview instances -The system SHALL create and manage one native webview instance for each Web Page View instance. - -#### Scenario: multiple views render independently -- **WHEN** a layout contains two Web Page Views with different valid URLs -- **THEN** the viewer maintains two independent native webview instances and displays both pages in their respective view areas - -### Requirement: webview lifecycle preserves hidden view state -The system SHALL keep a Web Page View's native webview instance alive while the Rerun view exists, including when temporarily hidden. - -#### Scenario: hidden view keeps page state -- **WHEN** a Web Page View is hidden behind a tab and later shown again -- **THEN** the viewer reuses the existing webview instance rather than reloading the configured URL from scratch - -#### Scenario: removed view destroys instance -- **WHEN** a Web Page View is removed from the layout -- **THEN** the viewer destroys the corresponding native webview instance - -### Requirement: browser-like navigation -The system SHALL allow runtime browser navigation inside the Web Page View while keeping the configured URL unchanged. - -#### Scenario: link navigation does not change blueprint URL -- **WHEN** a user clicks a link inside a Web Page View and the embedded page navigates to a different URL -- **THEN** the blueprint-configured URL remains the original configured URL - -#### Scenario: home returns to configured URL -- **WHEN** navigation controls are visible and the user activates Home after navigating away -- **THEN** the webview navigates back to the configured URL - -### Requirement: optional navigation controls -The system SHALL provide lightweight navigation controls for Web Page Views and allow them to be hidden by configuration. - -#### Scenario: navigation controls visible by default -- **WHEN** a Web Page View is configured without specifying `show_navigation_controls` -- **THEN** the viewer displays back, forward, reload, home, and URL display controls - -#### Scenario: navigation controls hidden -- **WHEN** a Web Page View is configured with `show_navigation_controls` set to false -- **THEN** the viewer hides the navigation controls and gives the webview the available view area - -### Requirement: shared embedded browser session -The system SHALL use a shared embedded browser session/profile for Web Page Views by default. - -#### Scenario: session state shared across views -- **WHEN** two Web Page Views load pages from the same origin -- **THEN** the embedded browser backend uses the shared default session/profile for both views - -### Requirement: rerun-side failure reporting -The system SHALL report configuration and backend failures using Rerun-side status UI outside the embedded webpage. - -#### Scenario: backend creation fails -- **WHEN** the native webview backend cannot create a webview instance -- **THEN** the Web Page View displays a clear Rerun-side failure message instead of crashing the viewer - -#### Scenario: invalid URL is configured -- **WHEN** the configured URL cannot be parsed as a URL -- **THEN** the Web Page View displays a clear Rerun-side invalid URL message diff --git a/openspec/changes/archive/2026-06-12-add-native-web-page-view/tasks.md b/openspec/changes/archive/2026-06-12-add-native-web-page-view/tasks.md deleted file mode 100644 index cff049a86e4a..000000000000 --- a/openspec/changes/archive/2026-06-12-add-native-web-page-view/tasks.md +++ /dev/null @@ -1,61 +0,0 @@ -## 1. Tdd tracer bullet: blueprint view skeleton - -- [x] 1.1 RED: Add one behavior test that expects a manually created Web Page View with no URL to render a Rerun-side "No URL configured" status through the public view/test harness. -- [x] 1.2 GREEN: Add the minimal `WebPageView` blueprint definition, generated code, `re_view_web_page` crate/module skeleton, view registration, and empty-URL status UI needed to pass 1.1. -- [x] 1.3 REFACTOR: Align names with existing view conventions (`Web Page View` user-facing name, `web_page`/`WebPageView` code names as appropriate) and run `pixi run rs-fmt` plus the smallest relevant Rust check. - -## 2. Tdd slice: blueprint configuration and manual creation - -- [x] 2.1 RED: Add a behavior test that creates a Web Page View from blueprint state containing `url` and `show_navigation_controls`, and verifies those values are read by the view without logged data. -- [x] 2.2 GREEN: Implement blueprint properties for required URL and defaulted `show_navigation_controls = true`; ensure manual creation works and data-driven spawn heuristics do not suggest the view. -- [x] 2.3 REFACTOR: Keep configuration access isolated behind a small typed helper so later backend code does not parse blueprint state directly. - -## 3. Tdd slice: URL policy and status errors - -- [x] 3.1 RED: Add behavior tests for accepted `https://example.com`, accepted `http://localhost:3000`, rejected `file:///tmp/report.html`, and invalid URL text. -- [x] 3.2 GREEN: Implement URL parsing/validation that allows only `http` and `https`, including localhost/private-network HTTP, and renders Rerun-side status messages for invalid or unsupported URLs. -- [x] 3.3 REFACTOR: Move URL policy into a backend-independent unit with focused tests so native webview code can trust validated URLs. - -## 4. Tdd slice: native webview backend seam - -- [x] 4.1 RED: Add tests using a fake backend that verify a valid configured URL causes exactly one backend webview instance to be created for one Web Page View. -- [x] 4.2 GREEN: Introduce a narrow native webview backend abstraction and wire the view to it with a fake/test backend; do not depend on `wry` in behavior tests. -- [x] 4.3 RED: Add a behavior test that two Web Page Views with different URLs create two independent backend instances. -- [x] 4.4 GREEN: Implement per-view instance ownership keyed by stable view identity. -- [x] 4.5 REFACTOR: Keep egui/view logic, lifecycle manager, and platform backend responsibilities separate. - -## 5. Tdd slice: direct wry integration - -- [x] 5.1 RED: Add a compile-gated native integration test or smoke test target that exercises construction through the real native backend boundary and reports backend creation failure instead of panicking. -- [x] 5.2 GREEN: Add direct `wry` integration for native builds, including required Cargo feature/dependency wiring and platform-specific creation paths supported by wry. -- [x] 5.3 REFACTOR: Encapsulate platform-specific wry details behind the backend boundary, including Linux WebKitGTK/X11/Wayland caveats and bounds-setting differences. - -## 6. Tdd slice: layout bounds and lifecycle - -- [x] 6.1 RED: Add fake-backend behavior tests that verify the backend receives updated bounds when the egui view rectangle changes. -- [x] 6.2 GREEN: Update native webview bounds from the allocated egui view rectangle each frame, accounting for DPI/points-to-pixels conversion. -- [x] 6.3 RED: Add fake-backend behavior tests that verify hiding a view keeps its instance alive and removing a view destroys its instance. -- [x] 6.4 GREEN: Implement keep-alive-while-hidden lifecycle and destroy-on-remove/app-exit cleanup. -- [x] 6.5 REFACTOR: Audit focus, clipping, tab, split-panel, and resize behavior with the fake backend before manual native smoke testing. - -## 7. Tdd slice: navigation controls and runtime navigation - -- [x] 7.1 RED: Add behavior tests that navigation controls are visible by default and hidden when `show_navigation_controls` is false. -- [x] 7.2 GREEN: Implement lightweight back, forward, reload, home, and URL display controls above the embedded webview, reserving the full view area when controls are hidden. -- [x] 7.3 RED: Add a behavior test that runtime navigation does not mutate the blueprint-configured URL and that Home navigates back to the configured URL. -- [x] 7.4 GREEN: Wire navigation commands through the backend abstraction while keeping configured URL state immutable during runtime navigation. - -## 8. Tdd slice: Platform support, session defaults, and failure UI - -- [x] 8.1 RED: Add behavior tests that web builds or unavailable native backends render explicit unsupported/failure status UI. -- [x] 8.2 GREEN: Implement unsupported-target and backend-failure reporting outside the webview surface. -- [x] 8.3 RED: Add a fake-backend behavior test that multiple views use the shared default browser profile/session configuration. -- [x] 8.4 GREEN: Implement shared default session/profile behavior in the backend boundary while leaving per-view isolation as a future extension point. - -## 9. Verification and documentation - -- [x] 9.1 Run `pixi run codegen` after blueprint definition changes and verify generated Rust/Python/C++ outputs are updated as expected. -- [x] 9.2 Run `pixi run rs-fmt` after Rust changes. -- [x] 9.3 Run targeted Rust checks/tests for the new view crate and affected viewer crates with `cargo clippy -p ` and `cargo nextest run --all-features --no-fail-fast -p ` (`cargo nextest` was unavailable in this environment; used focused `cargo test` plus clippy coverage instead). -- [x] 9.4 Manually smoke-test native Web Page View creation, URL editing, split/tab resizing, navigation controls, multiple simultaneous views, hidden-tab preservation, and backend failure messaging on at least one supported native platform. -- [x] 9.5 Document native platform/runtime requirements, including WebView2/WebKitGTK expectations and the fact that the Web Page View is unsupported in the web viewer. diff --git a/openspec/config.yaml b/openspec/config.yaml deleted file mode 100644 index 392946c67c03..000000000000 --- a/openspec/config.yaml +++ /dev/null @@ -1,20 +0,0 @@ -schema: spec-driven - -# Project context (optional) -# This is shown to AI when creating artifacts. -# Add your tech stack, conventions, style guides, domain knowledge, etc. -# Example: -# context: | -# Tech stack: TypeScript, React, Node.js -# We use conventional commits -# Domain: e-commerce platform - -# Per-artifact rules (optional) -# Add custom rules for specific artifacts. -# Example: -# rules: -# proposal: -# - Keep proposals under 500 words -# - Always include a "Non-goals" section -# tasks: -# - Break tasks into chunks of max 2 hours diff --git a/openspec/specs/native-web-page-view/spec.md b/openspec/specs/native-web-page-view/spec.md deleted file mode 100644 index 98c005a23914..000000000000 --- a/openspec/specs/native-web-page-view/spec.md +++ /dev/null @@ -1,144 +0,0 @@ -## Purpose - -Native Web Page View lets native Rerun viewer layouts embed configured `http(s)` webpages inline alongside other view types. - -## Requirements - -### Requirement: configured native web page view - -The system SHALL provide a native-only Web Page View that displays a configured webpage inline in the viewer layout. - -#### Scenario: native view displays configured page - -- **WHEN** a native viewer layout contains a Web Page View configured with `https://example.com` -- **THEN** the viewer displays that page inline in the view area - -#### Scenario: web viewer reports unsupported view - -- **WHEN** the web viewer renders a layout containing a Web Page View -- **THEN** the viewer displays a clear unsupported status for that view - -### Requirement: blueprint-owned configuration - -The system SHALL store Web Page View configuration as blueprint/view state with a required URL and a `show_navigation_controls` setting. - -#### Scenario: blueprint preconfigures a web page view - -- **WHEN** blueprint state contains a Web Page View with a valid URL and navigation controls setting -- **THEN** the viewer creates the view with those configured values - -#### Scenario: manual creation starts unconfigured - -- **WHEN** a user manually adds a Web Page View without setting a URL -- **THEN** the viewer displays a Rerun-side status explaining that no URL is configured - -### Requirement: no data-driven spawning - -The system SHALL make Web Page Views manually creatable and blueprint-preconfigurable without auto-spawning them from logged data. - -#### Scenario: logged entities do not suggest web page view - -- **WHEN** the viewer computes data-driven view suggestions from logged entities -- **THEN** the suggestion set excludes Web Page View unless it was explicitly created or configured - -### Requirement: HTTP URL policy - -The system SHALL validate configured Web Page View URLs and load only `http://` and `https://` schemes. - -#### Scenario: HTTPS URL loads - -- **WHEN** a Web Page View is configured with `https://example.com` -- **THEN** the viewer attempts to load the URL in the native webview - -#### Scenario: local HTTP URL loads - -- **WHEN** a Web Page View is configured with `http://localhost:3000` -- **THEN** the viewer attempts to load the URL in the native webview - -#### Scenario: unsupported scheme is rejected - -- **WHEN** a Web Page View is configured with `file:///tmp/report.html` -- **THEN** the viewer displays a Rerun-side status explaining that only `http` and `https` URLs are supported - -### Requirement: automatic loading - -The system SHALL automatically load a valid configured Web Page View URL without requiring an additional confirmation action. - -#### Scenario: valid URL autoloads - -- **WHEN** a Web Page View becomes visible with a valid configured URL -- **THEN** the native webview starts loading that URL automatically - -### Requirement: per-view webview instances - -The system SHALL create and manage one native webview instance for each Web Page View instance. - -#### Scenario: multiple views render independently - -- **WHEN** a layout contains two Web Page Views with different valid URLs -- **THEN** the viewer maintains two independent native webview instances and displays both pages in their respective view areas - -### Requirement: webview lifecycle preserves hidden view state - -The system SHALL keep a Web Page View's native webview instance alive while the Rerun view exists, including when temporarily hidden. - -#### Scenario: hidden view keeps page state - -- **WHEN** a Web Page View is hidden behind a tab and later shown again -- **THEN** the viewer reuses the existing webview instance rather than reloading the configured URL from scratch - -#### Scenario: removed view destroys instance - -- **WHEN** a Web Page View is removed from the layout -- **THEN** the viewer destroys the corresponding native webview instance - -### Requirement: browser-like navigation - -The system SHALL allow runtime browser navigation inside the Web Page View while keeping the configured URL unchanged. - -#### Scenario: link navigation does not change blueprint URL - -- **WHEN** a user clicks a link inside a Web Page View and the embedded page navigates to a different URL -- **THEN** the blueprint-configured URL remains the original configured URL - -#### Scenario: home returns to configured URL - -- **WHEN** navigation controls are visible and the user activates Home after navigating away -- **THEN** the webview navigates back to the configured URL - -### Requirement: optional navigation controls - -The system SHALL provide lightweight navigation controls for Web Page Views and allow them to be hidden by configuration. - -#### Scenario: navigation controls visible by default - -- **WHEN** a Web Page View is configured without specifying `show_navigation_controls` -- **THEN** the viewer displays back, forward, reload, home, and URL display controls - -#### Scenario: navigation controls hidden - -- **WHEN** a Web Page View is configured with `show_navigation_controls` set to false -- **THEN** the viewer hides the navigation controls and gives the webview the available view area - -### Requirement: shared embedded browser session - -The system SHALL use a shared embedded browser session/profile for Web Page Views by default. - -#### Scenario: session state shared across views - -- **WHEN** two Web Page Views load pages from the same origin -- **THEN** the embedded browser backend uses the shared default session/profile for both views - -### Requirement: rerun-side failure reporting - -The system SHALL report configuration and backend failures using Rerun-side status UI outside the embedded webpage. - -#### Scenario: backend creation fails - -- **WHEN** the native webview backend cannot create a webview instance -- **THEN** the Web Page View displays a clear Rerun-side failure message instead of crashing the viewer - -#### Scenario: invalid URL is configured - -- **WHEN** the configured URL cannot be parsed as a URL -- **THEN** the Web Page View displays a clear Rerun-side invalid URL message From fa0f2513f28155a1d29ef7f9803d32f2fdec345b Mon Sep 17 00:00:00 2001 From: cc Date: Tue, 16 Jun 2026 16:00:37 -0700 Subject: [PATCH 10/12] fix: address web page review comments --- Cargo.lock | 1 + crates/viewer/re_view_web_page/src/backend.rs | 15 ---- crates/viewer/re_view_web_page/src/lib.rs | 4 ++ .../viewer/re_view_web_page/src/lifecycle.rs | 2 + .../src/native_backend_stub.rs | 56 +++++++++++++++ crates/viewer/re_view_web_page/src/testing.rs | 18 +++-- .../re_view_web_page/tests/web_page_view.rs | 71 +++++++++++++++++++ dimos/Cargo.toml | 1 + dimos/src/interaction/handle.rs | 2 +- dimos/src/interaction/keyboard.rs | 4 +- dimos/src/interaction/ws.rs | 22 +++--- dimos/src/viewer.rs | 8 +-- 12 files changed, 165 insertions(+), 39 deletions(-) create mode 100644 crates/viewer/re_view_web_page/src/native_backend_stub.rs diff --git a/Cargo.lock b/Cargo.lock index dd3d20185f39..1c335e180dcc 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3167,6 +3167,7 @@ dependencies = [ "futures-util", "mimalloc", "parking_lot", + "re_log", "rerun", "serde", "serde_json", diff --git a/crates/viewer/re_view_web_page/src/backend.rs b/crates/viewer/re_view_web_page/src/backend.rs index 8c7441e19ef3..bb7c86df9c8c 100644 --- a/crates/viewer/re_view_web_page/src/backend.rs +++ b/crates/viewer/re_view_web_page/src/backend.rs @@ -5,7 +5,6 @@ pub(crate) struct WebViewInstance { pub(crate) url: String, #[cfg(debug_assertions)] fake_backend: Option, - #[cfg(all(not(target_arch = "wasm32"), feature = "native_webview"))] has_native_webview: bool, } @@ -46,12 +45,10 @@ impl WebViewInstance { view_id, url, fake_backend: Some(fake_backend), - #[cfg(all(not(target_arch = "wasm32"), feature = "native_webview"))] has_native_webview: false, } } - #[cfg(all(not(target_arch = "wasm32"), feature = "native_webview"))] pub(crate) fn new_native(view_id: ViewId, url: String) -> Self { Self { view_id, @@ -68,18 +65,12 @@ impl WebViewInstance { fake_backend.record_bounds_update(view_id, bounds); } - #[cfg(all(not(target_arch = "wasm32"), feature = "native_webview"))] if self.has_native_webview { crate::native_backend::set_bounds(self.view_id, bounds); } } pub(crate) fn set_visible(&self, visible: bool) { - #[cfg(not(all(not(target_arch = "wasm32"), feature = "native_webview")))] - let _ = self; - let _ = visible; - - #[cfg(all(not(target_arch = "wasm32"), feature = "native_webview"))] if self.has_native_webview { crate::native_backend::set_visible(self.view_id, visible); } @@ -91,7 +82,6 @@ impl WebViewInstance { fake_backend.record_navigation_command(self.view_id, FakeNavigationCommand::Back); } - #[cfg(all(not(target_arch = "wasm32"), feature = "native_webview"))] if self.has_native_webview { crate::native_backend::go_back(self.view_id); } @@ -103,7 +93,6 @@ impl WebViewInstance { fake_backend.record_navigation_command(self.view_id, FakeNavigationCommand::Forward); } - #[cfg(all(not(target_arch = "wasm32"), feature = "native_webview"))] if self.has_native_webview { crate::native_backend::go_forward(self.view_id); } @@ -115,7 +104,6 @@ impl WebViewInstance { fake_backend.record_navigation_command(self.view_id, FakeNavigationCommand::Reload); } - #[cfg(all(not(target_arch = "wasm32"), feature = "native_webview"))] if self.has_native_webview { crate::native_backend::reload(self.view_id); } @@ -130,7 +118,6 @@ impl WebViewInstance { ); } - #[cfg(all(not(target_arch = "wasm32"), feature = "native_webview"))] if self.has_native_webview { crate::native_backend::navigate_to(self.view_id, url); } @@ -147,7 +134,6 @@ impl Drop for WebViewInstance { fake_backend.record_destroyed_instance(self.view_id, &self.url); } - #[cfg(all(not(target_arch = "wasm32"), feature = "native_webview"))] if self.has_native_webview { crate::native_backend::destroy(self.view_id); } @@ -210,7 +196,6 @@ pub(crate) fn create_webview( .map(Some); } - #[cfg(all(not(target_arch = "wasm32"), feature = "native_webview"))] if crate::native_backend::has_native_parent_window() { let native_webview = crate::native_backend::NativeWebViewBackend .create_child(url, bounds) diff --git a/crates/viewer/re_view_web_page/src/lib.rs b/crates/viewer/re_view_web_page/src/lib.rs index 970bb249cea8..e3986e33bcfe 100644 --- a/crates/viewer/re_view_web_page/src/lib.rs +++ b/crates/viewer/re_view_web_page/src/lib.rs @@ -3,6 +3,10 @@ mod backend; mod lifecycle; #[cfg(all(not(target_arch = "wasm32"), feature = "native_webview"))] +#[path = "native_backend.rs"] +pub mod native_backend; +#[cfg(not(all(not(target_arch = "wasm32"), feature = "native_webview")))] +#[path = "native_backend_stub.rs"] pub mod native_backend; #[cfg(debug_assertions)] pub mod testing; diff --git a/crates/viewer/re_view_web_page/src/lifecycle.rs b/crates/viewer/re_view_web_page/src/lifecycle.rs index 133e384af4af..b89e6b1810be 100644 --- a/crates/viewer/re_view_web_page/src/lifecycle.rs +++ b/crates/viewer/re_view_web_page/src/lifecycle.rs @@ -21,6 +21,8 @@ impl WebViewLifecycle { .as_ref() .is_none_or(|webview| webview.url != url) { + self.webview = None; + match create_webview(ctx, view_id, url, bounds) { Ok(Some(webview)) => { self.webview = Some(webview); diff --git a/crates/viewer/re_view_web_page/src/native_backend_stub.rs b/crates/viewer/re_view_web_page/src/native_backend_stub.rs new file mode 100644 index 000000000000..aab7ff0fe04a --- /dev/null +++ b/crates/viewer/re_view_web_page/src/native_backend_stub.rs @@ -0,0 +1,56 @@ +//! No-op native backend used when embedded native webviews are not compiled in. + +use re_viewer_context::ViewId; + +use crate::backend::WebViewBounds; + +#[derive(Debug, Default)] +pub struct NativeWebViewBackend; + +pub struct NativeWebView; + +#[derive(Debug)] +pub enum NativeWebViewError { + Unavailable, +} + +impl std::fmt::Display for NativeWebViewError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::Unavailable => f.write_str("native webview support is not available"), + } + } +} + +impl std::error::Error for NativeWebViewError {} + +impl NativeWebViewBackend { + pub(crate) fn create_child( + &self, + _url: &str, + _bounds: WebViewBounds, + ) -> Result { + let _ = self; + Err(NativeWebViewError::Unavailable) + } +} + +pub(crate) fn has_native_parent_window() -> bool { + false +} + +pub(crate) fn insert(_view_id: ViewId, _webview: NativeWebView) {} + +pub(crate) fn destroy(_view_id: ViewId) {} + +pub(crate) fn set_bounds(_view_id: ViewId, _bounds: WebViewBounds) {} + +pub(crate) fn set_visible(_view_id: ViewId, _visible: bool) {} + +pub(crate) fn go_back(_view_id: ViewId) {} + +pub(crate) fn go_forward(_view_id: ViewId) {} + +pub(crate) fn reload(_view_id: ViewId) {} + +pub(crate) fn navigate_to(_view_id: ViewId, _url: &str) {} diff --git a/crates/viewer/re_view_web_page/src/testing.rs b/crates/viewer/re_view_web_page/src/testing.rs index 879eeab5e8d7..950b103bd625 100644 --- a/crates/viewer/re_view_web_page/src/testing.rs +++ b/crates/viewer/re_view_web_page/src/testing.rs @@ -140,6 +140,10 @@ impl FakeWebViewBackend { .insert(view_id, url.to_owned()); } + pub fn current_url(&self, view_id: ViewId) -> Option { + self.state.lock().current_urls.get(&view_id).cloned() + } + pub fn navigation_requests(&self) -> Vec { self.state .lock() @@ -165,13 +169,12 @@ impl FakeWebViewBackend { } pub(crate) fn record_destroyed_instance(&self, view_id: ViewId, url: &str) { - self.state - .lock() - .destroyed_instances - .push(FakeDestroyedWebView { - view_id, - url: url.to_owned(), - }); + let mut state = self.state.lock(); + state.current_urls.remove(&view_id); + state.destroyed_instances.push(FakeDestroyedWebView { + view_id, + url: url.to_owned(), + }); } pub(crate) fn record_navigation_command( @@ -220,6 +223,7 @@ impl WebViewBackend for FakeWebViewBackend { url: url.to_owned(), session: session.as_str().to_owned(), }); + state.current_urls.insert(view_id, url.to_owned()); Ok(WebViewInstance::new_fake( view_id, diff --git a/crates/viewer/re_view_web_page/tests/web_page_view.rs b/crates/viewer/re_view_web_page/tests/web_page_view.rs index 68a5d4a61eb9..eefbd8faccfb 100644 --- a/crates/viewer/re_view_web_page/tests/web_page_view.rs +++ b/crates/viewer/re_view_web_page/tests/web_page_view.rs @@ -330,6 +330,57 @@ fn valid_configured_url_creates_one_backend_webview_instance() { assert_eq!(fake_backend.created_urls(), ["https://example.com"]); } +#[test] +fn changed_configured_url_replaces_backend_webview_without_destroying_replacement() { + let fake_backend = FakeWebViewBackend::default(); + let _backend_guard = fake_backend.install(); + + let mut test_context = TestContext::new_with_view_class::(); + let view_id = setup_configured_web_page_view(&mut test_context, "https://example.com/a", true); + + { + let mut harness = test_context + .setup_kittest_for_rendering_ui([500.0, 250.0]) + .build_ui(|ui| { + test_context.run_with_single_view(ui, view_id); + }); + harness.run(); + } + + assert_eq!(fake_backend.created_urls(), ["https://example.com/a"]); + assert_eq!( + fake_backend.current_url(view_id).as_deref(), + Some("https://example.com/a") + ); + + update_configured_web_page_view(&mut test_context, view_id, "https://example.com/b", true); + + { + let mut harness = test_context + .setup_kittest_for_rendering_ui([500.0, 250.0]) + .build_ui(|ui| { + test_context.run_with_single_view(ui, view_id); + }); + harness.run(); + } + + let created_instances = fake_backend.created_instances(); + assert_eq!(created_instances.len(), 2); + assert_eq!(created_instances[0].view_id, view_id); + assert_eq!(created_instances[0].url, "https://example.com/a"); + assert_eq!(created_instances[1].view_id, view_id); + assert_eq!(created_instances[1].url, "https://example.com/b"); + + let destroyed_instances = fake_backend.destroyed_instances(); + assert_eq!(destroyed_instances.len(), 1); + assert_eq!(destroyed_instances[0].view_id, view_id); + assert_eq!(destroyed_instances[0].url, "https://example.com/a"); + assert_eq!( + fake_backend.current_url(view_id).as_deref(), + Some("https://example.com/b") + ); +} + #[test] fn two_web_page_views_create_independent_backend_webview_instances() { let fake_backend = FakeWebViewBackend::default(); @@ -510,6 +561,26 @@ fn setup_web_page_view_with_default_navigation_controls( }) } +fn update_configured_web_page_view( + test_context: &mut TestContext, + view_id: re_viewer_context::ViewId, + url: &str, + show_navigation_controls: bool, +) { + test_context.setup_viewport_blueprint(|ctx, _blueprint| { + let config = ViewProperty::from_archetype::( + ctx.blueprint_db(), + ctx.blueprint_query, + view_id, + ); + + ctx.save_blueprint_archetype( + config.blueprint_store_path, + &WebPageViewConfig::new(url).with_show_navigation_controls(show_navigation_controls), + ); + }); +} + fn add_configured_web_page_view( ctx: &ViewerContext<'_>, blueprint: &mut ViewportBlueprint, diff --git a/dimos/Cargo.toml b/dimos/Cargo.toml index 2e6bd33cf613..c73a03b57116 100644 --- a/dimos/Cargo.toml +++ b/dimos/Cargo.toml @@ -31,6 +31,7 @@ clap.workspace = true futures-util.workspace = true mimalloc.workspace = true parking_lot.workspace = true +re_log = { workspace = true, features = ["setup"] } serde = { workspace = true, features = ["derive"] } serde_json.workspace = true tokio = { workspace = true, features = [ diff --git a/dimos/src/interaction/handle.rs b/dimos/src/interaction/handle.rs index 8b986c9d1e8d..65e25a007d6c 100644 --- a/dimos/src/interaction/handle.rs +++ b/dimos/src/interaction/handle.rs @@ -35,7 +35,7 @@ impl InteractionHandle { }; if let Err(err) = self.tx.send(event) { - eprintln!("Failed to send click event: {err}"); + re_log::warn!("Failed to send click event: {err}"); } } } diff --git a/dimos/src/interaction/keyboard.rs b/dimos/src/interaction/keyboard.rs index a72f95942e6e..236b3f4e5eae 100644 --- a/dimos/src/interaction/keyboard.rs +++ b/dimos/src/interaction/keyboard.rs @@ -320,7 +320,7 @@ impl KeyboardHandler { .send_twist(lin_x, lin_y, lin_z, ang_x, ang_y, ang_z)?; if std::env::var("DIMOS_DEBUG").is_ok_and(|v| v == "1") { - eprintln!( + re_log::debug!( "[DIMOS_DEBUG] Published twist: lin=({lin_x:.2},{lin_y:.2},{lin_z:.2}) ang=({ang_x:.2},{ang_y:.2},{ang_z:.2})" ); } @@ -331,7 +331,7 @@ impl KeyboardHandler { fn publish_stop(&self) -> Result<(), super::ws::SendError> { self.ws.send_stop()?; if std::env::var("DIMOS_DEBUG").is_ok_and(|v| v == "1") { - eprintln!("[DIMOS_DEBUG] Published stop command"); + re_log::debug!("[DIMOS_DEBUG] Published stop command"); } Ok(()) } diff --git a/dimos/src/interaction/ws.rs b/dimos/src/interaction/ws.rs index 002ea3544868..de1530d3ad02 100644 --- a/dimos/src/interaction/ws.rs +++ b/dimos/src/interaction/ws.rs @@ -250,13 +250,13 @@ async fn run_client( loop { if debug { - eprintln!("[DIMOS_DEBUG] WsPublisher: connecting to {url}"); + re_log::debug!("[DIMOS_DEBUG] WsPublisher: connecting to {url}"); } match connect_async(&url).await { Ok((ws_stream, _)) => { if debug { - eprintln!("[DIMOS_DEBUG] WsPublisher: connected to {url}"); + re_log::debug!("[DIMOS_DEBUG] WsPublisher: connected to {url}"); } let (mut writer, mut reader) = ws_stream.split(); @@ -274,14 +274,14 @@ async fn run_client( if let Err(err) = command_tx_read.try_send(command) && debug_read { - eprintln!( + re_log::warn!( "[DIMOS_DEBUG] WsPublisher: inbound command dropped: {err}" ); } } Err(err) => { if debug_read { - eprintln!( + re_log::debug!( "[DIMOS_DEBUG] WsPublisher: ignoring inbound message: {err}" ); } @@ -289,13 +289,15 @@ async fn run_client( }, Ok(Message::Close(_)) => { if debug_read { - eprintln!("[DIMOS_DEBUG] WsPublisher: server sent close frame"); + re_log::debug!( + "[DIMOS_DEBUG] WsPublisher: server sent close frame" + ); } break; } Err(err) => { if debug_read { - eprintln!("[DIMOS_DEBUG] WsPublisher: read error: {err}"); + re_log::warn!("[DIMOS_DEBUG] WsPublisher: read error: {err}"); } break; } @@ -312,7 +314,7 @@ async fn run_client( Some(text) => { if let Err(err) = writer.send(Message::text(text)).await { if debug { - eprintln!("[DIMOS_DEBUG] WsPublisher: send error: {err} — reconnecting"); + re_log::warn!("[DIMOS_DEBUG] WsPublisher: send error: {err} — reconnecting"); } break false; } @@ -323,7 +325,7 @@ async fn run_client( _ = &mut read_handle => { // Reader exited → server closed the connection. if debug { - eprintln!("[DIMOS_DEBUG] WsPublisher: server closed connection — reconnecting"); + re_log::debug!("[DIMOS_DEBUG] WsPublisher: server closed connection — reconnecting"); } break false; } @@ -332,14 +334,14 @@ async fn run_client( if disconnected { if debug { - eprintln!("[DIMOS_DEBUG] WsPublisher: channel closed, shutting down"); + re_log::debug!("[DIMOS_DEBUG] WsPublisher: channel closed, shutting down"); } break; } } Err(err) => { if debug { - eprintln!( + re_log::warn!( "[DIMOS_DEBUG] WsPublisher: connection failed: {err} — retrying in 1s" ); } diff --git a/dimos/src/viewer.rs b/dimos/src/viewer.rs index 868c8efa30c8..821f76416de8 100644 --- a/dimos/src/viewer.rs +++ b/dimos/src/viewer.rs @@ -121,7 +121,7 @@ fn main() -> Result<(), Box> { // Connect WebSocket publisher for click/keyboard events let ws_publisher = WsPublisher::connect(ws_url.clone()); if debug { - eprintln!("[DIMOS_DEBUG] WebSocket client target: {ws_url}"); + re_log::debug!("[DIMOS_DEBUG] WebSocket client target: {ws_url}"); } let keyboard_handler_ws = ws_publisher.clone(); @@ -204,14 +204,14 @@ fn main() -> Result<(), Box> { if debug { if let Some(ref connect) = parsed.connect { match connect.as_deref() { - Some(url) => eprintln!("[DIMOS_DEBUG] gRPC connecting to: {url}"), - None => eprintln!( + Some(url) => re_log::debug!("[DIMOS_DEBUG] gRPC connecting to: {url}"), + None => re_log::debug!( "[DIMOS_DEBUG] gRPC connecting to default (port {})", parsed.port ), } } else { - eprintln!( + re_log::debug!( "[DIMOS_DEBUG] gRPC: starting local server on port {}", parsed.port ); From 025aa78d952d0f4af691cc2cd9739c73e6089d52 Mon Sep 17 00:00:00 2001 From: cc Date: Tue, 16 Jun 2026 16:28:29 -0700 Subject: [PATCH 11/12] fix: navigate existing web page view on URL changes --- .../viewer/re_view_web_page/src/lifecycle.rs | 23 +++++++------------ .../re_view_web_page/tests/web_page_view.rs | 16 ++++++------- 2 files changed, 16 insertions(+), 23 deletions(-) diff --git a/crates/viewer/re_view_web_page/src/lifecycle.rs b/crates/viewer/re_view_web_page/src/lifecycle.rs index b89e6b1810be..4c9d541b48a0 100644 --- a/crates/viewer/re_view_web_page/src/lifecycle.rs +++ b/crates/viewer/re_view_web_page/src/lifecycle.rs @@ -16,26 +16,19 @@ impl WebViewLifecycle { url: &str, bounds: WebViewBounds, ) -> WebViewLifecycleStatus { - if self - .webview - .as_ref() - .is_none_or(|webview| webview.url != url) - { - self.webview = None; - + if let Some(webview) = &mut self.webview { + if webview.url != url { + webview.navigate_to(url); + webview.url = url.to_owned(); + } + } else { match create_webview(ctx, view_id, url, bounds) { Ok(Some(webview)) => { self.webview = Some(webview); self.last_bounds = None; } - Ok(None) => { - self.webview = None; - return WebViewLifecycleStatus::Unavailable; - } - Err(err) => { - self.webview = None; - return WebViewLifecycleStatus::CreationFailed(err.to_string()); - } + Ok(None) => return WebViewLifecycleStatus::Unavailable, + Err(err) => return WebViewLifecycleStatus::CreationFailed(err.to_string()), } } diff --git a/crates/viewer/re_view_web_page/tests/web_page_view.rs b/crates/viewer/re_view_web_page/tests/web_page_view.rs index eefbd8faccfb..6999dd8e02ba 100644 --- a/crates/viewer/re_view_web_page/tests/web_page_view.rs +++ b/crates/viewer/re_view_web_page/tests/web_page_view.rs @@ -331,7 +331,7 @@ fn valid_configured_url_creates_one_backend_webview_instance() { } #[test] -fn changed_configured_url_replaces_backend_webview_without_destroying_replacement() { +fn changed_configured_url_navigates_existing_backend_webview() { let fake_backend = FakeWebViewBackend::default(); let _backend_guard = fake_backend.install(); @@ -365,16 +365,16 @@ fn changed_configured_url_replaces_backend_webview_without_destroying_replacemen } let created_instances = fake_backend.created_instances(); - assert_eq!(created_instances.len(), 2); + assert_eq!(created_instances.len(), 1); assert_eq!(created_instances[0].view_id, view_id); assert_eq!(created_instances[0].url, "https://example.com/a"); - assert_eq!(created_instances[1].view_id, view_id); - assert_eq!(created_instances[1].url, "https://example.com/b"); - let destroyed_instances = fake_backend.destroyed_instances(); - assert_eq!(destroyed_instances.len(), 1); - assert_eq!(destroyed_instances[0].view_id, view_id); - assert_eq!(destroyed_instances[0].url, "https://example.com/a"); + assert_eq!(fake_backend.destroyed_instance_count(), 0); + + let navigation_requests = fake_backend.navigation_requests(); + assert_eq!(navigation_requests.len(), 1); + assert_eq!(navigation_requests[0].view_id, view_id); + assert_eq!(navigation_requests[0].url, "https://example.com/b"); assert_eq!( fake_backend.current_url(view_id).as_deref(), Some("https://example.com/b") From ecc08d796329603ee813daa87b149be8d7794df3 Mon Sep 17 00:00:00 2001 From: Jeff Hykin Date: Sun, 21 Jun 2026 21:04:26 +0800 Subject: [PATCH 12/12] - --- .vscode/settings.json | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/.vscode/settings.json b/.vscode/settings.json index f3c08cc8a76a..2645e3d2cfdb 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -65,4 +65,13 @@ "[yaml]": { "editor.defaultFormatter": "esbenp.prettier-vscode" }, + "workbench.colorCustomizations": { + "sash.hoverBorder": "#fa1b49", + "statusBar.background": "#dd0531", + "statusBar.foreground": "#e7e7e7", + "statusBarItem.hoverBackground": "#fa1b49", + "statusBarItem.remoteBackground": "#dd0531", + "statusBarItem.remoteForeground": "#e7e7e7" + }, + "peacock.color": "#dd0531", }