Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 46 additions & 2 deletions docs/workstation/native-webview-scale-system--0622.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
---
title: Native WebView Scale System
status: active
last_updated: 2026-06-13
last_updated: 2026-08-25
---

# Native WebView Scale System
Expand Down Expand Up @@ -73,7 +73,7 @@ BrowserCore .browser-content
└── .browser-webview-frame-anchor
```

The anchor is the source of truth for the desired native child WebView rectangle. `useWebviewLayout` reads `anchor.getBoundingClientRect()`, converts it with `toNativeFrame`, and sends the result to Rust.
The anchor is the source of truth for the desired native child WebView rectangle. `useWebviewLayout` intersects the anchor with the viewport and every overflow-clipping ancestor, converts the resulting visible rectangle with `toNativeFrame`, and sends it to Rust. A fully clipped or invalid rectangle fails closed by staging the native surface offscreen.

## Shared browser owner flow

Expand All @@ -93,6 +93,42 @@ This means layout changes must update both:
1. the visible host rect registry, and
2. the native child WebView position after the shared owner host moves.

`SharedBrowserHostSlot` must publish the clipped rectangle from the original visible panel. Clipping only inside the fixed shared owner is insufficient because the original panel's overflow ancestors no longer exist in that copied DOM path.

## Native surface visibility and overlays

Native child WebViews do not participate in DOM stacking contexts. CSS `z-index`, portal roots, and `overflow: hidden` cannot reliably place React UI above or clip a child WebView.

ORGII therefore separates surface visibility from overlay occlusion:

```text
overlay DOMRect + dimming registry
↓ intersect + native-frame scale
BrowserSession local holes + strongest scrim alpha
↓ latest-wins IPC
macOS CALayer mask + dim layer + native input handoff
```

Important invariants:

- `isActive` controls page lifecycle; `isVisible` controls only the native surface. Opening an overlay must not destroy, reload, or navigate the page.
- On macOS, every opaque overlay surface publishes its real viewport rectangle. Each visible browser session intersects those rectangles with its host and applies only the resulting WebView-local holes. Full-screen modals additionally publish a black scrim alpha; they do not publish the translucent wrapper as an opaque hole.
- Interactive overlays temporarily hand native pointer input back to React while they are open. Passive overlays such as tooltips can leave page input enabled.
- macOS input handoff routes hit testing directly from the covered child WKWebView to the main React WKWebView. Re-running the child container's parent hit test is not sufficient because the two WebViews can live under different native container views and produce a bare `nil`, which lets the click escape to another application. The fallback must fail closed inside the inline WebView, and it must not change an individual WKWebView's runtime class because AppKit may KVO-observe its frame.
- Overlapping rectangles are conservatively coalesced before the even-odd mask is built, and both frontend and Rust cap the path at 64 rectangles.
- Platforms without native region masking currently retain the offscreen compatibility fallback.
- Resize, scroll, scale, and delayed layout callbacks re-check the latest desired visibility before writing a frame. A stale callback cannot move an obscured surface back onscreen.
- Surface commands are serialized per WebView. The last requested visibility wins.
- Restoration uses `reposition_and_show_webview`, which sets position and size before calling `show()` in one native command.
- Frame de-duplication state is committed only after the native command succeeds, so failed IPC remains retryable.

BrowserCore's loading and confirmed error panels also set `isVisible=false` while keeping `isActive=true`. The sensitive-host fallback is only a time-based hint and must not hide a successfully loaded native page. This preserves cookies, login state, history, and in-page memory while real blocking UI is shown.

Overlay coverage follows the interaction contract:

- Local popovers, dropdowns, hover cards, and tooltips register only their visible panel, so the native page remains painted and interactive everywhere else.
- Full-screen modals register the opaque dialog panel as a local hole and publish the matching scrim alpha. Because a DOM scrim cannot alpha-composite above a sibling WKWebView, macOS adds a named black `CALayer` above the live native page. The WKWebView's parent mask also masks that dim layer inside the dialog hole, allowing the React panel to remain fully opaque. Closing the modal removes both the mask and dim layer without navigation, reload, or loss of cookies, history, scroll, or in-page state.

## Layout-change event

Some layout changes move the browser anchor without changing its size. Examples:
Expand Down Expand Up @@ -125,12 +161,20 @@ When inline WebViews are misaligned under UI scale:
6. Confirm Rust receives `a/b` and derives size from corners.
7. If the browser panel moves without resizing, confirm `orgii-webview-layout-changed` reaches `SharedBrowserHostSlot` and `useWebviewLayout`.
8. If using the shared browser owner, confirm `SharedBrowserApp` has moved its fixed host before the final native position update.
9. If a React overlay is covered, confirm its primitive calls `useOverlayLayer(active, elementRef)` and publishes a non-zero rectangle in `overlayOcclusionStateAtom`.
10. If a hidden WebView reappears during resize or scroll, confirm all native position writes pass through `useWebviewLayout`'s serialized visibility gate.
11. If the whole page becomes white when an overlay opens, confirm no caller invokes the removed `browser_webviews_set_layer_for_all` z-order command.

## Files of interest

- `src/app/root/useAppShellEffects.ts` — applies native app zoom and CSS scale variables.
- `src/util/platform/tauri/nativeFrame.ts` — converts DOMRect to `x/y/a/b` native frame payloads.
- `src/hooks/platform/useInlineWebview/useWebviewLayout.ts` — observes and repositions inline WebViews.
- `src/hooks/platform/useInlineWebview/visibleWebviewRect.ts` — intersects anchors with viewport and overflow clipping ancestors.
- `src/store/ui/overlayLayerAtom.ts` — owns the runtime overlay rectangle registry.
- `src/hooks/platform/useInlineWebview/nativeWebviewOcclusion.ts` — intersects/coalesces overlay holes in the WebView-local coordinate system.
- `src/hooks/platform/useInlineWebview/useInlineWebviewOcclusions.ts` — serializes latest-wins native mask projection per browser session.
- `src-tauri/crates/browser/src/occlusion.rs` — applies the macOS CALayer mask and input handoff.
- `src/hooks/platform/useInlineWebview/useWebviewCommands.ts` — creates inline WebViews with native frame payloads.
- `src/hooks/platform/useInlineWebview/webviewLayoutEvents.ts` — shared layout-change event helper.
- `src/engines/BrowserCore/index.tsx` — owns the browser frame anchor.
Expand Down
3 changes: 3 additions & 0 deletions src-tauri/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions src-tauri/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,8 @@ axum = { version = "0.8", features = ["ws"] }
objc2 = "0.6"
objc2-foundation = "0.3"
objc2-app-kit = "0.3"
objc2-core-graphics = { version = "0.3", default-features = false, features = ["std", "CGColor", "CGGeometry", "CGPath"] }
objc2-quartz-core = { version = "0.3", default-features = false, features = ["std", "CALayer", "CAShapeLayer", "CATransaction", "objc2-core-foundation", "objc2-core-graphics"] }
block2 = "0.6"
dispatch2 = "0.3"

Expand Down
2 changes: 2 additions & 0 deletions src-tauri/crates/browser/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,8 @@ window-vibrancy = "0.6"
objc2 = { workspace = true }
objc2-foundation = { workspace = true }
objc2-app-kit = { workspace = true }
objc2-core-graphics = { workspace = true }
objc2-quartz-core = { workspace = true }
block2 = { workspace = true }

# Windows-specific WebView2 bindings for retrieving JavaScript evaluation
Expand Down
2 changes: 2 additions & 0 deletions src-tauri/crates/browser/src/inline.rs
Original file line number Diff line number Diff line change
Expand Up @@ -515,6 +515,7 @@ pub fn close_inline_webview(
}

if let Some(webview) = app.get_webview(&label) {
crate::occlusion::clear_webview_occlusions(&webview);
let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| webview.close()));

clear_generation(&label);
Expand Down Expand Up @@ -618,6 +619,7 @@ pub fn close_all_inline_webviews(app: AppHandle) -> Result<Vec<String>, String>
// Reset lifecycle state so the next create starts fresh.
reset_ref(label);
clear_generation(label);
crate::occlusion::clear_webview_occlusions(webview);

// Clone webview for catch_unwind (needs 'static lifetime)
let webview_clone = webview.clone();
Expand Down
177 changes: 0 additions & 177 deletions src-tauri/crates/browser/src/layering.rs

This file was deleted.

4 changes: 2 additions & 2 deletions src-tauri/crates/browser/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -40,8 +40,8 @@ pub mod dom_editor;
pub mod inline;
pub mod internal_browser_commands;
pub mod internal_browser_state;
pub mod layering;
pub mod logging;
pub mod occlusion;
pub mod screenshot_store;
pub mod scripts;
pub mod types;
Expand All @@ -54,8 +54,8 @@ pub use dom_editor::*;
pub use inline::*;
pub use internal_browser_commands::*;
pub use internal_browser_state::*;
pub use layering::*;
pub use logging::*;
pub use occlusion::*;
pub use screenshot_store::*;
pub use scripts::*;
pub use types::*;
Expand Down
Loading
Loading