tauri icon indicating copy to clipboard operation
tauri copied to clipboard

[bug] Webview Do Not Work Properly

Open KeepNoob opened this issue 4 months ago • 0 comments

Describe the bug

I am trying to implement a multi tabs layout like VS Code. And I try to use webview to show external url i.e. Tauri, Rust official page for testing. It compiles successfully but it cannot show any content from the webpage.

Image

Reproduction

npm create tauri-app@latest
TypeScript 
npm
React

In src-tauri/capabilities/default.json

{
  "permissions": [
    "core:default",
    "opener:default",
    "core:webview:allow-create-webview",
    "core:webview:allow-webview-close",
    "core:webview:allow-set-webview-position",
    "core:webview:allow-set-webview-size",
    "core:webview:allow-webview-show",
    "core:webview:allow-webview-hide",
    "core:webview:allow-reparent"
  ]
}

Create file src/components/NativeTab.tsx

import { useEffect, useRef, useState } from 'react';
import { Webview } from '@tauri-apps/api/webview';
import { getCurrentWindow } from '@tauri-apps/api/window';

interface NativeTabProps {
  id: string;
  url: string;
  isActive: boolean;
}

export const NativeTab = ({ id, url, isActive }: NativeTabProps) => {
  const containerRef = useRef<HTMLDivElement>(null);
  // We store the webview instance in a ref so we don't lose it on re-renders
  const webviewRef = useRef<Webview | null>(null);
  const [isReady, setIsReady] = useState(false);

  useEffect(() => {
    let unmounted = false;

    const setupWebview = async () => {
      // 1. Get the placeholder's exact position on screen
      if (!containerRef.current) return;
      const rect = containerRef.current.getBoundingClientRect();
      const appWindow = getCurrentWindow();

      // 2. Create the native webview (Host is the current window)
      // Note: Labels must be unique and alphanumeric
      const wv = new Webview(appWindow, `tab-${id}`, {
        url: url,
        x: Math.round(rect.x),
        y: Math.round(rect.y),
        width: Math.round(rect.width),
        height: Math.round(rect.height),
      });

      webviewRef.current = wv;

      // 3. Wait for creation to finish
      wv.once('tauri://created', () => {
        if (!unmounted) setIsReady(true);
      });

      wv.once('tauri://error', (e) => {
        console.error('Failed to create webview', e);
      });
    };

    setupWebview();

    // Cleanup: Close the native webview when this React component is removed
    return () => {
      unmounted = true;
      if (webviewRef.current) {
        webviewRef.current.close();
      }
    };
  }, [id, url]);

  // Sync Layout: Resize native webview when the HTML container resizes
  useEffect(() => {
    if (!containerRef.current || !webviewRef.current || !isReady) return;

    const observer = new ResizeObserver((entries) => {
      for (const entry of entries) {
        const { x, y, width, height } = entry.target.getBoundingClientRect();
        
        // Native methods to move the "Floating" window
        webviewRef.current?.setPosition({ x: Math.round(x), y: Math.round(y) });
        webviewRef.current?.setSize({ width: Math.round(width), height: Math.round(height) });
      }
    });

    observer.observe(containerRef.current);

    return () => observer.disconnect();
  }, [isReady]);

  // Visibility Logic: Hide the native window if the tab is not active
  useEffect(() => {
    if (!webviewRef.current || !isReady) return;

    if (isActive) {
      webviewRef.current.show();
      // Force a position update in case layout changed while hidden
      if (containerRef.current) {
         const { x, y } = containerRef.current.getBoundingClientRect();
         webviewRef.current.setPosition({ x: Math.round(x), y: Math.round(y) });
      }
    } else {
      webviewRef.current.hide();
    }
  }, [isActive, isReady]);

  // The React render is just an empty placeholder
  return <div ref={containerRef} style={{ width: '100%', height: '100%' }} />;
};

In file src/App.tsx

import { useState } from "react";
import { NativeTab } from "./components/NativeTab";
import "./App.css";

function App() {
  const [tabs, setTabs] = useState([
    { id: "1", title: "Tauri", url: "https://tauri.app" },
    { id: "2", title: "Rust", url: "https://www.rust-lang.org" },
  ]);
  const [activeTabId, setActiveTabId] = useState("1");

  const closeTab = (id: string) => {
    setTabs(tabs.filter((t) => t.id !== id));
    if (activeTabId === id && tabs.length > 1) {
      setActiveTabId(tabs[0].id); // Simple fallback logic
    }
  };

  return (
    <div style={{ display: "flex", flexDirection: "column", height: "100vh" }}>
      {/* 1. Top Bar (HTML UI) */}
      <div className="tab-bar" style={{ 
        display: "flex", 
        height: "40px", 
        background: "#252526", 
        alignItems: "center",
        paddingLeft: "10px"
      }}>
        {tabs.map((tab) => (
          <div
            key={tab.id}
            onClick={() => setActiveTabId(tab.id)}
            style={{
              padding: "8px 15px",
              background: tab.id === activeTabId ? "#1e1e1e" : "#2d2d2d",
              color: tab.id === activeTabId ? "white" : "#999",
              cursor: "pointer",
              marginRight: "2px",
              borderRadius: "5px 5px 0 0",
              userSelect: "none"
            }}
          >
            {tab.title} 
            <span 
              onClick={(e) => { e.stopPropagation(); closeTab(tab.id); }}
              style={{ marginLeft: "10px", fontWeight: "bold" }}
            >
              ×
            </span>
          </div>
        ))}
        <button onClick={() => {
          const newId = Date.now().toString();
          setTabs([...tabs, { id: newId, title: "Google", url: "https://google.com" }]);
          setActiveTabId(newId);
        }} style={{ marginLeft: "10px" }}>
          +
        </button>
      </div>

      {/* 2. Content Area (Native Webviews Overlay Here) */}
      <div className="content-area" style={{ flex: 1, position: "relative", background: "#1e1e1e" }}>
        {tabs.map((tab) => (
          <div 
            key={tab.id} 
            style={{ 
              display: activeTabId === tab.id ? "block" : "none", 
              width: "100%", 
              height: "100%" 
            }}
          >
            <NativeTab 
              id={tab.id} 
              url={tab.url} 
              isActive={activeTabId === tab.id} 
            />
          </div>
        ))}
      </div>
    </div>
  );
}

export default App;

Run

npm run tauri dev

Expected behavior

It should show the webpage content

Full tauri info output

> [email protected] tauri
> tauri info


[✔] Environment
    - OS: Windows 10.0.26100 x86_64 (X64)
    ✔ WebView2: 142.0.3595.94
    ✔ MSVC: Visual Studio Build Tools 2022
    ✔ rustc: 1.91.1 (ed61e7d7e 2025-11-07)
    ✔ cargo: 1.91.1 (ea2d97820 2025-10-10)
    ✔ rustup: 1.28.2 (e4f3ad6f8 2025-04-28)
    ✔ Rust toolchain: stable-x86_64-pc-windows-msvc (default)
    - node: 25.2.1
    - npm: 11.6.2

[-] Packages
    - tauri 🦀: 2.9.3, (outdated, latest: 2.9.4)
    - tauri-build 🦀: 2.5.2, (outdated, latest: 2.5.3)
    - wry 🦀: 0.53.5
    - tao 🦀: 0.34.5
    - @tauri-apps/api  ⱼₛ: 2.9.1
    - @tauri-apps/cli  ⱼₛ: 2.9.4 (outdated, latest: 2.9.5)

[-] Plugins
    - tauri-plugin-opener 🦀: 2.5.2
    - @tauri-apps/plugin-opener  ⱼₛ: 2.5.2

[-] App
    - build-type: bundle
    - CSP: unset
    - frontendDist: ../dist
    - devUrl: http://localhost:1420/
    - framework: React
    - bundler: Vite

Stack trace


Additional context

No response

KeepNoob avatar Nov 30 '25 16:11 KeepNoob