From 1a03c223168f33a451bff6174ecf99e9e97e7d31 Mon Sep 17 00:00:00 2001 From: gsvprharsha Date: Mon, 22 Jun 2026 16:58:06 +0530 Subject: [PATCH 1/2] feat: replace iframe with native Tauri embedded webview --- src-tauri/Cargo.lock | 2 + src-tauri/Cargo.toml | 4 +- src-tauri/capabilities/default.json | 6 +- src-tauri/src/lib.rs | 76 ++++++++++++++ src/components/editor/WebPreviewPane.tsx | 124 +++++++++++++++++++---- 5 files changed, 190 insertions(+), 22 deletions(-) diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index c89055d4..90e557ba 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -2972,6 +2972,7 @@ dependencies = [ name = "origin" version = "0.1.4" dependencies = [ + "dpi", "keyring", "portable-pty", "reqwest 0.12.28", @@ -2987,6 +2988,7 @@ dependencies = [ "tauri-plugin-store", "tauri-plugin-updater", "tokio", + "url", ] [[package]] diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 5091ebf1..48e36598 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -18,7 +18,9 @@ crate-type = ["staticlib", "cdylib", "rlib"] tauri-build = { version = "2", features = [] } [dependencies] -tauri = { version = "2", features = [] } +tauri = { version = "2", features = ["unstable"] } +url = "2" +dpi = "0.1" tauri-plugin-opener = "2" tauri-plugin-store = "2" tauri-plugin-dialog = "2" diff --git a/src-tauri/capabilities/default.json b/src-tauri/capabilities/default.json index 1d1e14dc..34a0ebf0 100644 --- a/src-tauri/capabilities/default.json +++ b/src-tauri/capabilities/default.json @@ -20,6 +20,10 @@ "sql:default", "sql:allow-execute", "updater:default", - "process:allow-restart" + "process:allow-restart", + "core:webview:allow-create-webview", + "core:webview:allow-set-webview-position", + "core:webview:allow-set-webview-size", + "core:webview:allow-webview-close" ] } diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index d7867986..0e2a2956 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -9,6 +9,79 @@ mod system; mod terminal; mod tree; +use tauri::{AppHandle, Manager, WebviewBuilder, WebviewUrl}; +use dpi::{LogicalPosition, LogicalSize}; + +// IMPORTANT: these commands MUST be `async fn`. +// +// `Window::add_child` (Tauri 2.11.2) dispatches the webview build onto the main +// thread via `run_on_main_thread` and then *blocks* on `rx.recv()` waiting for +// the result. A synchronous `#[tauri::command]` runs on the main thread itself, +// so the command would dispatch work to the main thread and then block that same +// thread waiting for it — a self-deadlock. The spinner would spin forever. +// +// Declaring the command `async` makes Tauri run it on its async runtime thread +// pool instead of the main thread, so the dispatch-and-wait completes normally. +#[tauri::command] +async fn embed_ide_panel( + app: AppHandle, + panel_id: String, + url: String, + x: f64, + y: f64, + width: f64, + height: f64, +) -> Result<(), String> { + // Destroy any existing embedded webview with this label first + if let Some(existing) = app.get_webview(&panel_id) { + let _ = existing.close(); + } + let host = app + .get_webview_window("main") + .ok_or_else(|| "Host window not found".to_string())?; + let window = host.as_ref().window(); + let parsed_url = url::Url::parse(&url).map_err(|e| e.to_string())?; + window + .add_child( + WebviewBuilder::new(&panel_id, WebviewUrl::External(parsed_url)), + LogicalPosition::new(x, y), + LogicalSize::new(width, height), + ) + .map_err(|e| e.to_string())?; + Ok(()) +} + +#[tauri::command] +async fn resize_ide_panel( + app: AppHandle, + panel_id: String, + x: f64, + y: f64, + width: f64, + height: f64, +) -> Result<(), String> { + if let Some(webview) = app.get_webview(&panel_id) { + webview + .set_position(LogicalPosition::new(x, y)) + .map_err(|e| e.to_string())?; + webview + .set_size(LogicalSize::new(width, height)) + .map_err(|e| e.to_string())?; + } + Ok(()) +} + +#[tauri::command] +async fn destroy_ide_panel( + app: AppHandle, + panel_id: String, +) -> Result<(), String> { + if let Some(webview) = app.get_webview(&panel_id) { + webview.close().map_err(|e| e.to_string())?; + } + Ok(()) +} + #[cfg_attr(mobile, tauri::mobile_entry_point)] pub fn run() { tauri::Builder::default() @@ -58,6 +131,9 @@ pub fn run() { dap::dap_start, dap::dap_request, dap::dap_stop, + embed_ide_panel, + resize_ide_panel, + destroy_ide_panel, ]) .run(tauri::generate_context!()) .expect("error while running tauri application"); diff --git a/src/components/editor/WebPreviewPane.tsx b/src/components/editor/WebPreviewPane.tsx index ce81377d..a5007e10 100644 --- a/src/components/editor/WebPreviewPane.tsx +++ b/src/components/editor/WebPreviewPane.tsx @@ -1,9 +1,13 @@ -import { useState } from 'react'; -import { RefreshCw, ExternalLink, ArrowLeft, ArrowRight, Globe, PictureInPicture2, X, Smartphone, Tablet, Monitor } from 'lucide-react'; +import { useState, useEffect, useRef, useCallback } from 'react'; +import { RefreshCw, ExternalLink, ArrowLeft, ArrowRight, Globe, PictureInPicture2, X, Smartphone, Tablet, Monitor, Loader2 } from 'lucide-react'; import { openUrl } from '@tauri-apps/plugin-opener'; import { WebviewWindow } from '@tauri-apps/api/webviewWindow'; +import { invoke } from '@tauri-apps/api/core'; import { Tooltip } from '../ui/Tooltip'; +// Stable label for the single embedded preview webview. +const PREVIEW_PANEL_ID = 'origin-web-preview'; + const COMMON_PORTS = [ { port: 5173, label: 'Vite' }, { port: 3000, label: 'Next / CRA' }, @@ -262,6 +266,10 @@ export default function WebPreviewPane() { const [reloadKey, setReloadKey] = useState(0); const [viewW, setViewW] = useState(null); const [viewH, setViewH] = useState(null); + const [iframeLoading, setIframeLoading] = useState(false); + + // Container that reserves layout space for the native embedded webview. + const containerRef = useRef(null); const activePreset = detectPreset(viewW, viewH); @@ -279,6 +287,7 @@ export default function WebPreviewPane() { // Open a fresh URL from the empty-state picker — resets the stack. function navigateFresh(to: string) { + setIframeLoading(true); setHistory([to]); setIndex(0); setInputUrl(to); @@ -286,6 +295,7 @@ export default function WebPreviewPane() { } function moveTo(nextIndex: number) { + setIframeLoading(true); setIndex(nextIndex); const to = history[nextIndex]; setInputUrl(to); @@ -295,8 +305,7 @@ export default function WebPreviewPane() { function handleUrlBarNavigate(raw: string) { const next = normalize(raw); if (!next) return; - // Same URL as current → treat Enter as a reload rather than pushing - // a duplicate history entry. + setIframeLoading(true); if (next === url) { setReloadKey(k => k + 1); setInputUrl(next); @@ -331,6 +340,71 @@ export default function WebPreviewPane() { }); } + // Push the current container geometry to the native webview. + const syncBounds = useCallback(() => { + const el = containerRef.current; + if (!el) return; + const rect = el.getBoundingClientRect(); + void invoke('resize_ide_panel', { + panelId: PREVIEW_PANEL_ID, + x: rect.left, + y: rect.top, + width: rect.width, + height: rect.height, + }).catch(() => {}); + }, []); + + // Embed / re-embed the native webview whenever the URL or reload key changes. + useEffect(() => { + const el = containerRef.current; + if (!url || !el) return; + + let cancelled = false; + setIframeLoading(true); + + const rect = el.getBoundingClientRect(); + void invoke('embed_ide_panel', { + panelId: PREVIEW_PANEL_ID, + url, + x: rect.left, + y: rect.top, + width: rect.width, + height: rect.height, + }) + .catch(() => {}) + .finally(() => { + if (!cancelled) setIframeLoading(false); + }); + + return () => { + cancelled = true; + void invoke('destroy_ide_panel', { panelId: PREVIEW_PANEL_ID }).catch(() => {}); + }; + }, [url, reloadKey]); + + // Keep the native webview aligned with the placeholder as it resizes. + useEffect(() => { + const el = containerRef.current; + if (!url || !el) return; + + const ro = new ResizeObserver(() => syncBounds()); + ro.observe(el); + window.addEventListener('resize', syncBounds); + window.addEventListener('scroll', syncBounds, true); + + return () => { + ro.disconnect(); + window.removeEventListener('resize', syncBounds); + window.removeEventListener('scroll', syncBounds, true); + }; + }, [url, viewW, viewH, syncBounds]); + + // Re-sync after preset/dimension changes so the webview tracks the new box. + useEffect(() => { + if (!url) return; + syncBounds(); + }, [viewW, viewH, url, syncBounds]); + // No URL yet — show picker if (url === null) { return ( @@ -358,7 +432,7 @@ export default function WebPreviewPane() {
- setReloadKey(k => k + 1)} title="Refresh"> + { setIframeLoading(true); setReloadKey(k => k + 1); }} title="Refresh"> @@ -451,7 +525,9 @@ export default function WebPreviewPane() {
- {/* Iframe wrapper */} + {/* Native embedded webview wrapper. The container below is a transparent + placeholder that reserves layout space; the real content is a native + Tauri webview positioned over it at viewport-relative coordinates. */}
-