From 9252b8f58fafbc953076e809c80834b5c5e51f86 Mon Sep 17 00:00:00 2001 From: Ralph Kuepper Date: Sun, 21 Jun 2026 13:48:58 +0200 Subject: [PATCH 1/4] =?UTF-8?q?feat(ui):=20BloomView=20=E2=80=94=20embed?= =?UTF-8?q?=20an=20external=20GPU=20renderer=20(Bloom=20engine)=20inside?= =?UTF-8?q?=20a=20Perry=20UI=20app=20(#2395)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds a `BloomView(width, height)` Perry UI widget (Windows) that reserves a native child window in the view tree and exposes its raw HWND via `bloomViewGetHwnd(view)`. An external renderer such as the Bloom engine then builds its surface on that window and drives frames from the host's run loop. perry-ui-windows itself never links or references Bloom — it only owns the window and hands out the handle, so apps that don't use BloomView pull in nothing extra. The Bloom-side embedding (`attachToHwnd`/`resize` FFI) lives in the engine repo. - widget: crates/perry-ui-windows/src/widgets/bloomview.rs (+ widgets/mod.rs, ffi/widget_create.rs) - table-driven like Canvas, no codegen special-case: perry-dispatch ui_table rows + perry-api-manifest entries (BloomView, bloomViewGetHwnd) - types: BloomView/bloomViewGetHwnd (+ onFrame/cancelFrame) in perry/ui d.ts Windows-only for now; codegen/manifest/dispatch are shared, so wiring other platform UI crates is an additive follow-up. Per request, no version bump or CHANGELOG entry — maintainer to add at merge if desired. Claude-Session: https://claude.ai/code/session_011z6cNYoSsNG4ffLYLXYRwc --- Cargo.lock | 152 +++++++++--------- crates/perry-api-manifest/src/entries.rs | 3 + crates/perry-dispatch/src/ui_table.rs | 16 ++ .../perry-ui-windows/src/ffi/widget_create.rs | 15 ++ .../perry-ui-windows/src/widgets/bloomview.rs | 133 +++++++++++++++ crates/perry-ui-windows/src/widgets/mod.rs | 1 + types/perry/ui/index.d.ts | 24 +++ 7 files changed, 268 insertions(+), 76 deletions(-) create mode 100644 crates/perry-ui-windows/src/widgets/bloomview.rs diff --git a/Cargo.lock b/Cargo.lock index 1504a5e998..69a5d90ce0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5325,7 +5325,7 @@ checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" [[package]] name = "perry" -version = "0.5.1197" +version = "0.5.1198" dependencies = [ "anyhow", "base64", @@ -5382,14 +5382,14 @@ dependencies = [ [[package]] name = "perry-api-manifest" -version = "0.5.1197" +version = "0.5.1198" dependencies = [ "serde", ] [[package]] name = "perry-audio-miniaudio" -version = "0.5.1197" +version = "0.5.1198" dependencies = [ "cc", "libc", @@ -5397,7 +5397,7 @@ dependencies = [ [[package]] name = "perry-codegen" -version = "0.5.1197" +version = "0.5.1198" dependencies = [ "anyhow", "log", @@ -5412,7 +5412,7 @@ dependencies = [ [[package]] name = "perry-codegen-arkts" -version = "0.5.1197" +version = "0.5.1198" dependencies = [ "anyhow", "perry-hir", @@ -5421,7 +5421,7 @@ dependencies = [ [[package]] name = "perry-codegen-glance" -version = "0.5.1197" +version = "0.5.1198" dependencies = [ "anyhow", "perry-hir", @@ -5429,7 +5429,7 @@ dependencies = [ [[package]] name = "perry-codegen-js" -version = "0.5.1197" +version = "0.5.1198" dependencies = [ "anyhow", "perry-dispatch", @@ -5439,7 +5439,7 @@ dependencies = [ [[package]] name = "perry-codegen-swiftui" -version = "0.5.1197" +version = "0.5.1198" dependencies = [ "anyhow", "perry-hir", @@ -5448,7 +5448,7 @@ dependencies = [ [[package]] name = "perry-codegen-wasm" -version = "0.5.1197" +version = "0.5.1198" dependencies = [ "anyhow", "base64", @@ -5461,7 +5461,7 @@ dependencies = [ [[package]] name = "perry-codegen-wear-tiles" -version = "0.5.1197" +version = "0.5.1198" dependencies = [ "anyhow", "perry-hir", @@ -5469,7 +5469,7 @@ dependencies = [ [[package]] name = "perry-container-compose" -version = "0.5.1197" +version = "0.5.1198" dependencies = [ "anyhow", "async-trait", @@ -5498,14 +5498,14 @@ dependencies = [ [[package]] name = "perry-container-e2e" -version = "0.5.1197" +version = "0.5.1198" dependencies = [ "anyhow", ] [[package]] name = "perry-diagnostics" -version = "0.5.1197" +version = "0.5.1198" dependencies = [ "serde", "serde_json", @@ -5513,7 +5513,7 @@ dependencies = [ [[package]] name = "perry-dispatch" -version = "0.5.1197" +version = "0.5.1198" [[package]] name = "perry-doc-fixture-my-bindings" @@ -5524,7 +5524,7 @@ dependencies = [ [[package]] name = "perry-doc-tests" -version = "0.5.1197" +version = "0.5.1198" dependencies = [ "anyhow", "clap", @@ -5539,14 +5539,14 @@ dependencies = [ [[package]] name = "perry-ext-ads" -version = "0.5.1197" +version = "0.5.1198" dependencies = [ "perry-ffi", ] [[package]] name = "perry-ext-argon2" -version = "0.5.1197" +version = "0.5.1198" dependencies = [ "argon2", "perry-ffi", @@ -5554,7 +5554,7 @@ dependencies = [ [[package]] name = "perry-ext-axios" -version = "0.5.1197" +version = "0.5.1198" dependencies = [ "perry-ffi", "reqwest", @@ -5563,7 +5563,7 @@ dependencies = [ [[package]] name = "perry-ext-bcrypt" -version = "0.5.1197" +version = "0.5.1198" dependencies = [ "bcrypt", "perry-ffi", @@ -5571,7 +5571,7 @@ dependencies = [ [[package]] name = "perry-ext-better-sqlite3" -version = "0.5.1197" +version = "0.5.1198" dependencies = [ "perry-ffi", "rusqlite", @@ -5579,7 +5579,7 @@ dependencies = [ [[package]] name = "perry-ext-cheerio" -version = "0.5.1197" +version = "0.5.1198" dependencies = [ "perry-ffi", "scraper", @@ -5587,7 +5587,7 @@ dependencies = [ [[package]] name = "perry-ext-commander" -version = "0.5.1197" +version = "0.5.1198" dependencies = [ "perry-ffi", "perry-runtime", @@ -5595,7 +5595,7 @@ dependencies = [ [[package]] name = "perry-ext-cron" -version = "0.5.1197" +version = "0.5.1198" dependencies = [ "chrono", "cron 0.16.0", @@ -5605,7 +5605,7 @@ dependencies = [ [[package]] name = "perry-ext-dayjs" -version = "0.5.1197" +version = "0.5.1198" dependencies = [ "chrono", "perry-ffi", @@ -5613,7 +5613,7 @@ dependencies = [ [[package]] name = "perry-ext-decimal" -version = "0.5.1197" +version = "0.5.1198" dependencies = [ "perry-ffi", "rust_decimal", @@ -5621,7 +5621,7 @@ dependencies = [ [[package]] name = "perry-ext-dotenv" -version = "0.5.1197" +version = "0.5.1198" dependencies = [ "perry-ffi", "serde_json", @@ -5629,7 +5629,7 @@ dependencies = [ [[package]] name = "perry-ext-ethers" -version = "0.5.1197" +version = "0.5.1198" dependencies = [ "perry-ffi", "rand 0.8.6", @@ -5637,7 +5637,7 @@ dependencies = [ [[package]] name = "perry-ext-events" -version = "0.5.1197" +version = "0.5.1198" dependencies = [ "perry-ffi", "perry-runtime", @@ -5645,14 +5645,14 @@ dependencies = [ [[package]] name = "perry-ext-exponential-backoff" -version = "0.5.1197" +version = "0.5.1198" dependencies = [ "perry-ffi", ] [[package]] name = "perry-ext-fastify" -version = "0.5.1197" +version = "0.5.1198" dependencies = [ "bytes", "http-body-util", @@ -5669,7 +5669,7 @@ dependencies = [ [[package]] name = "perry-ext-fetch" -version = "0.5.1197" +version = "0.5.1198" dependencies = [ "lazy_static", "perry-ffi", @@ -5681,7 +5681,7 @@ dependencies = [ [[package]] name = "perry-ext-http" -version = "0.5.1197" +version = "0.5.1198" dependencies = [ "bytes", "lazy_static", @@ -5695,7 +5695,7 @@ dependencies = [ [[package]] name = "perry-ext-http-server" -version = "0.5.1197" +version = "0.5.1198" dependencies = [ "bytes", "h2", @@ -5718,7 +5718,7 @@ dependencies = [ [[package]] name = "perry-ext-ioredis" -version = "0.5.1197" +version = "0.5.1198" dependencies = [ "lazy_static", "perry-ffi", @@ -5728,7 +5728,7 @@ dependencies = [ [[package]] name = "perry-ext-jsonwebtoken" -version = "0.5.1197" +version = "0.5.1198" dependencies = [ "base64", "jsonwebtoken", @@ -5739,7 +5739,7 @@ dependencies = [ [[package]] name = "perry-ext-lru-cache" -version = "0.5.1197" +version = "0.5.1198" dependencies = [ "lru", "perry-ffi", @@ -5747,7 +5747,7 @@ dependencies = [ [[package]] name = "perry-ext-moment" -version = "0.5.1197" +version = "0.5.1198" dependencies = [ "chrono", "perry-ffi", @@ -5755,7 +5755,7 @@ dependencies = [ [[package]] name = "perry-ext-mongodb" -version = "0.5.1197" +version = "0.5.1198" dependencies = [ "bson", "futures-util", @@ -5767,7 +5767,7 @@ dependencies = [ [[package]] name = "perry-ext-mysql2" -version = "0.5.1197" +version = "0.5.1198" dependencies = [ "chrono", "perry-ffi", @@ -5777,7 +5777,7 @@ dependencies = [ [[package]] name = "perry-ext-nanoid" -version = "0.5.1197" +version = "0.5.1198" dependencies = [ "nanoid", "perry-ffi", @@ -5786,7 +5786,7 @@ dependencies = [ [[package]] name = "perry-ext-net" -version = "0.5.1197" +version = "0.5.1198" dependencies = [ "perry-ffi", "perry-runtime", @@ -5798,7 +5798,7 @@ dependencies = [ [[package]] name = "perry-ext-nodemailer" -version = "0.5.1197" +version = "0.5.1198" dependencies = [ "lettre", "perry-ffi", @@ -5808,7 +5808,7 @@ dependencies = [ [[package]] name = "perry-ext-pdf" -version = "0.5.1197" +version = "0.5.1198" dependencies = [ "perry-ffi", "printpdf", @@ -5816,7 +5816,7 @@ dependencies = [ [[package]] name = "perry-ext-pg" -version = "0.5.1197" +version = "0.5.1198" dependencies = [ "perry-ffi", "sqlx", @@ -5825,7 +5825,7 @@ dependencies = [ [[package]] name = "perry-ext-ratelimit" -version = "0.5.1197" +version = "0.5.1198" dependencies = [ "governor", "perry-ffi", @@ -5833,7 +5833,7 @@ dependencies = [ [[package]] name = "perry-ext-sharp" -version = "0.5.1197" +version = "0.5.1198" dependencies = [ "fast_image_resize", "image", @@ -5843,14 +5843,14 @@ dependencies = [ [[package]] name = "perry-ext-slugify" -version = "0.5.1197" +version = "0.5.1198" dependencies = [ "perry-ffi", ] [[package]] name = "perry-ext-streams" -version = "0.5.1197" +version = "0.5.1198" dependencies = [ "lazy_static", "perry-ffi", @@ -5859,7 +5859,7 @@ dependencies = [ [[package]] name = "perry-ext-uuid" -version = "0.5.1197" +version = "0.5.1198" dependencies = [ "perry-ffi", "uuid", @@ -5867,7 +5867,7 @@ dependencies = [ [[package]] name = "perry-ext-validator" -version = "0.5.1197" +version = "0.5.1198" dependencies = [ "perry-ffi", "regex", @@ -5877,7 +5877,7 @@ dependencies = [ [[package]] name = "perry-ext-ws" -version = "0.5.1197" +version = "0.5.1198" dependencies = [ "futures-util", "lazy_static", @@ -5889,7 +5889,7 @@ dependencies = [ [[package]] name = "perry-ext-zlib" -version = "0.5.1197" +version = "0.5.1198" dependencies = [ "brotli", "flate2", @@ -5898,7 +5898,7 @@ dependencies = [ [[package]] name = "perry-ffi" -version = "0.5.1197" +version = "0.5.1198" dependencies = [ "dashmap", "once_cell", @@ -5907,7 +5907,7 @@ dependencies = [ [[package]] name = "perry-hir" -version = "0.5.1197" +version = "0.5.1198" dependencies = [ "anyhow", "perry-api-manifest", @@ -5925,7 +5925,7 @@ dependencies = [ [[package]] name = "perry-parser" -version = "0.5.1197" +version = "0.5.1198" dependencies = [ "anyhow", "perry-diagnostics", @@ -5937,7 +5937,7 @@ dependencies = [ [[package]] name = "perry-runtime" -version = "0.5.1197" +version = "0.5.1198" dependencies = [ "anyhow", "base64", @@ -5970,14 +5970,14 @@ dependencies = [ [[package]] name = "perry-runtime-static" -version = "0.5.1197" +version = "0.5.1198" dependencies = [ "perry-runtime", ] [[package]] name = "perry-stdlib" -version = "0.5.1197" +version = "0.5.1198" dependencies = [ "aes 0.8.4", "aes-gcm", @@ -6069,14 +6069,14 @@ dependencies = [ [[package]] name = "perry-stdlib-static" -version = "0.5.1197" +version = "0.5.1198" dependencies = [ "perry-stdlib", ] [[package]] name = "perry-transform" -version = "0.5.1197" +version = "0.5.1198" dependencies = [ "anyhow", "perry-hir", @@ -6086,7 +6086,7 @@ dependencies = [ [[package]] name = "perry-types" -version = "0.5.1197" +version = "0.5.1198" dependencies = [ "anyhow", "thiserror 1.0.69", @@ -6094,14 +6094,14 @@ dependencies = [ [[package]] name = "perry-ui" -version = "0.5.1197" +version = "0.5.1198" dependencies = [ "perry-ui-model", ] [[package]] name = "perry-ui-android" -version = "0.5.1197" +version = "0.5.1198" dependencies = [ "base64", "itoa", @@ -6118,7 +6118,7 @@ dependencies = [ [[package]] name = "perry-ui-geisterhand" -version = "0.5.1197" +version = "0.5.1198" dependencies = [ "rand 0.8.6", "serde", @@ -6128,7 +6128,7 @@ dependencies = [ [[package]] name = "perry-ui-gtk4" -version = "0.5.1197" +version = "0.5.1198" dependencies = [ "base64", "cairo-rs", @@ -6151,7 +6151,7 @@ dependencies = [ [[package]] name = "perry-ui-ios" -version = "0.5.1197" +version = "0.5.1198" dependencies = [ "base64", "block2", @@ -6167,7 +6167,7 @@ dependencies = [ [[package]] name = "perry-ui-macos" -version = "0.5.1197" +version = "0.5.1198" dependencies = [ "base64", "block2", @@ -6182,7 +6182,7 @@ dependencies = [ [[package]] name = "perry-ui-model" -version = "0.5.1197" +version = "0.5.1198" [[package]] name = "perry-ui-test" @@ -6190,11 +6190,11 @@ version = "0.1.0" [[package]] name = "perry-ui-testkit" -version = "0.5.1197" +version = "0.5.1198" [[package]] name = "perry-ui-tvos" -version = "0.5.1197" +version = "0.5.1198" dependencies = [ "base64", "block2", @@ -6210,7 +6210,7 @@ dependencies = [ [[package]] name = "perry-ui-visionos" -version = "0.5.1197" +version = "0.5.1198" dependencies = [ "base64", "block2", @@ -6226,7 +6226,7 @@ dependencies = [ [[package]] name = "perry-ui-watchos" -version = "0.5.1197" +version = "0.5.1198" dependencies = [ "block2", "libc", @@ -6239,7 +6239,7 @@ dependencies = [ [[package]] name = "perry-ui-windows" -version = "0.5.1197" +version = "0.5.1198" dependencies = [ "base64", "libc", @@ -6256,14 +6256,14 @@ dependencies = [ [[package]] name = "perry-ui-windows-winui" -version = "0.5.1197" +version = "0.5.1198" dependencies = [ "perry-ui-windows", ] [[package]] name = "perry-updater" -version = "0.5.1197" +version = "0.5.1198" dependencies = [ "base64", "ed25519-dalek", @@ -6277,7 +6277,7 @@ dependencies = [ [[package]] name = "perry-wasm-host" -version = "0.5.1197" +version = "0.5.1198" dependencies = [ "wasmi", ] diff --git a/crates/perry-api-manifest/src/entries.rs b/crates/perry-api-manifest/src/entries.rs index 4ba9679799..f52b353e60 100644 --- a/crates/perry-api-manifest/src/entries.rs +++ b/crates/perry-api-manifest/src/entries.rs @@ -4935,6 +4935,9 @@ pub static API_MANIFEST: &[ApiEntry] = &[ method("perry/ui", "lazyvstackSetScrollEndCallback", false, None), method("perry/ui", "Table", false, None), method("perry/ui", "Canvas", false, None), + // Issue #2395 — BloomView (embed an external GPU renderer / Bloom engine) + method("perry/ui", "BloomView", false, None), + method("perry/ui", "bloomViewGetHwnd", false, None), method("perry/ui", "CameraView", false, None), method("perry/ui", "cameraStart", false, None), method("perry/ui", "cameraStop", false, None), diff --git a/crates/perry-dispatch/src/ui_table.rs b/crates/perry-dispatch/src/ui_table.rs index ec09d6b4ad..f076cfd8a6 100644 --- a/crates/perry-dispatch/src/ui_table.rs +++ b/crates/perry-dispatch/src/ui_table.rs @@ -1983,6 +1983,22 @@ pub const PERRY_UI_TABLE: &[MethodRow] = &[ args: &[ArgKind::F64, ArgKind::F64], ret: ReturnKind::Widget, }, + // ---- BloomView (issue #2395) ---- + // A render-surface host: `BloomView(width, height)` reserves a child window + // the Bloom engine draws into. `bloomViewGetHwnd(view)` returns the raw HWND + // (as a JS number) so user TS can call `attachToHwnd` on the Bloom package. + MethodRow { + method: "BloomView", + runtime: "perry_ui_bloomview_create", + args: &[ArgKind::F64, ArgKind::F64], + ret: ReturnKind::Widget, + }, + MethodRow { + method: "bloomViewGetHwnd", + runtime: "perry_ui_bloomview_get_hwnd", + args: &[ArgKind::Widget], + ret: ReturnKind::I64AsF64, + }, // ---- Drag & drop (issue #4773) ---- // Widget-level setters that attach drag/drop behavior to an existing // widget handle. `widgetOnDrop` registers a drop destination; the diff --git a/crates/perry-ui-windows/src/ffi/widget_create.rs b/crates/perry-ui-windows/src/ffi/widget_create.rs index bfa8b434e1..64462beeee 100644 --- a/crates/perry-ui-windows/src/ffi/widget_create.rs +++ b/crates/perry-ui-windows/src/ffi/widget_create.rs @@ -74,6 +74,21 @@ pub extern "C" fn perry_ui_canvas_create(width: f64, height: f64) -> i64 { widgets::canvas::create(width, height) } +/// Create a BloomView render-surface host (issue #2395). Returns a widget +/// handle; pair with `perry_ui_bloomview_get_hwnd` to embed an external GPU +/// renderer (the Bloom engine) into the reserved child window. +#[no_mangle] +pub extern "C" fn perry_ui_bloomview_create(width: f64, height: f64) -> i64 { + widgets::bloomview::create(width, height) +} + +/// Return the raw HWND value for a BloomView handle (as an integer), so user +/// TypeScript can hand it to the Bloom package's `attachToHwnd`. +#[no_mangle] +pub extern "C" fn perry_ui_bloomview_get_hwnd(handle: i64) -> i64 { + widgets::bloomview::get_hwnd_value(handle) +} + /// Create a Form container. #[no_mangle] pub extern "C" fn perry_ui_form_create() -> i64 { diff --git a/crates/perry-ui-windows/src/widgets/bloomview.rs b/crates/perry-ui-windows/src/widgets/bloomview.rs new file mode 100644 index 0000000000..6fc3aca770 --- /dev/null +++ b/crates/perry-ui-windows/src/widgets/bloomview.rs @@ -0,0 +1,133 @@ +//! BloomView — a native render-surface host widget (issue #2395). +//! +//! BloomView reserves a child window inside the Perry UI view tree for an +//! external GPU renderer (the Bloom game engine) to draw into. Perry UI does +//! NOT link or know about Bloom: the widget only owns the HWND and exposes it +//! via `bloomViewGetHwnd`. User TypeScript then hands that HWND to the Bloom +//! package (`attachToHwnd`), which builds its wgpu surface on it and subclasses +//! it for resize/input. This keeps `perry-ui-windows` free of any Bloom +//! dependency — apps that never call `BloomView` pull in nothing extra. +//! +//! Like WebView, BloomView reuses `WidgetKind::Image` for its registry slot — +//! it's a leaf widget the layout engine sizes; there is no kind-specific +//! dispatch. + +#[cfg(target_os = "windows")] +use super::{register_widget_with_layout, set_fixed_height, set_fixed_width, WidgetKind}; + +#[cfg(target_os = "windows")] +use windows::Win32::Foundation::*; +#[cfg(target_os = "windows")] +use windows::Win32::Graphics::Gdi::HBRUSH; +#[cfg(target_os = "windows")] +use windows::Win32::System::LibraryLoader::GetModuleHandleW; +#[cfg(target_os = "windows")] +use windows::Win32::UI::WindowsAndMessaging::*; + +#[cfg(target_os = "windows")] +fn to_wide(s: &str) -> Vec { + s.encode_utf16().chain(std::iter::once(0)).collect() +} + +/// Default window proc for the host window. Bloom classic-subclasses this once +/// attached; until then (and for any messages Bloom doesn't handle) it just +/// defers to the system. +#[cfg(target_os = "windows")] +unsafe extern "system" fn bloom_host_wndproc( + hwnd: HWND, + msg: u32, + wparam: WPARAM, + lparam: LPARAM, +) -> LRESULT { + DefWindowProcW(hwnd, msg, wparam, lparam) +} + +/// Register the host window class once. A plain child window whose background +/// is never erased (the GPU swapchain owns every pixel), which avoids flicker +/// between presents. +#[cfg(target_os = "windows")] +fn ensure_class_registered() { + thread_local! { + static REGISTERED: std::cell::Cell = const { std::cell::Cell::new(false) }; + } + REGISTERED.with(|r| { + if r.get() { + return; + } + unsafe { + let hinstance = GetModuleHandleW(None).unwrap(); + let class_name = to_wide("PerryBloomView"); + let wc = WNDCLASSEXW { + cbSize: std::mem::size_of::() as u32, + style: CS_HREDRAW | CS_VREDRAW, + lpfnWndProc: Some(bloom_host_wndproc), + hInstance: HINSTANCE(hinstance.0), + lpszClassName: windows::core::PCWSTR(class_name.as_ptr()), + // No background brush: the renderer fills the whole client area. + hbrBackground: HBRUSH(std::ptr::null_mut()), + ..Default::default() + }; + RegisterClassExW(&wc); + } + r.set(true); + }); +} + +/// Create a BloomView with the given logical width/height. Returns the widget +/// handle. The reserved size is fixed so the viewport claims space in a stack. +pub fn create(width: f64, height: f64) -> i64 { + #[cfg(target_os = "windows")] + { + ensure_class_registered(); + let class_name = to_wide("PerryBloomView"); + let window_text = to_wide(""); + unsafe { + let hinstance = GetModuleHandleW(None).unwrap(); + let hwnd = CreateWindowExW( + WINDOW_EX_STYLE::default(), + windows::core::PCWSTR(class_name.as_ptr()), + windows::core::PCWSTR(window_text.as_ptr()), + WS_CHILD | WS_VISIBLE | WS_CLIPSIBLINGS | WS_CLIPCHILDREN, + 0, + 0, + width as i32, + height as i32, + Some(super::get_parking_hwnd()), + None, + Some(HINSTANCE::from(hinstance)), + None, + ) + .unwrap(); + + let handle = + register_widget_with_layout(hwnd, WidgetKind::Image, 0.0, (0.0, 0.0, 0.0, 0.0)); + // Reserve the requested size so the view is visible in a layout. + set_fixed_width(handle, width as i32); + set_fixed_height(handle, height as i32); + handle + } + } + + #[cfg(not(target_os = "windows"))] + { + let _ = (width, height); + super::register_widget_with_layout(0, super::WidgetKind::Image, 0.0, (0.0, 0.0, 0.0, 0.0)) + } +} + +/// Return the raw HWND value for a BloomView handle as an integer, for handing +/// to an external renderer (`attachToHwnd`). Returns 0 if the handle is unknown. +pub fn get_hwnd_value(handle: i64) -> i64 { + #[cfg(target_os = "windows")] + { + match super::get_hwnd(handle) { + Some(hwnd) => hwnd.0 as i64, + None => 0, + } + } + + #[cfg(not(target_os = "windows"))] + { + super::get_hwnd(handle).unwrap_or(0) as i64 + } +} diff --git a/crates/perry-ui-windows/src/widgets/mod.rs b/crates/perry-ui-windows/src/widgets/mod.rs index 6f8a8a0d2c..9fd6953aa9 100644 --- a/crates/perry-ui-windows/src/widgets/mod.rs +++ b/crates/perry-ui-windows/src/widgets/mod.rs @@ -2,6 +2,7 @@ //! Each widget has an HWND (on Windows), a kind, children list, and layout info. pub mod attributed_text; +pub mod bloomview; pub mod bottom_nav; pub mod button; pub mod calendar; diff --git a/types/perry/ui/index.d.ts b/types/perry/ui/index.d.ts index 6843941a56..a4f2b3e3d2 100644 --- a/types/perry/ui/index.d.ts +++ b/types/perry/ui/index.d.ts @@ -419,6 +419,30 @@ export function ZStack(): Widget; */ export function Canvas(width: number, height: number): Canvas; +/** + * Render-surface host for an external GPU renderer (issue #2395). + * + * `BloomView(width, height)` reserves a native child window inside the Perry UI + * view tree. Perry UI does not draw into it — pass the handle returned by + * `bloomViewGetHwnd(view)` to a renderer such as the Bloom engine + * (`attachToHwnd`), which builds its surface on the window and drives frames. + * Currently implemented on the Windows target. + */ +export function BloomView(width: number, height: number): Widget; + +/** Raw native window handle (HWND) for a `BloomView`, as an integer. */ +export function bloomViewGetHwnd(view: Widget): number; + +/** + * Register a one-shot frame callback (requestAnimationFrame-style). The + * callback receives `(timestampMs, deltaMs)`. Re-register from inside the + * callback to keep a loop running. Returns an id usable with `cancelFrame`. + */ +export function onFrame(callback: (timestampMs: number, deltaMs: number) => void): number; + +/** Cancel a pending `onFrame` callback by its id. */ +export function cancelFrame(id: number): void; + /** Dropdown picker. */ export function Picker(onChange: (index: number) => void): Widget; From f4c42d41bea292c8981f0b6854bbfe79c1b6c334 Mon Sep 17 00:00:00 2001 From: Ralph Kuepper Date: Sun, 21 Jun 2026 14:02:06 +0200 Subject: [PATCH 2/4] fix(ui): regen API docs for BloomView + harden bloomview Win32 calls MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Regenerate docs/src/api/reference.md and docs/api/perry.d.ts from the API manifest so the BloomView / bloomViewGetHwnd entries no longer drift (fixes the api-docs-drift check). - bloomview.rs: don't unwrap GetModuleHandleW / CreateWindowExW across the FFI boundary — return early / an invalid (0) handle on failure instead of panicking the process (CodeRabbit). Claude-Session: https://claude.ai/code/session_011z6cNYoSsNG4ffLYLXYRwc --- .../perry-ui-windows/src/widgets/bloomview.rs | 24 +++++++++++++++---- docs/api/perry.d.ts | 6 ++++- docs/src/api/reference.md | 4 +++- 3 files changed, 27 insertions(+), 7 deletions(-) diff --git a/crates/perry-ui-windows/src/widgets/bloomview.rs b/crates/perry-ui-windows/src/widgets/bloomview.rs index 6fc3aca770..aaef182e41 100644 --- a/crates/perry-ui-windows/src/widgets/bloomview.rs +++ b/crates/perry-ui-windows/src/widgets/bloomview.rs @@ -55,7 +55,12 @@ fn ensure_class_registered() { return; } unsafe { - let hinstance = GetModuleHandleW(None).unwrap(); + // Don't unwrap across the FFI boundary — bail and retry on the next + // create() if the module handle is somehow unavailable. + let hinstance = match GetModuleHandleW(None) { + Ok(h) => h, + Err(_) => return, + }; let class_name = to_wide("PerryBloomView"); let wc = WNDCLASSEXW { cbSize: std::mem::size_of::() as u32, @@ -67,6 +72,8 @@ fn ensure_class_registered() { hbrBackground: HBRUSH(std::ptr::null_mut()), ..Default::default() }; + // Returns 0 if the class already exists (e.g. registered from another + // thread) — harmless, the class is usable either way. RegisterClassExW(&wc); } r.set(true); @@ -82,8 +89,13 @@ pub fn create(width: f64, height: f64) -> i64 { let class_name = to_wide("PerryBloomView"); let window_text = to_wide(""); unsafe { - let hinstance = GetModuleHandleW(None).unwrap(); - let hwnd = CreateWindowExW( + // Avoid panicking across the FFI boundary; return an invalid (0) + // handle if the OS refuses the module handle or the window. + let hinstance = match GetModuleHandleW(None) { + Ok(h) => h, + Err(_) => return 0, + }; + let hwnd = match CreateWindowExW( WINDOW_EX_STYLE::default(), windows::core::PCWSTR(class_name.as_ptr()), windows::core::PCWSTR(window_text.as_ptr()), @@ -96,8 +108,10 @@ pub fn create(width: f64, height: f64) -> i64 { None, Some(HINSTANCE::from(hinstance)), None, - ) - .unwrap(); + ) { + Ok(h) => h, + Err(_) => return 0, + }; let handle = register_widget_with_layout(hwnd, WidgetKind::Image, 0.0, (0.0, 0.0, 0.0, 0.0)); diff --git a/docs/api/perry.d.ts b/docs/api/perry.d.ts index 86eddf61f3..06e1270275 100644 --- a/docs/api/perry.d.ts +++ b/docs/api/perry.d.ts @@ -1,6 +1,6 @@ // Auto-generated from Perry's API manifest (#465). Do not edit by hand. // Source: perry-api-manifest::API_MANIFEST -// Coverage: 1943 entries across 113 modules +// Coverage: 1945 entries across 113 modules type PerryU32 = number & { readonly __perryU32?: never }; type PerryU64 = number & { readonly __perryU64?: never }; @@ -2780,6 +2780,8 @@ declare module "perry/ui" { /** stdlib */ export function AttributedText(...args: any[]): any; /** stdlib */ + export function BloomView(...args: any[]): any; + /** stdlib */ export function BottomNavigation(...args: any[]): any; /** stdlib */ export function Button(...args: any[]): any; @@ -2864,6 +2866,8 @@ declare module "perry/ui" { /** stdlib */ export function attributedTextClear(...args: any[]): any; /** stdlib */ + export function bloomViewGetHwnd(...args: any[]): any; + /** stdlib */ export function blur(...args: any[]): any; /** stdlib */ export function bottomNavAddItem(...args: any[]): any; diff --git a/docs/src/api/reference.md b/docs/src/api/reference.md index e1bd5a6f1e..8acee9b34e 100644 --- a/docs/src/api/reference.md +++ b/docs/src/api/reference.md @@ -2,7 +2,7 @@ This page is auto-generated from Perry's compile-time API manifest (`perry-api-manifest::API_MANIFEST`). It is the source of truth for what `perry compile` accepts; references to symbols not listed here produce `R005 UnimplementedApi` (issue #463). Stubs (#464) are flagged ⚠ — they link cleanly but no-op at runtime on the chosen target. -Total: 2815 entries across 115 modules. +Total: 2817 entries across 115 modules. ## Modules @@ -2525,6 +2525,7 @@ Total: 2815 entries across 115 modules. - `App` — module - `AttributedText` — module +- `BloomView` — module - `BottomNavigation` — module - `Button` — module - `CameraView` — module @@ -2567,6 +2568,7 @@ Total: 2815 entries across 115 modules. - `appSetTimer` — module - `attributedTextAppend` — module - `attributedTextClear` — module +- `bloomViewGetHwnd` — module - `blur` — module - `bottomNavAddItem` — module - `bottomNavSetBadge` — module From bbf9120794856be18745c10866ea61d5a1f1a8fb Mon Sep 17 00:00:00 2001 From: Ralph Kuepper Date: Sun, 21 Jun 2026 14:56:57 +0200 Subject: [PATCH 3/4] feat(ui): wire BloomView across all platform UI backends (#2395) Adds the `perry_ui_bloomview_create` / `perry_ui_bloomview_get_hwnd` exports (and a widget module where applicable) to every Perry UI backend, so a BloomView app links and runs on all targets, not just Windows: - macOS / iOS / visionOS: register a bare NSView/UIView; the getter returns the raw view pointer. - GTK4 (Linux): a GtkDrawingArea; getter returns the GtkWidget* (the issue's MVP target for Vulkan dmabuf embedding). - Android: a bare android.view.View via JNI; getter echoes the handle token (no HWND on Android; a SurfaceView + JNI surface bridge is the follow-up). - tvOS / watchOS: link-stability stubs returning 0, mirroring how those crates already stub WebView. - wasm/web: a element registered in __perryUiDispatch; getter returns the handle id. Each mirrors that platform's existing WebView idiom. The native-handle meaning is platform-specific (HWND / NSView* / UIView* / GtkWidget* / handle token). Verified compiling locally: perry-codegen-wasm and perry-ui-windows. The other UI crates are excluded from the blocking Linux CI and require their native hosts to compile (verified by mirroring confirmed crate patterns). Claude-Session: https://claude.ai/code/session_011z6cNYoSsNG4ffLYLXYRwc --- crates/perry-codegen-wasm/src/wasm_runtime.js | 12 +++++ crates/perry-ui-android/src/ffi/issue_553.rs | 16 ++++++ .../perry-ui-android/src/widgets/bloomview.rs | 54 +++++++++++++++++++ crates/perry-ui-android/src/widgets/mod.rs | 1 + .../ffi/stubs_webview_attrtext_screenshot.rs | 12 +++++ crates/perry-ui-gtk4/src/widgets/bloomview.rs | 37 +++++++++++++ crates/perry-ui-gtk4/src/widgets/mod.rs | 1 + crates/perry-ui-ios/src/ffi/widgets_basic.rs | 12 +++++ crates/perry-ui-ios/src/widgets/bloomview.rs | 32 +++++++++++ crates/perry-ui-ios/src/widgets/mod.rs | 1 + .../src/lib_ffi/interactivity.rs | 12 +++++ .../perry-ui-macos/src/widgets/bloomview.rs | 30 +++++++++++ crates/perry-ui-macos/src/widgets/mod.rs | 1 + crates/perry-ui-tvos/src/ffi/media_extras.rs | 12 +++++ crates/perry-ui-visionos/src/ffi_core.rs | 12 +++++ .../src/widgets/bloomview.rs | 32 +++++++++++ crates/perry-ui-visionos/src/widgets/mod.rs | 1 + crates/perry-ui-watchos/src/lib.rs | 12 +++++ types/perry/ui/index.d.ts | 15 ++++-- 19 files changed, 300 insertions(+), 5 deletions(-) create mode 100644 crates/perry-ui-android/src/widgets/bloomview.rs create mode 100644 crates/perry-ui-gtk4/src/widgets/bloomview.rs create mode 100644 crates/perry-ui-ios/src/widgets/bloomview.rs create mode 100644 crates/perry-ui-macos/src/widgets/bloomview.rs create mode 100644 crates/perry-ui-visionos/src/widgets/bloomview.rs diff --git a/crates/perry-codegen-wasm/src/wasm_runtime.js b/crates/perry-codegen-wasm/src/wasm_runtime.js index c172820409..ee238cb2a3 100644 --- a/crates/perry-codegen-wasm/src/wasm_runtime.js +++ b/crates/perry-codegen-wasm/src/wasm_runtime.js @@ -2830,6 +2830,17 @@ function perry_ui_canvas_create(width, height) { el._ctx = el.getContext("2d"); return uiAlloc(el); } +// BloomView (issue #2395) — a render-surface host for an external GPU +// renderer. The web has no HWND; bloomViewGetHwnd returns the handle id (the +// surrogate the renderer keys off of) or 0 if unknown. +function perry_ui_bloomview_create(width, height) { + const el = document.createElement("canvas"); + el.width = width || 300; el.height = height || 150; + return uiAlloc(el); +} +function perry_ui_bloomview_get_hwnd(h) { + return uiGet(h) ? h : 0; +} function perry_ui_lazyvstack_create(count, renderClosure) { const el = document.createElement("div"); el.style.display = "flex"; el.style.flexDirection = "column"; el.style.overflow = "auto"; el.style.flex = "1 1 0%"; @@ -4632,6 +4643,7 @@ const __perryUiDispatch = { perry_ui_toggle_create, perry_ui_toggle_set_state, perry_ui_slider_create, perry_ui_scrollview_create, perry_ui_spacer_create, perry_ui_divider_create, perry_ui_progressview_create, perry_ui_image_create, perry_ui_picker_create, perry_ui_form_create, perry_ui_section_create, perry_ui_navigationstack_create, perry_ui_canvas_create, + perry_ui_bloomview_create, perry_ui_bloomview_get_hwnd, perry_ui_lazyvstack_create, perry_ui_lazyvstack_update, perry_ui_table_create, perry_ui_table_set_column_header, perry_ui_table_set_column_width, perry_ui_table_update_row_count, perry_ui_table_set_on_row_select, perry_ui_table_get_selected_row, diff --git a/crates/perry-ui-android/src/ffi/issue_553.rs b/crates/perry-ui-android/src/ffi/issue_553.rs index 4d2505a9e5..5b02115574 100644 --- a/crates/perry-ui-android/src/ffi/issue_553.rs +++ b/crates/perry-ui-android/src/ffi/issue_553.rs @@ -136,6 +136,22 @@ pub extern "C" fn perry_ui_webview_create( widgets::webview::create(url_ptr as *const u8, width, height, ephemeral) }) } + +/// Create a BloomView render-surface host (issue #2395). +#[no_mangle] +pub extern "C" fn perry_ui_bloomview_create(width: f64, height: f64) -> i64 { + catch_panic("perry_ui_bloomview_create", || { + widgets::bloomview::create(width, height) + }) +} + +/// Return the BloomView's native handle token (issue #2395). +#[no_mangle] +pub extern "C" fn perry_ui_bloomview_get_hwnd(handle: i64) -> i64 { + catch_panic("perry_ui_bloomview_get_hwnd", || { + widgets::bloomview::get_native_handle(handle) + }) +} #[no_mangle] pub extern "C" fn perry_ui_webview_set_user_agent(handle: i64, ua_ptr: i64) { catch_panic_void("perry_ui_webview_set_user_agent", || { diff --git a/crates/perry-ui-android/src/widgets/bloomview.rs b/crates/perry-ui-android/src/widgets/bloomview.rs new file mode 100644 index 0000000000..689ee4f7f0 --- /dev/null +++ b/crates/perry-ui-android/src/widgets/bloomview.rs @@ -0,0 +1,54 @@ +//! BloomView — a native render-surface host widget (issue #2395). +//! +//! Reserves a bare `android.view.View` in the Perry UI view tree for an external +//! GPU renderer (e.g. the Bloom engine) to draw into. Perry UI only owns the +//! view; user TypeScript drives the renderer. Mirrors the Windows implementation +//! conceptually — Android has no HWND, so `bloomViewGetHwnd` echoes the registry +//! handle as a stable token (a real `SurfaceView`/`TextureView` + JNI surface +//! bridge is the follow-up for live embedding). + +use crate::jni_bridge; +use jni::objects::JValue; + +/// Create a BloomView host. Returns the widget handle, or 0 on JNI failure. +pub fn create(_width: f64, _height: f64) -> i64 { + let mut env = jni_bridge::get_env(); + let _ = env.push_local_frame(8); + + let activity = super::get_activity(&mut env); + let view = match env.new_object( + "android/view/View", + "(Landroid/content/Context;)V", + &[JValue::Object(&activity)], + ) { + Ok(v) => v, + Err(_) => { + unsafe { + let _ = env.pop_local_frame(&jni::objects::JObject::null()); + } + return 0; + } + }; + + let global_ref = match env.new_global_ref(&view) { + Ok(g) => g, + Err(_) => { + unsafe { + let _ = env.pop_local_frame(&jni::objects::JObject::null()); + } + return 0; + } + }; + unsafe { + let _ = env.pop_local_frame(&jni::objects::JObject::null()); + } + + super::register_widget(global_ref) +} + +/// Android has no HWND; echo the registry handle as a stable token for the +/// caller. (A real native-surface address would only be valid on the JNI +/// thread and is not a durable handle.) +pub fn get_native_handle(handle: i64) -> i64 { + handle +} diff --git a/crates/perry-ui-android/src/widgets/mod.rs b/crates/perry-ui-android/src/widgets/mod.rs index fd34d1d1c1..bbf5142088 100644 --- a/crates/perry-ui-android/src/widgets/mod.rs +++ b/crates/perry-ui-android/src/widgets/mod.rs @@ -1,4 +1,5 @@ pub mod attributed_text; +pub mod bloomview; pub mod bottom_nav; pub mod button; pub mod calendar; diff --git a/crates/perry-ui-gtk4/src/ffi/stubs_webview_attrtext_screenshot.rs b/crates/perry-ui-gtk4/src/ffi/stubs_webview_attrtext_screenshot.rs index f5da323054..01674e6cd3 100644 --- a/crates/perry-ui-gtk4/src/ffi/stubs_webview_attrtext_screenshot.rs +++ b/crates/perry-ui-gtk4/src/ffi/stubs_webview_attrtext_screenshot.rs @@ -106,6 +106,18 @@ pub extern "C" fn perry_ui_webview_create( ) -> i64 { widgets::webview::create(url_ptr as *const u8, width, height, ephemeral) } + +/// Create a BloomView render-surface host (issue #2395). +#[no_mangle] +pub extern "C" fn perry_ui_bloomview_create(width: f64, height: f64) -> i64 { + widgets::bloomview::create(width, height) +} + +/// Return the BloomView's native `GtkWidget*` as an integer (issue #2395). +#[no_mangle] +pub extern "C" fn perry_ui_bloomview_get_hwnd(handle: i64) -> i64 { + widgets::bloomview::get_native_handle(handle) +} #[no_mangle] pub extern "C" fn perry_ui_webview_set_user_agent(handle: i64, ua_ptr: i64) { widgets::webview::set_user_agent(handle, ua_ptr as *const u8) diff --git a/crates/perry-ui-gtk4/src/widgets/bloomview.rs b/crates/perry-ui-gtk4/src/widgets/bloomview.rs new file mode 100644 index 0000000000..d28ceb210a --- /dev/null +++ b/crates/perry-ui-gtk4/src/widgets/bloomview.rs @@ -0,0 +1,37 @@ +//! BloomView — a native render-surface host widget (issue #2395). +//! +//! Reserves a `GtkDrawingArea` in the Perry UI view tree for an external GPU +//! renderer (e.g. the Bloom engine) to draw into. Perry UI only owns the widget +//! and exposes its `GtkWidget*` via `bloomViewGetHwnd`; user TypeScript hands +//! that to the renderer, which targets the widget's surface (the issue's MVP +//! used GTK4 + Vulkan dmabuf). Mirrors the Windows implementation, with the +//! HWND replaced by the raw `GtkWidget*`. + +use gtk4::prelude::*; + +/// Create a BloomView host. Reserves the requested size, or expands to fill if +/// none is given. Returns the widget handle. +pub fn create(width: f64, height: f64) -> i64 { + crate::app::ensure_gtk_init(); + let area = gtk4::DrawingArea::new(); + if width > 0.0 && height > 0.0 { + area.set_size_request(width as i32, height as i32); + } else { + area.set_hexpand(true); + area.set_vexpand(true); + } + super::register_widget(area.upcast()) +} + +/// Return the raw `GtkWidget*` for a BloomView handle as an integer, for handing +/// to an external GPU renderer. Returns 0 if the handle is unknown. +pub fn get_native_handle(handle: i64) -> i64 { + use gtk4::glib::translate::ToGlibPtr; + match super::get_widget(handle) { + Some(w) => { + let ptr: *mut gtk4::ffi::GtkWidget = w.to_glib_none().0; + ptr as i64 + } + None => 0, + } +} diff --git a/crates/perry-ui-gtk4/src/widgets/mod.rs b/crates/perry-ui-gtk4/src/widgets/mod.rs index fa322fe6d7..ff550ad901 100644 --- a/crates/perry-ui-gtk4/src/widgets/mod.rs +++ b/crates/perry-ui-gtk4/src/widgets/mod.rs @@ -1,4 +1,5 @@ pub mod attributed_text; +pub mod bloomview; pub mod bottom_nav; pub mod button; pub mod calendar; diff --git a/crates/perry-ui-ios/src/ffi/widgets_basic.rs b/crates/perry-ui-ios/src/ffi/widgets_basic.rs index d777c24e87..b4d39f200f 100644 --- a/crates/perry-ui-ios/src/ffi/widgets_basic.rs +++ b/crates/perry-ui-ios/src/ffi/widgets_basic.rs @@ -205,6 +205,18 @@ pub extern "C" fn perry_ui_webview_create( ) -> i64 { widgets::webview::create(url_ptr as *const u8, width, height, ephemeral) } + +/// Create a BloomView render-surface host (issue #2395). +#[no_mangle] +pub extern "C" fn perry_ui_bloomview_create(width: f64, height: f64) -> i64 { + widgets::bloomview::create(width, height) +} + +/// Return the BloomView's native view pointer as an integer (issue #2395). +#[no_mangle] +pub extern "C" fn perry_ui_bloomview_get_hwnd(handle: i64) -> i64 { + widgets::bloomview::get_native_handle(handle) +} #[no_mangle] pub extern "C" fn perry_ui_webview_set_user_agent(handle: i64, ua_ptr: i64) { widgets::webview::set_user_agent(handle, ua_ptr as *const u8) diff --git a/crates/perry-ui-ios/src/widgets/bloomview.rs b/crates/perry-ui-ios/src/widgets/bloomview.rs new file mode 100644 index 0000000000..a24823aaba --- /dev/null +++ b/crates/perry-ui-ios/src/widgets/bloomview.rs @@ -0,0 +1,32 @@ +//! BloomView — a native render-surface host widget (issue #2395). +//! +//! Reserves a bare `UIView` in the Perry UI view tree for an external GPU +//! renderer (e.g. the Bloom engine) to draw into. Perry UI only owns the view +//! and exposes its pointer via `bloomViewGetHwnd`; user TypeScript hands that +//! pointer to the renderer, which builds its (Metal) surface on it. Mirrors the +//! Windows implementation, with the HWND replaced by the raw `UIView*`. + +use objc2::msg_send; +use objc2::rc::Retained; +use objc2::runtime::AnyClass; +use objc2_ui_kit::UIView; + +/// Create a BloomView host. `width`/`height` are advisory — the layout engine +/// sizes the view. Returns the widget handle. +pub fn create(width: f64, height: f64) -> i64 { + let _ = (width, height); + unsafe { + let view: Retained = msg_send![AnyClass::get(c"UIView").unwrap(), new]; + super::register_widget(view) + } +} + +/// Return the raw `UIView*` for a BloomView handle as an integer, for handing +/// to an external GPU renderer. Returns 0 if the handle is unknown. The +/// registry retains the view; the returned pointer is non-owning. +pub fn get_native_handle(handle: i64) -> i64 { + match super::get_widget(handle) { + Some(view) => Retained::as_ptr(&view) as i64, + None => 0, + } +} diff --git a/crates/perry-ui-ios/src/widgets/mod.rs b/crates/perry-ui-ios/src/widgets/mod.rs index 3d6d593458..03b59db852 100644 --- a/crates/perry-ui-ios/src/widgets/mod.rs +++ b/crates/perry-ui-ios/src/widgets/mod.rs @@ -1,5 +1,6 @@ pub mod alert; pub mod attributed_text; +pub mod bloomview; pub mod bottom_nav; pub mod button; pub mod calendar; diff --git a/crates/perry-ui-macos/src/lib_ffi/interactivity.rs b/crates/perry-ui-macos/src/lib_ffi/interactivity.rs index 6d8a901230..d1c09bb7f2 100644 --- a/crates/perry-ui-macos/src/lib_ffi/interactivity.rs +++ b/crates/perry-ui-macos/src/lib_ffi/interactivity.rs @@ -334,6 +334,18 @@ pub extern "C" fn perry_ui_webview_create( ) -> i64 { widgets::webview::create(url_ptr as *const u8, width, height, ephemeral) } + +/// Create a BloomView render-surface host (issue #2395). +#[no_mangle] +pub extern "C" fn perry_ui_bloomview_create(width: f64, height: f64) -> i64 { + widgets::bloomview::create(width, height) +} + +/// Return the BloomView's native view pointer as an integer (issue #2395). +#[no_mangle] +pub extern "C" fn perry_ui_bloomview_get_hwnd(handle: i64) -> i64 { + widgets::bloomview::get_native_handle(handle) +} #[no_mangle] pub extern "C" fn perry_ui_webview_set_user_agent(handle: i64, ua_ptr: i64) { widgets::webview::set_user_agent(handle, ua_ptr as *const u8) diff --git a/crates/perry-ui-macos/src/widgets/bloomview.rs b/crates/perry-ui-macos/src/widgets/bloomview.rs new file mode 100644 index 0000000000..369308fe48 --- /dev/null +++ b/crates/perry-ui-macos/src/widgets/bloomview.rs @@ -0,0 +1,30 @@ +//! BloomView — a native render-surface host widget (issue #2395). +//! +//! Reserves a bare `NSView` in the Perry UI view tree for an external GPU +//! renderer (e.g. the Bloom engine) to draw into. Perry UI only owns the view +//! and exposes its pointer via `bloomViewGetHwnd`; user TypeScript hands that +//! pointer to the renderer, which builds its (Metal) surface on it. Mirrors the +//! Windows implementation (`perry-ui-windows`), with the HWND replaced by the +//! raw `NSView*`. + +use objc2_app_kit::NSView; +use objc2_foundation::MainThreadMarker; + +/// Create a BloomView host. `width`/`height` are advisory — the layout engine +/// sizes the view. Returns the widget handle. +pub fn create(width: f64, height: f64) -> i64 { + let _ = (width, height); + let mtm = MainThreadMarker::new().expect("perry/ui must run on the main thread"); + let view = NSView::new(mtm); + super::register_widget(view) +} + +/// Return the raw `NSView*` for a BloomView handle as an integer, for handing +/// to an external GPU renderer. Returns 0 if the handle is unknown. The +/// registry retains the view; the returned pointer is non-owning. +pub fn get_native_handle(handle: i64) -> i64 { + match super::get_widget(handle) { + Some(view) => objc2::rc::Retained::as_ptr(&view) as i64, + None => 0, + } +} diff --git a/crates/perry-ui-macos/src/widgets/mod.rs b/crates/perry-ui-macos/src/widgets/mod.rs index a0397ee7f6..fae56a4524 100644 --- a/crates/perry-ui-macos/src/widgets/mod.rs +++ b/crates/perry-ui-macos/src/widgets/mod.rs @@ -1,5 +1,6 @@ pub mod alert; pub mod attributed_text; +pub mod bloomview; pub mod bottom_nav; pub mod button; pub mod calendar; diff --git a/crates/perry-ui-tvos/src/ffi/media_extras.rs b/crates/perry-ui-tvos/src/ffi/media_extras.rs index a6691a6252..af6ae2f06c 100644 --- a/crates/perry-ui-tvos/src/ffi/media_extras.rs +++ b/crates/perry-ui-tvos/src/ffi/media_extras.rs @@ -210,6 +210,18 @@ pub extern "C" fn perry_ui_webview_create( ) -> i64 { 0 } + +// BloomView (issue #2395) — stub on this platform, matching the WebView shape +// above; returns a 0 handle so apps that import BloomView still link and run. +#[no_mangle] +pub extern "C" fn perry_ui_bloomview_create(_width: f64, _height: f64) -> i64 { + 0 +} +#[no_mangle] +pub extern "C" fn perry_ui_bloomview_get_hwnd(_handle: i64) -> i64 { + 0 +} + #[no_mangle] pub extern "C" fn perry_ui_webview_set_user_agent(_handle: i64, _ua_ptr: i64) {} #[no_mangle] diff --git a/crates/perry-ui-visionos/src/ffi_core.rs b/crates/perry-ui-visionos/src/ffi_core.rs index fecea4881a..34e85fe7a7 100644 --- a/crates/perry-ui-visionos/src/ffi_core.rs +++ b/crates/perry-ui-visionos/src/ffi_core.rs @@ -211,6 +211,18 @@ pub extern "C" fn perry_ui_webview_create( ) -> i64 { widgets::webview::create(url_ptr as *const u8, width, height, ephemeral) } + +/// Create a BloomView render-surface host (issue #2395). +#[no_mangle] +pub extern "C" fn perry_ui_bloomview_create(width: f64, height: f64) -> i64 { + widgets::bloomview::create(width, height) +} + +/// Return the BloomView's native view pointer as an integer (issue #2395). +#[no_mangle] +pub extern "C" fn perry_ui_bloomview_get_hwnd(handle: i64) -> i64 { + widgets::bloomview::get_native_handle(handle) +} #[no_mangle] pub extern "C" fn perry_ui_webview_set_user_agent(handle: i64, ua_ptr: i64) { widgets::webview::set_user_agent(handle, ua_ptr as *const u8) diff --git a/crates/perry-ui-visionos/src/widgets/bloomview.rs b/crates/perry-ui-visionos/src/widgets/bloomview.rs new file mode 100644 index 0000000000..a24823aaba --- /dev/null +++ b/crates/perry-ui-visionos/src/widgets/bloomview.rs @@ -0,0 +1,32 @@ +//! BloomView — a native render-surface host widget (issue #2395). +//! +//! Reserves a bare `UIView` in the Perry UI view tree for an external GPU +//! renderer (e.g. the Bloom engine) to draw into. Perry UI only owns the view +//! and exposes its pointer via `bloomViewGetHwnd`; user TypeScript hands that +//! pointer to the renderer, which builds its (Metal) surface on it. Mirrors the +//! Windows implementation, with the HWND replaced by the raw `UIView*`. + +use objc2::msg_send; +use objc2::rc::Retained; +use objc2::runtime::AnyClass; +use objc2_ui_kit::UIView; + +/// Create a BloomView host. `width`/`height` are advisory — the layout engine +/// sizes the view. Returns the widget handle. +pub fn create(width: f64, height: f64) -> i64 { + let _ = (width, height); + unsafe { + let view: Retained = msg_send![AnyClass::get(c"UIView").unwrap(), new]; + super::register_widget(view) + } +} + +/// Return the raw `UIView*` for a BloomView handle as an integer, for handing +/// to an external GPU renderer. Returns 0 if the handle is unknown. The +/// registry retains the view; the returned pointer is non-owning. +pub fn get_native_handle(handle: i64) -> i64 { + match super::get_widget(handle) { + Some(view) => Retained::as_ptr(&view) as i64, + None => 0, + } +} diff --git a/crates/perry-ui-visionos/src/widgets/mod.rs b/crates/perry-ui-visionos/src/widgets/mod.rs index 325a4e67bb..a5f98e1ee5 100644 --- a/crates/perry-ui-visionos/src/widgets/mod.rs +++ b/crates/perry-ui-visionos/src/widgets/mod.rs @@ -1,5 +1,6 @@ pub mod alert; pub mod attributed_text; +pub mod bloomview; pub mod bottom_nav; pub mod button; pub mod calendar; diff --git a/crates/perry-ui-watchos/src/lib.rs b/crates/perry-ui-watchos/src/lib.rs index 7c8027a777..64e2e84853 100644 --- a/crates/perry-ui-watchos/src/lib.rs +++ b/crates/perry-ui-watchos/src/lib.rs @@ -1702,6 +1702,18 @@ pub extern "C" fn perry_ui_webview_create( ) -> i64 { 0 } + +// BloomView (issue #2395) — stub on this platform, matching the WebView shape +// above; returns a 0 handle so apps that import BloomView still link and run. +#[no_mangle] +pub extern "C" fn perry_ui_bloomview_create(_width: f64, _height: f64) -> i64 { + 0 +} +#[no_mangle] +pub extern "C" fn perry_ui_bloomview_get_hwnd(_handle: i64) -> i64 { + 0 +} + #[no_mangle] pub extern "C" fn perry_ui_webview_set_user_agent(_handle: i64, _ua_ptr: i64) {} #[no_mangle] diff --git a/types/perry/ui/index.d.ts b/types/perry/ui/index.d.ts index a4f2b3e3d2..d44786c0d4 100644 --- a/types/perry/ui/index.d.ts +++ b/types/perry/ui/index.d.ts @@ -422,15 +422,20 @@ export function Canvas(width: number, height: number): Canvas; /** * Render-surface host for an external GPU renderer (issue #2395). * - * `BloomView(width, height)` reserves a native child window inside the Perry UI - * view tree. Perry UI does not draw into it — pass the handle returned by + * `BloomView(width, height)` reserves a native render-surface view in the Perry + * UI view tree. Perry UI does not draw into it — pass the handle returned by * `bloomViewGetHwnd(view)` to a renderer such as the Bloom engine - * (`attachToHwnd`), which builds its surface on the window and drives frames. - * Currently implemented on the Windows target. + * (`attachToHwnd`), which builds its surface on the view and drives frames. + * Available on all native targets (Windows HWND, macOS/iOS/visionOS native + * view, GTK4 widget, Android view; tvOS/watchOS link as no-ops). */ export function BloomView(width: number, height: number): Widget; -/** Raw native window handle (HWND) for a `BloomView`, as an integer. */ +/** + * The `BloomView`'s native handle as a number — the platform's render-surface + * pointer (HWND on Windows, NSView/UIView on Apple, GtkWidget on GTK4). Hand + * this to an external renderer's attach call. + */ export function bloomViewGetHwnd(view: Widget): number; /** From 2bb5a59f56ab023e6c5870c3f89c48a049f80e7a Mon Sep 17 00:00:00 2001 From: Ralph Kuepper Date: Sun, 21 Jun 2026 15:10:20 +0200 Subject: [PATCH 4/4] fix(ui): address CodeRabbit review on cross-platform BloomView MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - macOS/iOS/visionOS: guard create() with `MainThreadMarker::new()` and return 0 off the main thread instead of `.expect()`-panicking across the FFI boundary (UIKit/AppKit views must be made on the main thread). - GTK4: validate width/height are finite and >= 1 and clamp before set_size_request, so NaN/inf/sub-pixel values don't produce a bogus request. - Android: get_native_handle now validates the handle via get_widget and returns 0 for unknown/stale handles (matches the web peer). (Skipped the Android create() width/height nit: Android WebView create() likewise ignores them — sizing is applied via the layout system, not at View construction.) Claude-Session: https://claude.ai/code/session_011z6cNYoSsNG4ffLYLXYRwc --- crates/perry-ui-android/src/widgets/bloomview.rs | 9 ++++++--- crates/perry-ui-gtk4/src/widgets/bloomview.rs | 8 ++++++-- crates/perry-ui-ios/src/widgets/bloomview.rs | 8 +++++++- crates/perry-ui-macos/src/widgets/bloomview.rs | 6 +++++- crates/perry-ui-visionos/src/widgets/bloomview.rs | 8 +++++++- 5 files changed, 31 insertions(+), 8 deletions(-) diff --git a/crates/perry-ui-android/src/widgets/bloomview.rs b/crates/perry-ui-android/src/widgets/bloomview.rs index 689ee4f7f0..6951e79ea7 100644 --- a/crates/perry-ui-android/src/widgets/bloomview.rs +++ b/crates/perry-ui-android/src/widgets/bloomview.rs @@ -47,8 +47,11 @@ pub fn create(_width: f64, _height: f64) -> i64 { } /// Android has no HWND; echo the registry handle as a stable token for the -/// caller. (A real native-surface address would only be valid on the JNI -/// thread and is not a durable handle.) +/// caller — but validate it first (return 0 for an unknown/stale handle), so +/// downstream renderers never treat a bogus handle as attachable. pub fn get_native_handle(handle: i64) -> i64 { - handle + match super::get_widget(handle) { + Some(_) => handle, + None => 0, + } } diff --git a/crates/perry-ui-gtk4/src/widgets/bloomview.rs b/crates/perry-ui-gtk4/src/widgets/bloomview.rs index d28ceb210a..5e31a4c762 100644 --- a/crates/perry-ui-gtk4/src/widgets/bloomview.rs +++ b/crates/perry-ui-gtk4/src/widgets/bloomview.rs @@ -14,8 +14,12 @@ use gtk4::prelude::*; pub fn create(width: f64, height: f64) -> i64 { crate::app::ensure_gtk_init(); let area = gtk4::DrawingArea::new(); - if width > 0.0 && height > 0.0 { - area.set_size_request(width as i32, height as i32); + // Only honor finite, sensibly-bounded sizes — NaN/inf or sub-pixel + // fractions would produce a bogus GTK size request. Otherwise expand. + if width.is_finite() && height.is_finite() && width >= 1.0 && height >= 1.0 { + let w = (width as i32).clamp(1, 16384); + let h = (height as i32).clamp(1, 16384); + area.set_size_request(w, h); } else { area.set_hexpand(true); area.set_vexpand(true); diff --git a/crates/perry-ui-ios/src/widgets/bloomview.rs b/crates/perry-ui-ios/src/widgets/bloomview.rs index a24823aaba..d628d7a66d 100644 --- a/crates/perry-ui-ios/src/widgets/bloomview.rs +++ b/crates/perry-ui-ios/src/widgets/bloomview.rs @@ -9,12 +9,18 @@ use objc2::msg_send; use objc2::rc::Retained; use objc2::runtime::AnyClass; +use objc2_foundation::MainThreadMarker; use objc2_ui_kit::UIView; /// Create a BloomView host. `width`/`height` are advisory — the layout engine -/// sizes the view. Returns the widget handle. +/// sizes the view. Returns the widget handle (0 if called off the main thread). pub fn create(width: f64, height: f64) -> i64 { let _ = (width, height); + // UIKit views must be created on the main thread; don't panic across the + // FFI boundary if that contract is violated — return an invalid handle. + let Some(_mtm) = MainThreadMarker::new() else { + return 0; + }; unsafe { let view: Retained = msg_send![AnyClass::get(c"UIView").unwrap(), new]; super::register_widget(view) diff --git a/crates/perry-ui-macos/src/widgets/bloomview.rs b/crates/perry-ui-macos/src/widgets/bloomview.rs index 369308fe48..bb7637d6b4 100644 --- a/crates/perry-ui-macos/src/widgets/bloomview.rs +++ b/crates/perry-ui-macos/src/widgets/bloomview.rs @@ -14,7 +14,11 @@ use objc2_foundation::MainThreadMarker; /// sizes the view. Returns the widget handle. pub fn create(width: f64, height: f64) -> i64 { let _ = (width, height); - let mtm = MainThreadMarker::new().expect("perry/ui must run on the main thread"); + // Public C ABI entry — don't panic across the FFI boundary if called off + // the main thread; return an invalid (0) handle instead. + let Some(mtm) = MainThreadMarker::new() else { + return 0; + }; let view = NSView::new(mtm); super::register_widget(view) } diff --git a/crates/perry-ui-visionos/src/widgets/bloomview.rs b/crates/perry-ui-visionos/src/widgets/bloomview.rs index a24823aaba..d628d7a66d 100644 --- a/crates/perry-ui-visionos/src/widgets/bloomview.rs +++ b/crates/perry-ui-visionos/src/widgets/bloomview.rs @@ -9,12 +9,18 @@ use objc2::msg_send; use objc2::rc::Retained; use objc2::runtime::AnyClass; +use objc2_foundation::MainThreadMarker; use objc2_ui_kit::UIView; /// Create a BloomView host. `width`/`height` are advisory — the layout engine -/// sizes the view. Returns the widget handle. +/// sizes the view. Returns the widget handle (0 if called off the main thread). pub fn create(width: f64, height: f64) -> i64 { let _ = (width, height); + // UIKit views must be created on the main thread; don't panic across the + // FFI boundary if that contract is violated — return an invalid handle. + let Some(_mtm) = MainThreadMarker::new() else { + return 0; + }; unsafe { let view: Retained = msg_send![AnyClass::get(c"UIView").unwrap(), new]; super::register_widget(view)