@livx.cc/appwrap 0.46.1 → 0.46.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/runtime/App_Resources/Android/src/main/AndroidManifest.xml +1 -0
- package/runtime-desktop/bridge-shim/shim.ts +12 -197
- package/runtime-desktop/crates/webview-control/Cargo.lock +330 -0
- package/runtime-desktop/crates/webview-control/Cargo.toml +28 -0
- package/runtime-desktop/crates/webview-control/examples/host_harness.rs +245 -0
- package/runtime-desktop/crates/webview-control/src/embedded.rs +100 -0
- package/runtime-desktop/crates/webview-control/src/lib.rs +95 -0
- package/runtime-desktop/crates/webview-control/src/popup.rs +519 -0
- package/runtime-desktop/crates/webview-control/src/profiles.rs +45 -0
- package/runtime-desktop/crates/webview-control/src/wkwebview_backend.rs +497 -0
- package/runtime-desktop/src-tauri/Cargo.lock +13 -0
- package/runtime-desktop/src-tauri/Cargo.toml +3 -0
- package/runtime-desktop/src-tauri/src/bridge_mac.rs +83 -441
- package/runtime-desktop/src-tauri/src/browser_tab.rs +193 -0
- package/runtime-desktop/src-tauri/src/main.rs +8 -1
- package/runtime-desktop/src-tauri/src/popup_mac.rs +5 -519
- package/runtime-desktop/src-tauri/tauri.conf.json +1 -0
- package/src/cli.ts +16 -0
- package/src/config.ts +10 -1
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
// browser_tab — Tauri commands for an EMBEDDED, tear-off/dock-able browser tab (project "browser-tab"
|
|
2
|
+
// Phase 3). Thin front-end-facing wrapper over `webview_control::embedded::Controller`: the same live
|
|
3
|
+
// WKWebView (page + WKWebsiteDataStore session + bridgeShim) is mounted `ChildOf` a host pane NSView
|
|
4
|
+
// and torn off / docked with `set_mount`.
|
|
5
|
+
//
|
|
6
|
+
// R2 (agent-drive, DRY): the tab is created in the crate-level registry keyed by `id` (its label), so
|
|
7
|
+
// the control-channel adapter (`bridge_mac.rs`) — and thus the mcp-desktop-browser MCP — can drive
|
|
8
|
+
// this SAME embedded tab (navigate/eval/screenshot/close) by that id, with NO new TS<->Rust binding.
|
|
9
|
+
//
|
|
10
|
+
// Landmine (a): the pane NSView* is fetched + the webview mounted on the MAIN THREAD, AFTER the pane
|
|
11
|
+
// container has non-zero bounds (we create the container at the caller-supplied paneRect), else blank.
|
|
12
|
+
#[cfg(target_os = "macos")]
|
|
13
|
+
mod imp {
|
|
14
|
+
use std::collections::HashMap;
|
|
15
|
+
use std::ffi::c_void;
|
|
16
|
+
use std::sync::{Mutex, OnceLock};
|
|
17
|
+
|
|
18
|
+
use objc2::rc::Retained;
|
|
19
|
+
use objc2::{MainThreadMarker, MainThreadOnly};
|
|
20
|
+
use objc2_app_kit::{NSView, NSWindow};
|
|
21
|
+
use objc2_foundation::{NSPoint, NSRect, NSSize};
|
|
22
|
+
use objc2_web_kit::WKWebView;
|
|
23
|
+
use serde::Deserialize;
|
|
24
|
+
use tauri::Manager;
|
|
25
|
+
|
|
26
|
+
use webview_control::embedded::Controller;
|
|
27
|
+
use webview_control::{CreateSpec, Mount, ViewHandle};
|
|
28
|
+
|
|
29
|
+
/// Web-DOM pane rect the front-end reserves (top-left origin, CSS px == points here since the
|
|
30
|
+
/// shell runs at the window's backing scale). We flip Y to AppKit's bottom-left origin at mount.
|
|
31
|
+
#[derive(Deserialize, Clone, Copy)]
|
|
32
|
+
pub struct PaneRect {
|
|
33
|
+
pub x: f64,
|
|
34
|
+
pub y: f64,
|
|
35
|
+
pub width: f64,
|
|
36
|
+
pub height: f64,
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// id -> host pane container NSView* (as usize). The container is retained by being a subview of
|
|
40
|
+
// the window contentView; we hold only its pointer to reposition / reparent / remove it. Access
|
|
41
|
+
// is main-thread only (all commands marshal via run_on_main_thread), the Mutex just satisfies the
|
|
42
|
+
// `static` Send/Sync bound for the raw pointer.
|
|
43
|
+
fn containers() -> &'static Mutex<HashMap<String, usize>> {
|
|
44
|
+
static CONTAINERS: OnceLock<Mutex<HashMap<String, usize>>> = OnceLock::new();
|
|
45
|
+
CONTAINERS.get_or_init(|| Mutex::new(HashMap::new()))
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
unsafe fn appkit_rect(content: &NSView, r: PaneRect) -> NSRect {
|
|
49
|
+
let ch = content.bounds().size.height;
|
|
50
|
+
NSRect::new(
|
|
51
|
+
NSPoint::new(r.x, ch - r.y - r.height),
|
|
52
|
+
NSSize::new(r.width, r.height),
|
|
53
|
+
)
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/// The main window's contentView (parent of the shell WKWebView). Main-thread only.
|
|
57
|
+
unsafe fn content_view(win: &NSWindow) -> Retained<NSView> {
|
|
58
|
+
win.contentView().expect("main window has a contentView")
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
pub fn create(
|
|
62
|
+
app: tauri::AppHandle,
|
|
63
|
+
id: String,
|
|
64
|
+
url: String,
|
|
65
|
+
profile_id: String,
|
|
66
|
+
rect: PaneRect,
|
|
67
|
+
) -> Result<(), String> {
|
|
68
|
+
let win = app.get_webview_window("main").ok_or("no main window")?;
|
|
69
|
+
// with_webview runs its closure on the AppKit main thread. Build the pane container at
|
|
70
|
+
// non-zero bounds FIRST (landmine a), then mount the webview ChildOf it — all on main.
|
|
71
|
+
let (tx, rx) = std::sync::mpsc::channel();
|
|
72
|
+
win.with_webview(move |w| {
|
|
73
|
+
let res = (|| -> Result<(), String> {
|
|
74
|
+
let mtm = MainThreadMarker::new().ok_or("not main thread")?;
|
|
75
|
+
unsafe {
|
|
76
|
+
let wk = &*(w.inner() as *mut WKWebView);
|
|
77
|
+
let ns_win = wk.window().ok_or("shell webview has no window")?;
|
|
78
|
+
let content = content_view(&ns_win);
|
|
79
|
+
let frame = appkit_rect(&content, rect);
|
|
80
|
+
let container = NSView::initWithFrame(NSView::alloc(mtm), frame);
|
|
81
|
+
content.addSubview(&container);
|
|
82
|
+
let cptr = Retained::as_ptr(&container) as *mut c_void;
|
|
83
|
+
let handle = ViewHandle::from_ptr(cptr);
|
|
84
|
+
let spec = CreateSpec {
|
|
85
|
+
label: id.clone(),
|
|
86
|
+
url: url.clone(),
|
|
87
|
+
profile_id: profile_id.clone(),
|
|
88
|
+
init_js: String::new(),
|
|
89
|
+
mount: Mount::ChildOf(handle),
|
|
90
|
+
};
|
|
91
|
+
Controller::new().create_webview(mtm, spec, Box::new(|_raw| {}))?;
|
|
92
|
+
// contentView holds a strong ref via addSubview; store only the pointer.
|
|
93
|
+
containers().lock().unwrap().insert(id.clone(), cptr as usize);
|
|
94
|
+
}
|
|
95
|
+
Ok(())
|
|
96
|
+
})();
|
|
97
|
+
let _ = tx.send(res);
|
|
98
|
+
})
|
|
99
|
+
.map_err(|e| e.to_string())?;
|
|
100
|
+
rx.recv().map_err(|e| e.to_string())?
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
pub fn set_mount(app: tauri::AppHandle, id: String, mode: String) -> Result<(), String> {
|
|
104
|
+
let (tx, rx) = std::sync::mpsc::channel();
|
|
105
|
+
let win = app.get_webview_window("main").ok_or("no main window")?;
|
|
106
|
+
win.run_on_main_thread(move || {
|
|
107
|
+
let res = (|| -> Result<(), String> {
|
|
108
|
+
let mtm = MainThreadMarker::new().ok_or("not main thread")?;
|
|
109
|
+
let mount = match mode.as_str() {
|
|
110
|
+
"toplevel" => Mount::TopLevel,
|
|
111
|
+
"child" => {
|
|
112
|
+
let ptr = *containers().lock().unwrap().get(&id).ok_or("no pane for id")?;
|
|
113
|
+
unsafe { Mount::ChildOf(ViewHandle::from_ptr(ptr as *mut c_void)) }
|
|
114
|
+
}
|
|
115
|
+
other => return Err(format!("bad mount mode: {other}")),
|
|
116
|
+
};
|
|
117
|
+
if Controller::new().set_mount(mtm, &id, mount) {
|
|
118
|
+
Ok(())
|
|
119
|
+
} else {
|
|
120
|
+
Err(format!("no webview {id}"))
|
|
121
|
+
}
|
|
122
|
+
})();
|
|
123
|
+
let _ = tx.send(res);
|
|
124
|
+
})
|
|
125
|
+
.map_err(|e| e.to_string())?;
|
|
126
|
+
rx.recv().map_err(|e| e.to_string())?
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
pub fn navigate(app: tauri::AppHandle, id: String, url: String) -> Result<(), String> {
|
|
130
|
+
let (tx, rx) = std::sync::mpsc::channel();
|
|
131
|
+
let win = app.get_webview_window("main").ok_or("no main window")?;
|
|
132
|
+
win.run_on_main_thread(move || {
|
|
133
|
+
let res = (|| -> Result<(), String> {
|
|
134
|
+
let mtm = MainThreadMarker::new().ok_or("not main thread")?;
|
|
135
|
+
Controller::new().navigate(mtm, &id, &url)
|
|
136
|
+
})();
|
|
137
|
+
let _ = tx.send(res);
|
|
138
|
+
})
|
|
139
|
+
.map_err(|e| e.to_string())?;
|
|
140
|
+
rx.recv().map_err(|e| e.to_string())?
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
pub fn close(app: tauri::AppHandle, id: String) -> Result<(), String> {
|
|
144
|
+
let (tx, rx) = std::sync::mpsc::channel();
|
|
145
|
+
let win = app.get_webview_window("main").ok_or("no main window")?;
|
|
146
|
+
win.run_on_main_thread(move || {
|
|
147
|
+
let res = (|| -> Result<(), String> {
|
|
148
|
+
let mtm = MainThreadMarker::new().ok_or("not main thread")?;
|
|
149
|
+
Controller::new().close(mtm, &id);
|
|
150
|
+
if let Some(ptr) = containers().lock().unwrap().remove(&id) {
|
|
151
|
+
unsafe {
|
|
152
|
+
let container = &*(ptr as *mut NSView);
|
|
153
|
+
container.removeFromSuperview();
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
Ok(())
|
|
157
|
+
})();
|
|
158
|
+
let _ = tx.send(res);
|
|
159
|
+
})
|
|
160
|
+
.map_err(|e| e.to_string())?;
|
|
161
|
+
rx.recv().map_err(|e| e.to_string())?
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
#[cfg(target_os = "macos")]
|
|
166
|
+
#[tauri::command]
|
|
167
|
+
pub fn browser_tab_create(
|
|
168
|
+
app: tauri::AppHandle,
|
|
169
|
+
id: String,
|
|
170
|
+
url: String,
|
|
171
|
+
profile_id: String,
|
|
172
|
+
rect: imp::PaneRect,
|
|
173
|
+
) -> Result<String, String> {
|
|
174
|
+
imp::create(app, id.clone(), url, profile_id, rect).map(|_| id)
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
#[cfg(target_os = "macos")]
|
|
178
|
+
#[tauri::command]
|
|
179
|
+
pub fn browser_tab_set_mount(app: tauri::AppHandle, id: String, mode: String) -> Result<(), String> {
|
|
180
|
+
imp::set_mount(app, id, mode)
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
#[cfg(target_os = "macos")]
|
|
184
|
+
#[tauri::command]
|
|
185
|
+
pub fn browser_tab_navigate(app: tauri::AppHandle, id: String, url: String) -> Result<(), String> {
|
|
186
|
+
imp::navigate(app, id, url)
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
#[cfg(target_os = "macos")]
|
|
190
|
+
#[tauri::command]
|
|
191
|
+
pub fn browser_tab_close(app: tauri::AppHandle, id: String) -> Result<(), String> {
|
|
192
|
+
imp::close(app, id)
|
|
193
|
+
}
|
|
@@ -17,6 +17,7 @@ use tauri::Manager;
|
|
|
17
17
|
mod biometrics_mac;
|
|
18
18
|
#[cfg(target_os = "macos")]
|
|
19
19
|
mod bridge_mac;
|
|
20
|
+
mod browser_tab;
|
|
20
21
|
#[cfg(target_os = "macos")]
|
|
21
22
|
mod notifications_mac;
|
|
22
23
|
#[cfg(target_os = "macos")]
|
|
@@ -688,7 +689,13 @@ fn main() {
|
|
|
688
689
|
builder = builder.plugin(tauri_plugin_window_state::Builder::default().build());
|
|
689
690
|
}
|
|
690
691
|
builder
|
|
691
|
-
.invoke_handler(tauri::generate_handler![
|
|
692
|
+
.invoke_handler(tauri::generate_handler![
|
|
693
|
+
appwrap_invoke,
|
|
694
|
+
browser_tab::browser_tab_create,
|
|
695
|
+
browser_tab::browser_tab_set_mount,
|
|
696
|
+
browser_tab::browser_tab_navigate,
|
|
697
|
+
browser_tab::browser_tab_close,
|
|
698
|
+
])
|
|
692
699
|
// Popups are plain NSWindows tauri doesn't know about; with the main (only tauri) window
|
|
693
700
|
// closed, tauri exits the app — but tear popups down explicitly so no dangling windows or
|
|
694
701
|
// live child webviews outlive the shell during teardown (or if the exit is ever prevented).
|