From 4d61bf72ccc211f1e3e890dd85553f3c1d77a69d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ralph=20K=C3=BCpper?= Date: Mon, 22 Jun 2026 07:03:38 +0200 Subject: [PATCH 1/2] feat(ui): BloomView live-render plumbing on every backend (#5519) Perry-UI side of #5519 (the engine side landed in Bloom-Engine/engine#71 as the platform-neutral bloom_attach_native). Makes a BloomView actually usable as a render surface on every backend, not just Windows. - Rename bloomViewGetHwnd -> bloomViewGetNativeHandle, keeping the old name as a deprecated alias (both dispatch rows route to the same perry_ui_bloomview_get_hwnd runtime symbol). Platform-neutral now that the handle is an NSView*/UIView*/GtkWidget*/ANativeWindow*, not only an HWND. Updated ui_table.rs, api-manifest entries.rs, types/perry/ui/index.d.ts, and regenerated docs/api/perry.d.ts + docs/src/api/reference.md. - Sizing: the non-Windows backends ignored the requested size, so the renderer's surface came up 0x0. BloomView(w,h) now pins the size on macOS/iOS/visionOS/ tvOS (Auto Layout) and Android (LayoutParams); GTK/Windows already did. - tvOS: promoted the 0-handle stub to a real UIView (new widgets/bloomview.rs). - Android: switched View -> SurfaceView; bloomViewGetNativeHandle now returns the real ANativeWindow* (ANativeWindow_fromSurface on the SurfaceHolder surface), 0 until the surface is ready. - Input/focus: the host view is now focusable so a focused BloomView receives key/pointer events for the attached engine (macOS acceptsFirstResponder via a PerryBloomView subclass; iOS/tvOS/visionOS userInteractionEnabled; GTK set_focusable; Android setFocusable). - examples/bloomview_embed_demo.ts: a live Bloom scene inside a Perry UI window (BloomView + bloomViewGetNativeHandle + attachToNSView, driven by onFrame). Verified on macOS: the integrated binary (Perry UI + Bloom engine) links and runs the attach/frame loop without crashing; iOS cross-compiles; tvOS/visionOS host-build; dispatch+manifest tests pass; no api-docs drift. Android/GTK/Windows are best-effort (no local cross-toolchain), mirroring established per-backend patterns. --- CHANGELOG.md | 37 +++++ CLAUDE.md | 2 +- Cargo.lock | 152 +++++++++--------- Cargo.toml | 2 +- crates/perry-api-manifest/src/entries.rs | 4 +- crates/perry-dispatch/src/ui_table.rs | 20 ++- .../perry-ui-android/src/widgets/bloomview.rs | 105 ++++++++++-- crates/perry-ui-gtk4/src/widgets/bloomview.rs | 10 +- crates/perry-ui-ios/src/widgets/bloomview.rs | 25 ++- .../perry-ui-macos/src/widgets/bloomview.rs | 68 ++++++-- crates/perry-ui-tvos/src/ffi/media_extras.rs | 12 +- crates/perry-ui-tvos/src/widgets/bloomview.rs | 43 +++++ crates/perry-ui-tvos/src/widgets/mod.rs | 1 + .../src/widgets/bloomview.rs | 25 ++- docs/api/perry.d.ts | 4 +- docs/src/api/reference.md | 3 +- examples/bloomview_embed_demo.ts | 54 +++++++ types/perry/ui/index.d.ts | 24 ++- 18 files changed, 446 insertions(+), 145 deletions(-) create mode 100644 crates/perry-ui-tvos/src/widgets/bloomview.rs create mode 100644 examples/bloomview_embed_demo.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 7e97fdfda1..60a3e00ac5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,40 @@ +## v0.5.1199 — feat(ui): BloomView live-render plumbing on every backend (#5519) + +Perry-UI side of #5519 (live `BloomView` rendering on all platforms; the engine +side landed in Bloom-Engine/engine#71 as the platform-neutral `bloom_attach_native`). + +- **Rename** `bloomViewGetHwnd` → `bloomViewGetNativeHandle`, keeping + `bloomViewGetHwnd` as a deprecated alias (both dispatch rows route to the same + `perry_ui_bloomview_get_hwnd` runtime symbol). The name is platform-neutral now + that the handle is an `NSView*` / `UIView*` / `GtkWidget*` / `ANativeWindow*`, + not only an HWND. Updated `crates/perry-dispatch/src/ui_table.rs`, + `crates/perry-api-manifest/src/entries.rs`, `types/perry/ui/index.d.ts`, and + regenerated `docs/api/perry.d.ts` + `docs/src/api/reference.md`. +- **Sizing**: the non-Windows backends created the view but ignored the requested + size, so the renderer's surface came up 0×0 and nothing drew. `BloomView(w, h)` + now pins the size — macOS/iOS/visionOS/tvOS via Auto Layout + (`set_width`/`set_height` + `translatesAutoresizingMaskIntoConstraints = false`), + GTK4 already used `set_size_request`, Android via `LayoutParams`, Windows already + reserved a fixed-size child window. +- **tvOS**: promoted the 0-handle stub to a real `UIView` (new + `crates/perry-ui-tvos/src/widgets/bloomview.rs`), mirroring iOS. +- **Android**: switched the host widget from `android.view.View` to + `android.view.SurfaceView`, and `bloomViewGetNativeHandle` now returns the real + `ANativeWindow*` (via NDK `ANativeWindow_fromSurface` on the view's + `SurfaceHolder.getSurface()`) instead of echoing the registry token. Returns 0 + until the surface is ready (laid out / `surfaceCreated`). +- **Input/focus**: the host view is now focusable so a focused `BloomView` can + receive keyboard/pointer events for the attached engine to consume (macOS + `acceptsFirstResponder` via a `PerryBloomView` subclass; iOS/tvOS/visionOS + `userInteractionEnabled`; GTK `set_focusable`/`set_can_target`; Android + `setFocusable`/`setFocusableInTouchMode`). +- **Demo**: `examples/bloomview_embed_demo.ts` renders a live Bloom scene inside a + Perry UI window — `BloomView` + `bloomViewGetNativeHandle` + the engine's + `attachToNSView`, driven from `onFrame`. Verified on macOS: the integrated + binary (Perry UI + Bloom engine) links and runs the attach/frame loop; iOS + cross-compiles. Android/GTK/Windows/tvOS/visionOS are best-effort (no local + cross-toolchain), mirroring established per-backend patterns. + ## v0.5.1198 — fix(packaging): ship `libperry_ui_android.a` to Windows installs so `perry/ui` android apps link (#4823) A Windows user (discussion #4823) with the NDK/SDK and Rust android targets diff --git a/CLAUDE.md b/CLAUDE.md index faa5cb9e0d..20ec3e091d 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -8,7 +8,7 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co Perry is a native TypeScript compiler written in Rust that compiles TypeScript source code directly to native executables. It uses SWC for TypeScript parsing and LLVM for code generation. -**Current Version:** 0.5.1198 +**Current Version:** 0.5.1199 ## TypeScript Parity Status diff --git a/Cargo.lock b/Cargo.lock index 69a5d90ce0..e15e0727d6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5325,7 +5325,7 @@ checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" [[package]] name = "perry" -version = "0.5.1198" +version = "0.5.1199" dependencies = [ "anyhow", "base64", @@ -5382,14 +5382,14 @@ dependencies = [ [[package]] name = "perry-api-manifest" -version = "0.5.1198" +version = "0.5.1199" dependencies = [ "serde", ] [[package]] name = "perry-audio-miniaudio" -version = "0.5.1198" +version = "0.5.1199" dependencies = [ "cc", "libc", @@ -5397,7 +5397,7 @@ dependencies = [ [[package]] name = "perry-codegen" -version = "0.5.1198" +version = "0.5.1199" dependencies = [ "anyhow", "log", @@ -5412,7 +5412,7 @@ dependencies = [ [[package]] name = "perry-codegen-arkts" -version = "0.5.1198" +version = "0.5.1199" dependencies = [ "anyhow", "perry-hir", @@ -5421,7 +5421,7 @@ dependencies = [ [[package]] name = "perry-codegen-glance" -version = "0.5.1198" +version = "0.5.1199" dependencies = [ "anyhow", "perry-hir", @@ -5429,7 +5429,7 @@ dependencies = [ [[package]] name = "perry-codegen-js" -version = "0.5.1198" +version = "0.5.1199" dependencies = [ "anyhow", "perry-dispatch", @@ -5439,7 +5439,7 @@ dependencies = [ [[package]] name = "perry-codegen-swiftui" -version = "0.5.1198" +version = "0.5.1199" dependencies = [ "anyhow", "perry-hir", @@ -5448,7 +5448,7 @@ dependencies = [ [[package]] name = "perry-codegen-wasm" -version = "0.5.1198" +version = "0.5.1199" dependencies = [ "anyhow", "base64", @@ -5461,7 +5461,7 @@ dependencies = [ [[package]] name = "perry-codegen-wear-tiles" -version = "0.5.1198" +version = "0.5.1199" dependencies = [ "anyhow", "perry-hir", @@ -5469,7 +5469,7 @@ dependencies = [ [[package]] name = "perry-container-compose" -version = "0.5.1198" +version = "0.5.1199" dependencies = [ "anyhow", "async-trait", @@ -5498,14 +5498,14 @@ dependencies = [ [[package]] name = "perry-container-e2e" -version = "0.5.1198" +version = "0.5.1199" dependencies = [ "anyhow", ] [[package]] name = "perry-diagnostics" -version = "0.5.1198" +version = "0.5.1199" dependencies = [ "serde", "serde_json", @@ -5513,7 +5513,7 @@ dependencies = [ [[package]] name = "perry-dispatch" -version = "0.5.1198" +version = "0.5.1199" [[package]] name = "perry-doc-fixture-my-bindings" @@ -5524,7 +5524,7 @@ dependencies = [ [[package]] name = "perry-doc-tests" -version = "0.5.1198" +version = "0.5.1199" dependencies = [ "anyhow", "clap", @@ -5539,14 +5539,14 @@ dependencies = [ [[package]] name = "perry-ext-ads" -version = "0.5.1198" +version = "0.5.1199" dependencies = [ "perry-ffi", ] [[package]] name = "perry-ext-argon2" -version = "0.5.1198" +version = "0.5.1199" dependencies = [ "argon2", "perry-ffi", @@ -5554,7 +5554,7 @@ dependencies = [ [[package]] name = "perry-ext-axios" -version = "0.5.1198" +version = "0.5.1199" dependencies = [ "perry-ffi", "reqwest", @@ -5563,7 +5563,7 @@ dependencies = [ [[package]] name = "perry-ext-bcrypt" -version = "0.5.1198" +version = "0.5.1199" dependencies = [ "bcrypt", "perry-ffi", @@ -5571,7 +5571,7 @@ dependencies = [ [[package]] name = "perry-ext-better-sqlite3" -version = "0.5.1198" +version = "0.5.1199" dependencies = [ "perry-ffi", "rusqlite", @@ -5579,7 +5579,7 @@ dependencies = [ [[package]] name = "perry-ext-cheerio" -version = "0.5.1198" +version = "0.5.1199" dependencies = [ "perry-ffi", "scraper", @@ -5587,7 +5587,7 @@ dependencies = [ [[package]] name = "perry-ext-commander" -version = "0.5.1198" +version = "0.5.1199" dependencies = [ "perry-ffi", "perry-runtime", @@ -5595,7 +5595,7 @@ dependencies = [ [[package]] name = "perry-ext-cron" -version = "0.5.1198" +version = "0.5.1199" dependencies = [ "chrono", "cron 0.16.0", @@ -5605,7 +5605,7 @@ dependencies = [ [[package]] name = "perry-ext-dayjs" -version = "0.5.1198" +version = "0.5.1199" dependencies = [ "chrono", "perry-ffi", @@ -5613,7 +5613,7 @@ dependencies = [ [[package]] name = "perry-ext-decimal" -version = "0.5.1198" +version = "0.5.1199" dependencies = [ "perry-ffi", "rust_decimal", @@ -5621,7 +5621,7 @@ dependencies = [ [[package]] name = "perry-ext-dotenv" -version = "0.5.1198" +version = "0.5.1199" dependencies = [ "perry-ffi", "serde_json", @@ -5629,7 +5629,7 @@ dependencies = [ [[package]] name = "perry-ext-ethers" -version = "0.5.1198" +version = "0.5.1199" dependencies = [ "perry-ffi", "rand 0.8.6", @@ -5637,7 +5637,7 @@ dependencies = [ [[package]] name = "perry-ext-events" -version = "0.5.1198" +version = "0.5.1199" dependencies = [ "perry-ffi", "perry-runtime", @@ -5645,14 +5645,14 @@ dependencies = [ [[package]] name = "perry-ext-exponential-backoff" -version = "0.5.1198" +version = "0.5.1199" dependencies = [ "perry-ffi", ] [[package]] name = "perry-ext-fastify" -version = "0.5.1198" +version = "0.5.1199" dependencies = [ "bytes", "http-body-util", @@ -5669,7 +5669,7 @@ dependencies = [ [[package]] name = "perry-ext-fetch" -version = "0.5.1198" +version = "0.5.1199" dependencies = [ "lazy_static", "perry-ffi", @@ -5681,7 +5681,7 @@ dependencies = [ [[package]] name = "perry-ext-http" -version = "0.5.1198" +version = "0.5.1199" dependencies = [ "bytes", "lazy_static", @@ -5695,7 +5695,7 @@ dependencies = [ [[package]] name = "perry-ext-http-server" -version = "0.5.1198" +version = "0.5.1199" dependencies = [ "bytes", "h2", @@ -5718,7 +5718,7 @@ dependencies = [ [[package]] name = "perry-ext-ioredis" -version = "0.5.1198" +version = "0.5.1199" dependencies = [ "lazy_static", "perry-ffi", @@ -5728,7 +5728,7 @@ dependencies = [ [[package]] name = "perry-ext-jsonwebtoken" -version = "0.5.1198" +version = "0.5.1199" dependencies = [ "base64", "jsonwebtoken", @@ -5739,7 +5739,7 @@ dependencies = [ [[package]] name = "perry-ext-lru-cache" -version = "0.5.1198" +version = "0.5.1199" dependencies = [ "lru", "perry-ffi", @@ -5747,7 +5747,7 @@ dependencies = [ [[package]] name = "perry-ext-moment" -version = "0.5.1198" +version = "0.5.1199" dependencies = [ "chrono", "perry-ffi", @@ -5755,7 +5755,7 @@ dependencies = [ [[package]] name = "perry-ext-mongodb" -version = "0.5.1198" +version = "0.5.1199" dependencies = [ "bson", "futures-util", @@ -5767,7 +5767,7 @@ dependencies = [ [[package]] name = "perry-ext-mysql2" -version = "0.5.1198" +version = "0.5.1199" dependencies = [ "chrono", "perry-ffi", @@ -5777,7 +5777,7 @@ dependencies = [ [[package]] name = "perry-ext-nanoid" -version = "0.5.1198" +version = "0.5.1199" dependencies = [ "nanoid", "perry-ffi", @@ -5786,7 +5786,7 @@ dependencies = [ [[package]] name = "perry-ext-net" -version = "0.5.1198" +version = "0.5.1199" dependencies = [ "perry-ffi", "perry-runtime", @@ -5798,7 +5798,7 @@ dependencies = [ [[package]] name = "perry-ext-nodemailer" -version = "0.5.1198" +version = "0.5.1199" dependencies = [ "lettre", "perry-ffi", @@ -5808,7 +5808,7 @@ dependencies = [ [[package]] name = "perry-ext-pdf" -version = "0.5.1198" +version = "0.5.1199" dependencies = [ "perry-ffi", "printpdf", @@ -5816,7 +5816,7 @@ dependencies = [ [[package]] name = "perry-ext-pg" -version = "0.5.1198" +version = "0.5.1199" dependencies = [ "perry-ffi", "sqlx", @@ -5825,7 +5825,7 @@ dependencies = [ [[package]] name = "perry-ext-ratelimit" -version = "0.5.1198" +version = "0.5.1199" dependencies = [ "governor", "perry-ffi", @@ -5833,7 +5833,7 @@ dependencies = [ [[package]] name = "perry-ext-sharp" -version = "0.5.1198" +version = "0.5.1199" dependencies = [ "fast_image_resize", "image", @@ -5843,14 +5843,14 @@ dependencies = [ [[package]] name = "perry-ext-slugify" -version = "0.5.1198" +version = "0.5.1199" dependencies = [ "perry-ffi", ] [[package]] name = "perry-ext-streams" -version = "0.5.1198" +version = "0.5.1199" dependencies = [ "lazy_static", "perry-ffi", @@ -5859,7 +5859,7 @@ dependencies = [ [[package]] name = "perry-ext-uuid" -version = "0.5.1198" +version = "0.5.1199" dependencies = [ "perry-ffi", "uuid", @@ -5867,7 +5867,7 @@ dependencies = [ [[package]] name = "perry-ext-validator" -version = "0.5.1198" +version = "0.5.1199" dependencies = [ "perry-ffi", "regex", @@ -5877,7 +5877,7 @@ dependencies = [ [[package]] name = "perry-ext-ws" -version = "0.5.1198" +version = "0.5.1199" dependencies = [ "futures-util", "lazy_static", @@ -5889,7 +5889,7 @@ dependencies = [ [[package]] name = "perry-ext-zlib" -version = "0.5.1198" +version = "0.5.1199" dependencies = [ "brotli", "flate2", @@ -5898,7 +5898,7 @@ dependencies = [ [[package]] name = "perry-ffi" -version = "0.5.1198" +version = "0.5.1199" dependencies = [ "dashmap", "once_cell", @@ -5907,7 +5907,7 @@ dependencies = [ [[package]] name = "perry-hir" -version = "0.5.1198" +version = "0.5.1199" dependencies = [ "anyhow", "perry-api-manifest", @@ -5925,7 +5925,7 @@ dependencies = [ [[package]] name = "perry-parser" -version = "0.5.1198" +version = "0.5.1199" dependencies = [ "anyhow", "perry-diagnostics", @@ -5937,7 +5937,7 @@ dependencies = [ [[package]] name = "perry-runtime" -version = "0.5.1198" +version = "0.5.1199" dependencies = [ "anyhow", "base64", @@ -5970,14 +5970,14 @@ dependencies = [ [[package]] name = "perry-runtime-static" -version = "0.5.1198" +version = "0.5.1199" dependencies = [ "perry-runtime", ] [[package]] name = "perry-stdlib" -version = "0.5.1198" +version = "0.5.1199" dependencies = [ "aes 0.8.4", "aes-gcm", @@ -6069,14 +6069,14 @@ dependencies = [ [[package]] name = "perry-stdlib-static" -version = "0.5.1198" +version = "0.5.1199" dependencies = [ "perry-stdlib", ] [[package]] name = "perry-transform" -version = "0.5.1198" +version = "0.5.1199" dependencies = [ "anyhow", "perry-hir", @@ -6086,7 +6086,7 @@ dependencies = [ [[package]] name = "perry-types" -version = "0.5.1198" +version = "0.5.1199" dependencies = [ "anyhow", "thiserror 1.0.69", @@ -6094,14 +6094,14 @@ dependencies = [ [[package]] name = "perry-ui" -version = "0.5.1198" +version = "0.5.1199" dependencies = [ "perry-ui-model", ] [[package]] name = "perry-ui-android" -version = "0.5.1198" +version = "0.5.1199" dependencies = [ "base64", "itoa", @@ -6118,7 +6118,7 @@ dependencies = [ [[package]] name = "perry-ui-geisterhand" -version = "0.5.1198" +version = "0.5.1199" dependencies = [ "rand 0.8.6", "serde", @@ -6128,7 +6128,7 @@ dependencies = [ [[package]] name = "perry-ui-gtk4" -version = "0.5.1198" +version = "0.5.1199" dependencies = [ "base64", "cairo-rs", @@ -6151,7 +6151,7 @@ dependencies = [ [[package]] name = "perry-ui-ios" -version = "0.5.1198" +version = "0.5.1199" dependencies = [ "base64", "block2", @@ -6167,7 +6167,7 @@ dependencies = [ [[package]] name = "perry-ui-macos" -version = "0.5.1198" +version = "0.5.1199" dependencies = [ "base64", "block2", @@ -6182,7 +6182,7 @@ dependencies = [ [[package]] name = "perry-ui-model" -version = "0.5.1198" +version = "0.5.1199" [[package]] name = "perry-ui-test" @@ -6190,11 +6190,11 @@ version = "0.1.0" [[package]] name = "perry-ui-testkit" -version = "0.5.1198" +version = "0.5.1199" [[package]] name = "perry-ui-tvos" -version = "0.5.1198" +version = "0.5.1199" dependencies = [ "base64", "block2", @@ -6210,7 +6210,7 @@ dependencies = [ [[package]] name = "perry-ui-visionos" -version = "0.5.1198" +version = "0.5.1199" dependencies = [ "base64", "block2", @@ -6226,7 +6226,7 @@ dependencies = [ [[package]] name = "perry-ui-watchos" -version = "0.5.1198" +version = "0.5.1199" dependencies = [ "block2", "libc", @@ -6239,7 +6239,7 @@ dependencies = [ [[package]] name = "perry-ui-windows" -version = "0.5.1198" +version = "0.5.1199" dependencies = [ "base64", "libc", @@ -6256,14 +6256,14 @@ dependencies = [ [[package]] name = "perry-ui-windows-winui" -version = "0.5.1198" +version = "0.5.1199" dependencies = [ "perry-ui-windows", ] [[package]] name = "perry-updater" -version = "0.5.1198" +version = "0.5.1199" dependencies = [ "base64", "ed25519-dalek", @@ -6277,7 +6277,7 @@ dependencies = [ [[package]] name = "perry-wasm-host" -version = "0.5.1198" +version = "0.5.1199" dependencies = [ "wasmi", ] diff --git a/Cargo.toml b/Cargo.toml index f60fa8bcda..f0ed3f6fb5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -301,7 +301,7 @@ strip = false codegen-units = 16 [workspace.package] -version = "0.5.1198" +version = "0.5.1199" edition = "2021" license = "MIT" repository = "https://github.com/PerryTS/perry" diff --git a/crates/perry-api-manifest/src/entries.rs b/crates/perry-api-manifest/src/entries.rs index 8a30f58fbf..af260f9161 100644 --- a/crates/perry-api-manifest/src/entries.rs +++ b/crates/perry-api-manifest/src/entries.rs @@ -4943,8 +4943,10 @@ 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) + // Issue #2395 / #5519 — BloomView (embed an external GPU renderer / Bloom engine) method("perry/ui", "BloomView", false, None), + method("perry/ui", "bloomViewGetNativeHandle", false, None), + // Deprecated alias for bloomViewGetNativeHandle (#5519). method("perry/ui", "bloomViewGetHwnd", false, None), method("perry/ui", "CameraView", false, None), method("perry/ui", "cameraStart", false, None), diff --git a/crates/perry-dispatch/src/ui_table.rs b/crates/perry-dispatch/src/ui_table.rs index f076cfd8a6..e168a9f4da 100644 --- a/crates/perry-dispatch/src/ui_table.rs +++ b/crates/perry-dispatch/src/ui_table.rs @@ -1983,16 +1983,28 @@ 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. + // ---- BloomView (issue #2395 / #5519) ---- + // A render-surface host: `BloomView(width, height)` reserves a native view + // the Bloom engine draws into. `bloomViewGetNativeHandle(view)` returns the + // platform handle (HWND / NSView* / UIView* / GtkWidget* / ANativeWindow*) + // as a JS number so user TS can call the engine's attach (`attachToNSView` + // / `attachToSurface` / …, all forwarding to `bloom_attach_native`). MethodRow { method: "BloomView", runtime: "perry_ui_bloomview_create", args: &[ArgKind::F64, ArgKind::F64], ret: ReturnKind::Widget, }, + // Canonical name since #5519 — platform-neutral now that the handle is an + // NSView*/UIView*/GtkWidget*/ANativeWindow*, not only an HWND. + MethodRow { + method: "bloomViewGetNativeHandle", + runtime: "perry_ui_bloomview_get_hwnd", + args: &[ArgKind::Widget], + ret: ReturnKind::I64AsF64, + }, + // Deprecated alias — kept so existing code keeps working. Same runtime + // symbol as `bloomViewGetNativeHandle`. MethodRow { method: "bloomViewGetHwnd", runtime: "perry_ui_bloomview_get_hwnd", diff --git a/crates/perry-ui-android/src/widgets/bloomview.rs b/crates/perry-ui-android/src/widgets/bloomview.rs index 6951e79ea7..7da8df9089 100644 --- a/crates/perry-ui-android/src/widgets/bloomview.rs +++ b/crates/perry-ui-android/src/widgets/bloomview.rs @@ -1,23 +1,37 @@ -//! BloomView — a native render-surface host widget (issue #2395). +//! BloomView — a native render-surface host widget (issue #2395 / #5519). //! -//! 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). +//! Reserves an `android.view.SurfaceView` 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; `bloomViewGetNativeHandle` returns the `ANativeWindow*` of the +//! view's `Surface` (via `ANativeWindow_fromSurface`), which user TypeScript +//! hands to the engine's `attachToSurface` → `bloom_attach_native`. +//! +//! Note: a `SurfaceView`'s `Surface` only becomes valid once it's laid out and +//! `surfaceCreated` has fired, so `get_native_handle` returns 0 until then — the +//! host should attach after the view is on screen (or retry). 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 { +// NDK libandroid: turn a Java `android.view.Surface` into a native window the +// GPU backend (wgpu) can build a swapchain on. +#[link(name = "android")] +extern "C" { + fn ANativeWindow_fromSurface( + env: *mut jni::sys::JNIEnv, + surface: jni::sys::jobject, + ) -> *mut std::ffi::c_void; +} + +/// Create a BloomView host sized `width` × `height` dp. 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", + "android/view/SurfaceView", "(Landroid/content/Context;)V", &[JValue::Object(&activity)], ) { @@ -30,6 +44,10 @@ pub fn create(_width: f64, _height: f64) -> i64 { } }; + // Let the host view take focus so the attached engine can route key/touch. + let _ = env.call_method(&view, "setFocusable", "(Z)V", &[JValue::Bool(1)]); + let _ = env.call_method(&view, "setFocusableInTouchMode", "(Z)V", &[JValue::Bool(1)]); + let global_ref = match env.new_global_ref(&view) { Ok(g) => g, Err(_) => { @@ -43,15 +61,68 @@ pub fn create(_width: f64, _height: f64) -> i64 { let _ = env.pop_local_frame(&jni::objects::JObject::null()); } - super::register_widget(global_ref) + let handle = super::register_widget(global_ref); + if width.is_finite() && width >= 1.0 { + super::set_width(handle, width); + } + if height.is_finite() && height >= 1.0 { + super::set_height(handle, height); + } + handle } -/// Android has no HWND; echo the registry handle as a stable token for the -/// caller — but validate it first (return 0 for an unknown/stale handle), so -/// downstream renderers never treat a bogus handle as attachable. +/// Return the `ANativeWindow*` of the BloomView's `Surface` as an integer, for +/// handing to an external GPU renderer. Returns 0 if the handle is unknown or +/// the surface isn't ready yet (not laid out / `surfaceCreated` not fired). +/// +/// `ANativeWindow_fromSurface` returns a window with a +1 reference; the caller +/// (the engine's attach) acquires its own reference, so this one is intentionally +/// left for the host to manage over the BloomView's lifetime. pub fn get_native_handle(handle: i64) -> i64 { - match super::get_widget(handle) { - Some(_) => handle, - None => 0, + let Some(view_ref) = super::get_widget(handle) else { + return 0; + }; + let mut env = jni_bridge::get_env(); + let _ = env.push_local_frame(8); + + let result = (|| -> Option { + // holder = surfaceView.getHolder() + let holder = env + .call_method( + view_ref.as_obj(), + "getHolder", + "()Landroid/view/SurfaceHolder;", + &[], + ) + .ok()? + .l() + .ok()?; + // surface = holder.getSurface() + let surface = env + .call_method(&holder, "getSurface", "()Landroid/view/Surface;", &[]) + .ok()? + .l() + .ok()?; + // Bail unless the surface is backed by a live buffer queue. + let valid = env + .call_method(&surface, "isValid", "()Z", &[]) + .ok()? + .z() + .ok()?; + if !valid { + return None; + } + let env_raw = env.get_native_interface(); + let win = unsafe { ANativeWindow_fromSurface(env_raw, surface.as_raw()) }; + if win.is_null() { + None + } else { + Some(win as i64) + } + })(); + + unsafe { + let _ = env.pop_local_frame(&jni::objects::JObject::null()); } + result.unwrap_or(0) } diff --git a/crates/perry-ui-gtk4/src/widgets/bloomview.rs b/crates/perry-ui-gtk4/src/widgets/bloomview.rs index 5e31a4c762..8d4a3b17ad 100644 --- a/crates/perry-ui-gtk4/src/widgets/bloomview.rs +++ b/crates/perry-ui-gtk4/src/widgets/bloomview.rs @@ -2,9 +2,9 @@ //! //! 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 +//! and exposes its `GtkWidget*` via `bloomViewGetNativeHandle`; 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::*; @@ -24,6 +24,10 @@ pub fn create(width: f64, height: f64) -> i64 { area.set_hexpand(true); area.set_vexpand(true); } + // Let the host widget take keyboard focus + pointer events so the attached + // engine can route input (#5519). + area.set_focusable(true); + area.set_can_target(true); super::register_widget(area.upcast()) } diff --git a/crates/perry-ui-ios/src/widgets/bloomview.rs b/crates/perry-ui-ios/src/widgets/bloomview.rs index d628d7a66d..33e4c517b7 100644 --- a/crates/perry-ui-ios/src/widgets/bloomview.rs +++ b/crates/perry-ui-ios/src/widgets/bloomview.rs @@ -2,9 +2,9 @@ //! //! 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*`. +//! and exposes its pointer via `bloomViewGetNativeHandle`; 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; @@ -12,10 +12,10 @@ 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 (0 if called off the main thread). +/// Create a BloomView host sized `width` × `height` points. Returns the widget +/// handle (0 if called off the main thread). The size is pinned via Auto Layout +/// so the renderer's surface comes up non-zero (#5519). 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 { @@ -23,7 +23,18 @@ pub fn create(width: f64, height: f64) -> i64 { }; unsafe { let view: Retained = msg_send![AnyClass::get(c"UIView").unwrap(), new]; - super::register_widget(view) + // Auto Layout drives the size (set_width/set_height below). + let _: () = msg_send![&view, setTranslatesAutoresizingMaskIntoConstraints: false]; + // Let the host view receive touches so the attached engine can route input. + let _: () = msg_send![&view, setUserInteractionEnabled: true]; + let handle = super::register_widget(view); + if width.is_finite() && width >= 1.0 { + super::set_width(handle, width); + } + if height.is_finite() && height >= 1.0 { + super::set_height(handle, height); + } + handle } } diff --git a/crates/perry-ui-macos/src/widgets/bloomview.rs b/crates/perry-ui-macos/src/widgets/bloomview.rs index bb7637d6b4..297ad1611d 100644 --- a/crates/perry-ui-macos/src/widgets/bloomview.rs +++ b/crates/perry-ui-macos/src/widgets/bloomview.rs @@ -1,26 +1,68 @@ -//! BloomView — a native render-surface host widget (issue #2395). +//! BloomView — a native render-surface host widget (issue #2395 / #5519). //! -//! 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*`. +//! Reserves an `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 `bloomViewGetNativeHandle`; 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*`. +//! +//! The view is a `PerryBloomView` subclass that accepts first-responder status +//! so the window routes keyboard/mouse here once focused — the attached engine +//! overrides the event selectors to consume them (mirrors Bloom subclassing the +//! Windows host wndproc). +use objc2::rc::Retained; +use objc2::{define_class, msg_send, MainThreadOnly}; 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 struct PerryBloomViewIvars; + +define_class!( + #[unsafe(super(NSView))] + #[name = "PerryBloomView"] + #[ivars = PerryBloomViewIvars] + pub struct PerryBloomView; + + impl PerryBloomView { + // Accept first-responder status so a focused BloomView receives + // keyDown:/mouse events (a stock NSView returns NO and never would). + #[unsafe(method(acceptsFirstResponder))] + fn accepts_first_responder(&self) -> bool { + true + } + } +); + +/// Create a BloomView host sized `width` × `height` points. Returns the widget +/// handle. The size is pinned via Auto Layout so the renderer's surface comes +/// up non-zero even before the layout engine positions the view (#5519 — a +/// zero-frame view yields a 0×0 surface and nothing renders). pub fn create(width: f64, height: f64) -> i64 { - let _ = (width, height); // 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) + let view: Retained = { + let this = PerryBloomView::alloc(mtm).set_ivars(PerryBloomViewIvars); + unsafe { msg_send![super(this), init] } + }; + // Auto Layout drives the size (set_width/set_height below); opt out of the + // autoresizing-mask→constraints translation so those constraints take hold. + unsafe { + let _: () = msg_send![&view, setTranslatesAutoresizingMaskIntoConstraints: false]; + } + let ns: Retained = unsafe { Retained::cast_unchecked(view) }; + let handle = super::register_widget(ns); + if width.is_finite() && width >= 1.0 { + super::set_width(handle, width); + } + if height.is_finite() && height >= 1.0 { + super::set_height(handle, height); + } + handle } /// Return the raw `NSView*` for a BloomView handle as an integer, for handing @@ -28,7 +70,7 @@ pub fn create(width: f64, height: f64) -> i64 { /// 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, + Some(view) => Retained::as_ptr(&view) as i64, None => 0, } } diff --git a/crates/perry-ui-tvos/src/ffi/media_extras.rs b/crates/perry-ui-tvos/src/ffi/media_extras.rs index af6ae2f06c..a19f4e34f6 100644 --- a/crates/perry-ui-tvos/src/ffi/media_extras.rs +++ b/crates/perry-ui-tvos/src/ffi/media_extras.rs @@ -211,15 +211,15 @@ pub extern "C" fn perry_ui_webview_create( 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. +// BloomView (issue #2395 / #5519) — a real `UIView` render-surface host the +// Bloom engine attaches its Metal surface to (via `attachToUIView`). #[no_mangle] -pub extern "C" fn perry_ui_bloomview_create(_width: f64, _height: f64) -> i64 { - 0 +pub extern "C" fn perry_ui_bloomview_create(width: f64, height: f64) -> i64 { + crate::widgets::bloomview::create(width, height) } #[no_mangle] -pub extern "C" fn perry_ui_bloomview_get_hwnd(_handle: i64) -> i64 { - 0 +pub extern "C" fn perry_ui_bloomview_get_hwnd(handle: i64) -> i64 { + crate::widgets::bloomview::get_native_handle(handle) } #[no_mangle] diff --git a/crates/perry-ui-tvos/src/widgets/bloomview.rs b/crates/perry-ui-tvos/src/widgets/bloomview.rs new file mode 100644 index 0000000000..044bda024e --- /dev/null +++ b/crates/perry-ui-tvos/src/widgets/bloomview.rs @@ -0,0 +1,43 @@ +//! BloomView — a native render-surface host widget (issue #2395 / #5519). +//! +//! tvOS promotes the former 0-handle stub to a real `UIView` that the Bloom +//! engine attaches its Metal surface to (via `attachToUIView` → +//! `bloom_attach_native`), mirroring the iOS implementation. Perry UI only owns +//! the view and exposes its pointer via `bloomViewGetNativeHandle`. + +use objc2::msg_send; +use objc2::rc::Retained; +use objc2::runtime::AnyClass; +use objc2_ui_kit::UIView; + +/// Create a BloomView host sized `width` × `height` points. The size is pinned +/// via Auto Layout so the renderer's surface comes up non-zero. Returns the +/// widget handle. +pub fn create(width: f64, height: f64) -> i64 { + unsafe { + let view: Retained = msg_send![AnyClass::get(c"UIView").unwrap(), new]; + // Auto Layout drives the size (set_width/set_height below). + let _: () = msg_send![&*view, setTranslatesAutoresizingMaskIntoConstraints: false]; + // Let the host view participate in input so the attached engine can + // route the focus engine / Siri Remote events. + let _: () = msg_send![&*view, setUserInteractionEnabled: true]; + let handle = super::register_widget(view); + if width.is_finite() && width >= 1.0 { + super::set_width(handle, width); + } + if height.is_finite() && height >= 1.0 { + super::set_height(handle, height); + } + handle + } +} + +/// 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-tvos/src/widgets/mod.rs b/crates/perry-ui-tvos/src/widgets/mod.rs index f02bc0a968..94f76a8b1d 100644 --- a/crates/perry-ui-tvos/src/widgets/mod.rs +++ b/crates/perry-ui-tvos/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 canvas; diff --git a/crates/perry-ui-visionos/src/widgets/bloomview.rs b/crates/perry-ui-visionos/src/widgets/bloomview.rs index d628d7a66d..33e4c517b7 100644 --- a/crates/perry-ui-visionos/src/widgets/bloomview.rs +++ b/crates/perry-ui-visionos/src/widgets/bloomview.rs @@ -2,9 +2,9 @@ //! //! 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*`. +//! and exposes its pointer via `bloomViewGetNativeHandle`; 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; @@ -12,10 +12,10 @@ 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 (0 if called off the main thread). +/// Create a BloomView host sized `width` × `height` points. Returns the widget +/// handle (0 if called off the main thread). The size is pinned via Auto Layout +/// so the renderer's surface comes up non-zero (#5519). 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 { @@ -23,7 +23,18 @@ pub fn create(width: f64, height: f64) -> i64 { }; unsafe { let view: Retained = msg_send![AnyClass::get(c"UIView").unwrap(), new]; - super::register_widget(view) + // Auto Layout drives the size (set_width/set_height below). + let _: () = msg_send![&view, setTranslatesAutoresizingMaskIntoConstraints: false]; + // Let the host view receive touches so the attached engine can route input. + let _: () = msg_send![&view, setUserInteractionEnabled: true]; + let handle = super::register_widget(view); + if width.is_finite() && width >= 1.0 { + super::set_width(handle, width); + } + if height.is_finite() && height >= 1.0 { + super::set_height(handle, height); + } + handle } } diff --git a/docs/api/perry.d.ts b/docs/api/perry.d.ts index 06e1270275..7ce193d81b 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: 1945 entries across 113 modules +// Coverage: 1946 entries across 113 modules type PerryU32 = number & { readonly __perryU32?: never }; type PerryU64 = number & { readonly __perryU64?: never }; @@ -2868,6 +2868,8 @@ declare module "perry/ui" { /** stdlib */ export function bloomViewGetHwnd(...args: any[]): any; /** stdlib */ + export function bloomViewGetNativeHandle(...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 3703c3a345..41ab2d2510 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: 2819 entries across 115 modules. +Total: 2820 entries across 115 modules. ## Modules @@ -2569,6 +2569,7 @@ Total: 2819 entries across 115 modules. - `attributedTextAppend` — module - `attributedTextClear` — module - `bloomViewGetHwnd` — module +- `bloomViewGetNativeHandle` — module - `blur` — module - `bottomNavAddItem` — module - `bottomNavSetBadge` — module diff --git a/examples/bloomview_embed_demo.ts b/examples/bloomview_embed_demo.ts new file mode 100644 index 0000000000..c6ea683bb5 --- /dev/null +++ b/examples/bloomview_embed_demo.ts @@ -0,0 +1,54 @@ +// BloomView embed demo (#5519) — render a live Bloom scene inside a Perry UI +// window on any platform Bloom supports (not just Windows). +// +// How it fits together: +// * Perry UI owns the native window and the run loop (`App(...)`). +// * `BloomView(w, h)` reserves a native render-surface view in the view tree +// and sizes it (macOS/iOS/visionOS/tvOS via Auto Layout, GTK via +// set_size_request, Android via a SurfaceView's LayoutParams, Windows via a +// fixed-size child HWND). +// * `bloomViewGetNativeHandle(view)` returns that view's platform handle +// (NSView* / UIView* / GtkWidget* / ANativeWindow* / HWND). +// * The Bloom engine attaches its GPU surface to that handle +// (`attachToNSView` / `attachToUIView` / `attachToSurface`, all forwarding +// to `bloom_attach_native`), then renders when the host drives frames. +// * `onFrame(...)` drives the engine's frame loop from Perry UI's run loop. +// +// Build (macOS), with `@bloomengine/engine` resolvable (a `"bloom": "file:…"` +// dependency or `perry.compilePackages`): +// perry examples/bloomview_embed_demo.ts -o demo && ./demo + +import { App, VStack, BloomView, bloomViewGetNativeHandle, onFrame } from "perry/ui"; +import { attachToNSView, beginDrawing, endDrawing, clearBackground, Colors } from "bloom/core"; +import { drawCircle } from "bloom/shapes"; + +const WIDTH = 800; +const HEIGHT = 600; +const view = BloomView(WIDTH, HEIGHT); + +let attached = false; +let t = 0; + +function frame(_timestampMs: number, deltaMs: number): void { + // The native view exists immediately, but its handle is only usable once the + // window is on screen — attach on the first frame the handle is non-zero. + if (!attached) { + const handle = bloomViewGetNativeHandle(view); + if (handle !== 0) { + attached = attachToNSView(handle, WIDTH, HEIGHT); + } + } + if (attached) { + t = t + deltaMs * 0.001; + beginDrawing(); + clearBackground(Colors.DARKBLUE); + const x = WIDTH / 2 + Math.cos(t) * 220; + const y = HEIGHT / 2 + Math.sin(t) * 140; + drawCircle(x, y, 60, Colors.GOLD); + endDrawing(); + } + onFrame(frame); // re-arm for the next frame +} + +onFrame(frame); +App({ title: "BloomView Embed (#5519)", width: WIDTH, height: HEIGHT, body: VStack([view]) }); diff --git a/types/perry/ui/index.d.ts b/types/perry/ui/index.d.ts index d44786c0d4..920cb76ada 100644 --- a/types/perry/ui/index.d.ts +++ b/types/perry/ui/index.d.ts @@ -424,17 +424,27 @@ export function Canvas(width: number, height: number): Canvas; * * `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 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). + * `bloomViewGetNativeHandle(view)` to a renderer such as the Bloom engine + * (`attachToNSView` / `attachToSurface` / …), 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 SurfaceView; tvOS real + * view, watchOS links as a no-op). */ export function BloomView(width: number, height: number): Widget; /** - * 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. + * The `BloomView`'s native render-surface handle as a number — the platform's + * view/window pointer: `HWND` on Windows, `NSView*`/`UIView*` on Apple, + * `GtkWidget*` on GTK4, `ANativeWindow*` on Android. Hand this to the Bloom + * engine's attach call (`attachToNSView` / `attachToUIView` / `attachToSurface`, + * all forwarding to `bloom_attach_native`). + */ +export function bloomViewGetNativeHandle(view: Widget): number; + +/** + * @deprecated Renamed to {@link bloomViewGetNativeHandle} in #5519 — the handle + * is no longer Windows-only (it's an NSView*/UIView*/GtkWidget*/ANativeWindow* + * on the other platforms). This alias still works and returns the same value. */ export function bloomViewGetHwnd(view: Widget): number; From 553125f494550e81a88876d94cfda8dc1d446c08 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ralph=20K=C3=BCpper?= Date: Mon, 22 Jun 2026 08:04:46 +0200 Subject: [PATCH 2/2] fix(ui): address CodeRabbit review on #5538 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - android BloomView: cache the ANativeWindow* per handle instead of calling ANativeWindow_fromSurface (which returns a +1-refcount window) on every get_native_handle poll — that leaked a window reference per call. The first ready surface builds the window; later calls reuse the cached pointer. - tvos BloomView create(): add the MainThreadMarker main-thread guard the iOS path has (UIKit view creation must be on the main thread; return 0 otherwise). - types/perry/ui/index.d.ts: the deprecated bloomViewGetHwnd JSDoc had a raw `NSView*/UIView*/…` whose `*/` prematurely closed the block comment — backtick the type names so the sequence no longer terminates the comment. --- .../perry-ui-android/src/widgets/bloomview.rs | 35 ++++++++++++++++--- crates/perry-ui-tvos/src/widgets/bloomview.rs | 8 ++++- types/perry/ui/index.d.ts | 5 +-- 3 files changed, 41 insertions(+), 7 deletions(-) diff --git a/crates/perry-ui-android/src/widgets/bloomview.rs b/crates/perry-ui-android/src/widgets/bloomview.rs index 7da8df9089..04e4b31778 100644 --- a/crates/perry-ui-android/src/widgets/bloomview.rs +++ b/crates/perry-ui-android/src/widgets/bloomview.rs @@ -12,6 +12,7 @@ use crate::jni_bridge; use jni::objects::JValue; +use std::sync::Mutex; // NDK libandroid: turn a Java `android.view.Surface` into a native window the // GPU backend (wgpu) can build a swapchain on. @@ -23,6 +24,14 @@ extern "C" { ) -> *mut std::ffi::c_void; } +// `ANativeWindow_fromSurface` returns a window with a +1 reference, so creating +// one per `get_native_handle` call (the host polls it every frame until the +// surface is ready) would leak a reference each time. Cache the first window we +// build per widget handle and hand the same pointer back on later calls — the +// single retained reference lives for the BloomView's lifetime. Keyed by the +// registry handle; a small Vec since an app has at most a handful of BloomViews. +static BLOOM_WINDOWS: Mutex> = Mutex::new(Vec::new()); + /// Create a BloomView host sized `width` × `height` dp. Returns the widget /// handle, or 0 on JNI failure. pub fn create(width: f64, height: f64) -> i64 { @@ -75,10 +84,18 @@ pub fn create(width: f64, height: f64) -> i64 { /// handing to an external GPU renderer. Returns 0 if the handle is unknown or /// the surface isn't ready yet (not laid out / `surfaceCreated` not fired). /// -/// `ANativeWindow_fromSurface` returns a window with a +1 reference; the caller -/// (the engine's attach) acquires its own reference, so this one is intentionally -/// left for the host to manage over the BloomView's lifetime. +/// The `ANativeWindow*` is created once per BloomView (on the first call that +/// finds a ready surface) and cached; later calls return the same pointer, so +/// the host polling this every frame doesn't leak a window reference per call. +/// The cached reference is released when the engine's attach takes over and on +/// process teardown. pub fn get_native_handle(handle: i64) -> i64 { + // Return the window we already built for this view, if any. + if let Ok(cache) = BLOOM_WINDOWS.lock() { + if let Some(&(_, win)) = cache.iter().find(|&&(h, _)| h == handle) { + return win; + } + } let Some(view_ref) = super::get_widget(handle) else { return 0; }; @@ -124,5 +141,15 @@ pub fn get_native_handle(handle: i64) -> i64 { unsafe { let _ = env.pop_local_frame(&jni::objects::JObject::null()); } - result.unwrap_or(0) + // Cache the window so the next poll reuses this reference instead of + // acquiring a fresh one (only on success — keep retrying while not ready). + if let Some(win) = result { + if let Ok(mut cache) = BLOOM_WINDOWS.lock() { + if !cache.iter().any(|&(h, _)| h == handle) { + cache.push((handle, win)); + } + } + return win; + } + 0 } diff --git a/crates/perry-ui-tvos/src/widgets/bloomview.rs b/crates/perry-ui-tvos/src/widgets/bloomview.rs index 044bda024e..fb64cc8287 100644 --- a/crates/perry-ui-tvos/src/widgets/bloomview.rs +++ b/crates/perry-ui-tvos/src/widgets/bloomview.rs @@ -8,12 +8,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 sized `width` × `height` points. The size is pinned /// via Auto Layout so the renderer's surface comes up non-zero. Returns the -/// widget handle. +/// widget handle (0 if called off the main thread). pub fn create(width: f64, height: f64) -> i64 { + // 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]; // Auto Layout drives the size (set_width/set_height below). diff --git a/types/perry/ui/index.d.ts b/types/perry/ui/index.d.ts index 920cb76ada..8ee0aab517 100644 --- a/types/perry/ui/index.d.ts +++ b/types/perry/ui/index.d.ts @@ -443,8 +443,9 @@ export function bloomViewGetNativeHandle(view: Widget): number; /** * @deprecated Renamed to {@link bloomViewGetNativeHandle} in #5519 — the handle - * is no longer Windows-only (it's an NSView*/UIView*/GtkWidget*/ANativeWindow* - * on the other platforms). This alias still works and returns the same value. + * is no longer Windows-only (it's an `NSView*`/`UIView*`/`GtkWidget*`/ + * `ANativeWindow*` on the other platforms). This alias still works and returns + * the same value. */ export function bloomViewGetHwnd(view: Widget): number;