diff --git a/src-tauri/src/commands/mod.rs b/src-tauri/src/commands/mod.rs index 31f3cc7..636cdbf 100644 --- a/src-tauri/src/commands/mod.rs +++ b/src-tauri/src/commands/mod.rs @@ -14,3 +14,4 @@ pub mod updater; pub mod tools_commands_r2; pub mod workspace_commands; pub mod docker_commands; +pub mod window_commands; diff --git a/src-tauri/src/commands/window_commands.rs b/src-tauri/src/commands/window_commands.rs new file mode 100644 index 0000000..8273528 --- /dev/null +++ b/src-tauri/src/commands/window_commands.rs @@ -0,0 +1,24 @@ +use tauri::AppHandle; +use tauri::WebviewWindowBuilder; + +/// Open a child window from the Rust side using WebviewWindowBuilder. +/// This is more reliable than JS-side WebviewWindow on macOS WKWebView. +#[tauri::command] +pub async fn open_child_window( + app_handle: AppHandle, + label: String, + title: String, + url: String, + width: f64, + height: f64, +) -> Result<(), String> { + let webview_url = tauri::WebviewUrl::App(url.into()); + WebviewWindowBuilder::new(&app_handle, &label, webview_url) + .title(&title) + .inner_size(width, height) + .resizable(true) + .center() + .build() + .map_err(|e| format!("Failed to create window '{}': {}", label, e))?; + Ok(()) +} diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index de2f234..4ffef4e 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -234,6 +234,7 @@ pub fn run() { commands::updater::check_for_updates, commands::workspace_commands::save_workspace, commands::workspace_commands::load_workspace, commands::docker_commands::docker_list_containers, commands::docker_commands::docker_list_images, commands::docker_commands::docker_list_volumes, commands::docker_commands::docker_action, + commands::window_commands::open_child_window, ]) .run(tauri::generate_context!()) .expect("error while running tauri application"); diff --git a/src/components/rdp/RdpView.vue b/src/components/rdp/RdpView.vue index 89b684d..7c7b3fc 100644 --- a/src/components/rdp/RdpView.vue +++ b/src/components/rdp/RdpView.vue @@ -204,44 +204,45 @@ onBeforeUnmount(() => { if (resizeTimeout) { clearTimeout(resizeTimeout); resizeTimeout = null; } }); -// Focus canvas, re-check dimensions, and force full frame on tab switch +// Focus canvas, re-check dimensions, and force full frame on tab switch. +// Uses 300ms delay to let the flex layout fully settle (copilot panel toggle, etc.) watch( () => props.isActive, (active) => { if (!active || !canvasRef.value) return; - // Wait for layout to settle after tab becomes visible - requestAnimationFrame(() => { - requestAnimationFrame(() => { - const wrapper = canvasWrapper.value; - const canvas = canvasRef.value; - if (!wrapper || !canvas) return; + // Immediate focus so keyboard works right away + if (keyboardGrabbed.value) canvasRef.value.focus(); - const { width: cw, height: ch } = wrapper.getBoundingClientRect(); - const newW = Math.round(cw) & ~1; - const newH = Math.round(ch); + // Immediate force refresh to show SOMETHING while we check dimensions + invoke("rdp_force_refresh", { sessionId: props.sessionId }).catch(() => {}); - // If container size differs from canvas resolution, resize the RDP session - if (newW >= 200 && newH >= 200 && (newW !== canvas.width || newH !== canvas.height)) { - invoke("rdp_resize", { - sessionId: props.sessionId, - width: newW, - height: newH, - }).then(() => { + // Delayed dimension check — layout needs time to settle + setTimeout(() => { + const wrapper = canvasWrapper.value; + const canvas = canvasRef.value; + if (!wrapper || !canvas) return; + + const { width: cw, height: ch } = wrapper.getBoundingClientRect(); + const newW = Math.round(cw) & ~1; + const newH = Math.round(ch); + + if (newW >= 200 && newH >= 200 && (newW !== canvas.width || newH !== canvas.height)) { + invoke("rdp_resize", { + sessionId: props.sessionId, + width: newW, + height: newH, + }).then(() => { + if (canvas) { canvas.width = newW; canvas.height = newH; - setTimeout(() => { - invoke("rdp_force_refresh", { sessionId: props.sessionId }).catch(() => {}); - }, 200); - }).catch(() => {}); - } else { - // Same size — just refresh the frame - invoke("rdp_force_refresh", { sessionId: props.sessionId }).catch(() => {}); - } - - if (keyboardGrabbed.value) canvas.focus(); - }); - }); + } + setTimeout(() => { + invoke("rdp_force_refresh", { sessionId: props.sessionId }).catch(() => {}); + }, 500); + }).catch(() => {}); + } + }, 300); }, ); diff --git a/src/components/session/TabBar.vue b/src/components/session/TabBar.vue index 0339312..93b852a 100644 --- a/src/components/session/TabBar.vue +++ b/src/components/session/TabBar.vue @@ -133,20 +133,14 @@ async function detachTab(): Promise { session.active = false; // Open a new Tauri window for this session - const { WebviewWindow } = await import("@tauri-apps/api/webviewWindow"); - const label = `detached-${session.id.substring(0, 8)}-${Date.now()}`; - const wv = new WebviewWindow(label, { - title: `${session.name} — Wraith`, - width: 900, - height: 600, - resizable: true, - center: true, - visible: false, - focus: true, - url: `index.html#/detached-session?sessionId=${session.id}&name=${encodeURIComponent(session.name)}&protocol=${session.protocol}`, - }); - wv.once("tauri://created", () => { wv.show(); }); - wv.once("tauri://error", (e) => { console.error("Detach window error:", e); }); + try { + await invoke("open_child_window", { + label: `detached-${session.id.substring(0, 8)}-${Date.now()}`, + title: `${session.name} — Wraith`, + url: `index.html#/detached-session?sessionId=${session.id}&name=${encodeURIComponent(session.name)}&protocol=${session.protocol}`, + width: 900, height: 600, + }); + } catch (err) { console.error("Detach window error:", err); } } function closeMenuTab(): void { diff --git a/src/layouts/MainLayout.vue b/src/layouts/MainLayout.vue index 7b6441c..d4171bd 100644 --- a/src/layouts/MainLayout.vue +++ b/src/layouts/MainLayout.vue @@ -367,20 +367,14 @@ function closeHelpMenuDeferred(): void { async function handleHelpAction(page: string): Promise { showHelpMenu.value = false; - const { WebviewWindow } = await import("@tauri-apps/api/webviewWindow"); - const label = `help-${page}-${Date.now()}`; - const wv = new WebviewWindow(label, { - title: `Wraith — Help`, - width: 750, - height: 600, - resizable: true, - center: true, - visible: false, - focus: true, - url: `index.html#/tool/help?page=${page}`, - }); - wv.once("tauri://created", () => { wv.show(); }); - wv.once("tauri://error", (e) => { console.error("Help window error:", e); alert("Window error: " + JSON.stringify(e.payload)); }); + try { + await invoke("open_child_window", { + label: `help-${page}-${Date.now()}`, + title: "Wraith — Help", + url: `index.html#/tool/help?page=${page}`, + width: 750, height: 600, + }); + } catch (err) { console.error("Help window error:", err); alert("Window error: " + String(err)); } } async function handleToolAction(tool: string): Promise { @@ -394,8 +388,6 @@ async function handleToolAction(tool: string): Promise { return; } - const { WebviewWindow } = await import("@tauri-apps/api/webviewWindow"); - const toolConfig: Record = { "network-scanner": { title: "Network Scanner", width: 800, height: 600 }, "port-scanner": { title: "Port Scanner", width: 700, height: 500 }, @@ -416,20 +408,14 @@ async function handleToolAction(tool: string): Promise { const sessionId = activeSessionId.value || ""; - // Open tool in a new Tauri window — create hidden, show after webview confirms ready - const label = `tool-${tool}-${Date.now()}`; - const wv = new WebviewWindow(label, { - title: `Wraith — ${config.title}`, - width: config.width, - height: config.height, - resizable: true, - center: true, - visible: false, - focus: true, - url: `index.html#/tool/${tool}?sessionId=${sessionId}`, - }); - wv.once("tauri://created", () => { wv.show(); }); - wv.once("tauri://error", (e) => { console.error("Tool window error:", e); alert("Window error: " + JSON.stringify(e.payload)); }); + try { + await invoke("open_child_window", { + label: `tool-${tool}-${Date.now()}`, + title: `Wraith — ${config.title}`, + url: `index.html#/tool/${tool}?sessionId=${sessionId}`, + width: config.width, height: config.height, + }); + } catch (err) { console.error("Tool window error:", err); alert("Tool window error: " + String(err)); } } async function handleFileMenuAction(action: string): Promise { @@ -449,23 +435,14 @@ function handleThemeSelect(theme: ThemeDefinition): void { async function handleOpenFile(entry: FileEntry): Promise { if (!activeSessionId.value) return; try { - const { WebviewWindow } = await import("@tauri-apps/api/webviewWindow"); const fileName = entry.path.split("/").pop() || entry.path; - const label = `editor-${Date.now()}`; const sessionId = activeSessionId.value; - - const wv = new WebviewWindow(label, { + await invoke("open_child_window", { + label: `editor-${Date.now()}`, title: `${fileName} — Wraith Editor`, - width: 800, - height: 600, - resizable: true, - center: true, - visible: false, - focus: true, url: `index.html#/tool/editor?sessionId=${sessionId}&path=${encodeURIComponent(entry.path)}`, + width: 800, height: 600, }); - wv.once("tauri://created", () => { wv.show(); }); - wv.once("tauri://error", (e) => { console.error("Editor window error:", e); alert("Window error: " + JSON.stringify(e.payload)); }); } catch (err) { console.error("Failed to open editor:", err); } }