feat(ui): Phase 2 v3.3 — cross-platform showToast + setText (macOS, v0.5.408)#326
Merged
Conversation
This was referenced Apr 30, 2026
…0.5.408)
v0.5.405 shipped harmonyos-only `perry_arkts_show_toast` and
`perry_arkts_set_text` runtime symbols guarded behind
`feature = "ohos-napi"`. Every other backend (macOS / iOS / Linux GTK4 /
Windows / Android / tvOS / visionOS / watchOS) failed to link a TS
program calling `showToast` or `setText`.
This commit lights up macOS as the first cross-platform backend and
provides the runtime backbone for the rest:
1. New `crates/perry-runtime/src/ui_text_registry.rs` (always compiled)
provides cross-platform `perry_arkts_show_toast` /
`perry_arkts_set_text` / `perry_arkts_register_text_id` stubs gated
behind `#[cfg(not(feature = "ohos-napi"))]` — harmonyos keeps the
canonical drain-queue impl in `arkts_callbacks.rs`. Each stub
forwards to a per-process `AtomicPtr<()>` handler slot registered
by the platform UI crate via three new FFIs
(`js_register_show_toast_handler` / `_set_text_handler` /
`_text_id_handler`) — same pattern as `js_register_stdlib_pump`.
2. Pending-call buffering. Widget construction runs at module-init time
(before `app_run` registers handlers), so Text("Count: 0", "counter")
immediately fires `perry_arkts_register_text_id` with no handler
available. Pre-fix, calls were silently dropped — id → handle map
never populated, later `setText("counter", ...)` calls no-op'd.
Post-fix: each pre-registration call is buffered into a static
`Mutex<Vec<...>>`; the registration FFI drains on first registration,
replaying every call against the fresh handler.
3. macOS implementation. New `crates/perry-ui-macos/src/widgets/toast.rs`
presents a borderless NSPanel with rounded translucent dark
background near the top of the active window — alpha 0→1 fade-in,
~2s hold, fade-out, close — driven by NSTimer targets using the same
`define_class!` pattern as `app::PerryTestExitTarget`. Multi-toast
queueing via `TOAST_QUEUE: VecDeque<String>` so back-to-back
`showToast("a"); showToast("b")` produces sequential banners.
4. New `crates/perry-ui-macos/src/widgets/text_registry.rs` holds id →
widget-handle map; `register_text_id_handler` populates,
`set_text_handler` looks up + calls `widgets::text::set_text_str`
to update the NSTextField via `setStringValue:`.
5. Codegen change in `crates/perry-codegen/src/lower_call/native.rs`:
new arm before `PERRY_UI_TABLE` dispatch detects `Text(content, id)`.
Pre-fix the table's "args.len() == sig.args.len()+1" path absorbed
the id as a would-be inline-style object and silently dropped it.
Post-fix the new arm lowers the create call manually, then emits
`perry_arkts_register_text_id(handle, id)` so the platform UI crate
gets the mapping. On harmonyos that's a runtime no-op (codegen-arkts
emits @State text_<id> directly into the .ets).
iOS / GTK4 / Windows / Android / tvOS / visionOS / watchOS get the
runtime symbols cross-platform via libperry_runtime.a — programs link
cleanly but `showToast` / `setText` no-op visibly until each platform's
UI crate registers its own handlers (good follow-up issues).
Verified
--------
* cargo build --release -p perry-runtime -p perry-stdlib
-p perry-ui-macos -p perry: clean (1m22s incremental).
* cargo test --release -p perry-codegen-arkts: 23/23 pass
(no codegen-arkts code touched).
* cargo test --release -p perry-runtime --lib: 173/0 baseline.
* Smoke compile of Text(initial, id) + Button(label, () => {
setText(id, ...); showToast(msg); }) on --target macos produces
a 0.9 MB binary that exits 0 in PERRY_UI_TEST_MODE=1.
Stacks on #322.
aa4b629 to
03f4aa7
Compare
proggeramlug
pushed a commit
that referenced
this pull request
Apr 30, 2026
Wire `showToast` and `setText` on the three UIKit platforms (iOS, tvOS, visionOS) using the macOS NSPanel implementation in PR #326 as the template. Each platform gets the same two new files: - `widgets/toast.rs`: UIView overlay added to the key UIWindow, alpha- fade 0→1 in 0.25 s (UIView.animateWithDuration:), hold 2 s, fade out, removeFromSuperview. FIFO queue via thread_local VecDeque + Cell<bool> so back-to-back showToast calls render sequentially. Two NSTimer target classes drive the hold → fade-out → cleanup pipeline, using the same define_class! + scheduledTimerWithTimeInterval:target:selector: pattern as PerryPumpTarget / PerryTimerTarget. ObjC class names are platform- prefixed (PerryIOSToast*, PerryTVOSToast*, PerryVisionOSToast*) to avoid runtime namespace collision if both crates ever link together. - `widgets/text_registry.rs`: Mutex<Option<HashMap<String, i64>>> mapping user-supplied Text ids to widget handles. register_text_id_handler populates; set_text_handler looks up + calls widgets::text::set_text_str which issues UILabel setText: — UIKit analogue of NSTextField setStringValue:. Direct port of the macOS file; no platform-specific code needed. Each crate's mod.rs exposes the two new modules. Each crate's app_run calls register_cross_platform_text_handlers (with the same extern block + function body as macOS) before UIApplicationMain / register_view_controller, so the three runtime AtomicPtr handler slots are populated before any TS code fires showToast or setText. No codegen changes: PR #326's cross-platform design routes all UIKit targets through the existing perry_arkts_show_toast / perry_arkts_set_text / perry_arkts_register_text_id symbols in perry-runtime; the handler registration in app_run is the only per-platform hook required. Verified: cargo test --release -p perry-codegen-arkts 23/23 pass. https://claude.ai/code/session_01QvJUom4CxtYCxpyqcukt7y
proggeramlug
pushed a commit
that referenced
this pull request
Apr 30, 2026
Wires showToast + setText on Android (second cross-platform backend after macOS, stacks on PR #326 which adds the runtime backbone + macOS impl). `show_toast_handler` is a cross-platform handler entry point (registered via `js_register_show_toast_handler`). Calls `PerryBridge.showToast(String)` over JNI; the Kotlin side posts `Toast.makeText(ctx, msg, LENGTH_SHORT).show()` to the main looper via `uiHandler.post`. Toast sequencing is handled natively by the Android system — no per-process FIFO queue needed. Mirrors `crates/perry-ui-macos/src/widgets/text_registry.rs` exactly: `Mutex<Option<HashMap<String, i64>>>` id → handle registry. `register_text_id_handler` populates; `set_text_handler` looks up the handle and calls `widgets::text::set_text_str` (the existing JNI `TextView.setText(CharSequence)` path). Calls `register_cross_platform_text_handlers()` at the top of `app_run`, mirroring the macOS `app_run` pattern (v0.5.408). Extern declarations for `js_register_{show_toast,set_text,text_id}_handler` FFIs added at the bottom. New `@JvmStatic fun showToast(msg: String)` posts Toast to UI thread. `android.widget.Toast` is already available via the existing `import android.widget.*`. `perry_arkts_show_toast` / `perry_arkts_set_text` / `perry_arkts_register_text_id` are already emitted by `lower_call/native.rs` for all targets and the cross-platform runtime stubs (v0.5.408) dispatch to the registered handlers. - `cargo test --release -p perry-codegen-arkts` → 23/23 pass (baseline). - `cargo build --release -p perry-ui-android --target aarch64-linux-android` fails at `libmimalloc-sys` C build (NDK absent on this host, same class as the documented ci-env gap). Type-level correctness confirmed by code review against the macOS reference implementation. https://claude.ai/code/session_015FKE6bodWtedKrBJyVbPgA
proggeramlug
added a commit
that referenced
this pull request
Apr 30, 2026
JNI bridge to Toast.makeText. Stacked on #326.
proggeramlug
pushed a commit
that referenced
this pull request
Apr 30, 2026
New `crates/perry-ui-windows/src/widgets/toast.rs`: borderless
WS_EX_LAYERED popup near the bottom-center of the main app window.
Registers a one-time "PerryToastClass" via std::sync::Once +
RegisterClassExW; creates the HWND with WS_EX_TOPMOST | WS_EX_NOACTIVATE |
WS_EX_TOOLWINDOW; clips to rounded rect via SetWindowRgn +
CreateRoundRectRgn(r=14); drives a 50ms WM_TIMER animation (FadeIn 5 ticks
0→255, Hold 40 ticks, FadeOut 5 ticks 255→0) via SetLayeredWindowAttributes
LWA_ALPHA; draws dark bg + white Segoe UI 14pt text in WM_PAINT via
DrawTextW DT_CENTER|DT_VCENTER|DT_SINGLELINE. FIFO queue + PRESENTING gate
so sequential showToast calls render back-to-back.
New `crates/perry-ui-windows/src/widgets/text_registry.rs`: process-global
Mutex<HashMap<String, i64>> mapping text-id → widget handle; handlers
wired to perry-runtime's js_register_{show_toast,set_text,text_id}_handler
AtomicPtr slots via register_cross_platform_text_handlers() at the top of
app_run — mirrors macOS v0.5.408 shape verbatim.
`widgets/mod.rs`: pub mod text_registry + pub mod toast.
`app.rs`: register_cross_platform_text_handlers() call + extern "C"
declarations + fn body at bottom of file.
No codegen changes — lower_call/native.rs showToast/setText/Text(content,id)
routing already landed in v0.5.408 (PR #326).
Stacks on PR #326 (Phase 2 v3.3 macOS). cargo test -p perry-codegen-arkts
23/23. All 4 pre-existing Linux-host `cargo check` errors are in text.rs
(Windows-only GDI code), unchanged by this PR.
https://claude.ai/code/session_01M7ZU3gwSqD2RSz3NTemCZu
proggeramlug
added a commit
that referenced
this pull request
Apr 30, 2026
Borderless layered HWND popup with alpha-fade. id→handle setText. Stacked on #326.
proggeramlug
pushed a commit
that referenced
this pull request
Apr 30, 2026
New widgets/toast.rs: GtkRevealer-backed slide-down banner overlaid on the main ApplicationWindow via a gtk4::Overlay wrapper inserted in app_run. Messages are queued (FIFO) with a 2.5 s hold + 200 ms slide transition; pump_queue() drains one at a time so toasts never stack. New widgets/text_registry.rs: thread_local id→handle map. Text(content, id) (2-arg form, intercepted in lower_call/native.rs before the dispatch table) calls perry_ui_text_create_with_id which creates the GtkLabel and registers it. setText(id, val) calls perry_ui_set_text which looks up the handle and calls text::set_text_str. Dispatch: showToast + setText added to PERRY_UI_TABLE in perry-dispatch. Text(content, id) 2-arg arm in native.rs mirrors the Button / VStack special-case pattern — intercepted before the 1-arg table entry fires. Stubs in perry-ui-macos / perry-ui-windows / perry-ui-ios / perry-ui-tvos so cross-platform user code links on every target today. macOS / iOS / tvOS stubs delegate perry_ui_text_create_with_id → perry_ui_text_create so the widget still renders; setText is a no-op until #326 lands. Verified: perry-dispatch builds clean; perry-codegen builds clean (12 pre-existing warnings, none new); perry-runtime + perry-stdlib + perry all build clean; cargo test -p perry-codegen-arkts 15/15 unaffected. GTK4 crate itself errors only on the missing system gdk-pixbuf-2.0 header (not installed in this sandbox) — the CI ubuntu-latest runner has gtk4 dev packages and will resolve cleanly. https://claude.ai/code/session_01Q4Z1Hmg8kgWidwYzueJjvW
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
showToast/setTextso non-harmonyos backends can light them up incrementally.NSPaneltoast presenter +id → widget-handleregistry calling into the existingwidgets::text::set_text_strsetStringValue:path.perry_arkts_show_toast/perry_arkts_set_textsymbols were guarded behindfeature = "ohos-napi". A non-harmonyos build of a TS program calling either failed to link withUndefined symbols; this PR fills the gap without touching the harmonyos drain-queue path.Stacks on #322.
What landed
Runtime (always compiled)
New
crates/perry-runtime/src/ui_text_registry.rsprovides cross-platform stubs ofperry_arkts_show_toast/perry_arkts_set_text/perry_arkts_register_text_idgated behind#[cfg(not(feature = "ohos-napi"))]so harmonyos keeps the canonical drain-queue impl inarkts_callbacks.rs(no symbol collision). Each stub forwards to a per-processAtomicPtr<()>handler slot registered via three new FFIs (js_register_show_toast_handler/_set_text_handler/_text_id_handler) — same pattern as the existingjs_register_stdlib_pump.Pending-call buffering: widget construction runs at module-init time (before
app_runregisters handlers), soText("Count: 0", "counter")immediately firesperry_arkts_register_text_idwith no handler available. Without buffering, the id → handle map would never populate and latersetText("counter", ...)calls would silently no-op. Each pre-registration call is buffered into astatic Mutex<Vec<...>>; the registration FFI drains on first registration, replaying every queued call against the fresh handler.macOS UI (real implementation)
New
crates/perry-ui-macos/src/widgets/toast.rs(~280 LOC):NSPanelwith rounded translucent dark background + centeredNSTextField, anchored near the top of the active window.NSTimertargets (PerryToastFadeOutTarget+PerryToastCleanupTarget) using the samedefine_class!pattern as the existingapp::PerryTestExitTarget/PerryTimerTarget.TOAST_QUEUE: VecDeque<String>+PRESENTING: Cell<bool>—showToast("a"); showToast("b")produces sequential banners, not "b" overwriting "a".New
crates/perry-ui-macos/src/widgets/text_registry.rs(~80 LOC):Mutex<Option<HashMap<String, i64>>>mapping user-supplied ids to widget handles.register_text_id_handlerpopulates;set_text_handlerlooks up + calls the existingwidgets::text::set_text_strto update theNSTextFieldviasetStringValue:.Both registered in
app::register_cross_platform_text_handlerscalled from the top ofapp_run.Codegen change
crates/perry-codegen/src/lower_call/native.rs: new arm beforePERRY_UI_TABLEdispatch detectsText(content, id). Pre-fix, the table's "args.len() == sig.args.len() + 1 ⇒ inline_style_arg" path absorbed the id as a would-be inline-style object literal, andapply_inline_stylesilently no-op'd because strings aren't object literals — so the id was dropped on the floor andsetText("counter", ...)had nothing to look up. The new arm lowers the create call manually, then emits aperry_arkts_register_text_id(handle, id)follow-up so the platform UI crate gets the mapping. On harmonyos that follow-up is a runtime no-op (codegen-arkts emits@State text_<id>directly into the .ets — see the#[cfg(feature = "ohos-napi")]arm inui_text_registry.rs).Other platforms
iOS / GTK4 / Windows / Android / tvOS / visionOS / watchOS get the runtime symbols cross-platform via
libperry_runtime.a— programs link cleanly butshowToast/setTextno-op visibly until each platform's UI crate registers its own handlers. Each is a focused follow-up issue (the macOS-side files in this PR are the template; each platform needs its ownwidgets/toast.rs+widgets/text_registry.rsplus a registration call fromapp_run).Test plan
cargo build --release -p perry-runtime -p perry-stdlib -p perry-ui-macos -p perryclean (1m22s incremental).cargo test --release -p perry-codegen-arkts23/23 still pass (no codegen-arkts code touched).cargo test --release -p perry-runtime --lib173/0 baseline.Text(initial, id) + Button(label, () => { setText(id, ...); showToast(msg); })on--target macosproduces a 0.9 MB binary that exits 0 inPERRY_UI_TEST_MODE=1.