Cat100
Cat100

Reputation: 61

How to create Mulitwindows in tauri (rust + react + typescript + html + css)?

I am quite new to tauri and its backend, rust. I started developing in visual studio and the development thus far has gone smoothly, until I needed to created physical windows when the user needs to be directed to other sections of the application such as the settings or accessing an AI model from the frontend of the application.

From my understanding in this setup of tauri, Frontend is being handled by Typescript, CSS, HTMl, and React. Backend is being handled by rust.

According to Tauri's official documentation, a window can be opened through three approaches (refer to the Tauri documentation https://tauri.app/v1/guides/features/multiwindow#accessing-a-window-at-runtime) :

1. Statically

The window can be statically added by means of adding an entry in the "windows:" section or block of the tauri.conf.json file provided by the create-tauri-app command when initially creating the tauri project.

tauri.conf.json:

{
  "tauri": {
    "windows": [
      {
        "label": "external",
        "title": "Tauri Docs",
        "url": "https://tauri.app"
      },
      {
        "label": "local",
        "title": "Tauri",
        "url": "index.html"
      }
    ]
  }
}

Runtime approaches

2. Frontend: Javascript/Typescript

A window can be opened by creating a webview instance in the frontend.

Javascript:

import { WebviewWindow } from '@tauri-apps/api/window'
const webview = new WebviewWindow('theUniqueLabel', {
  url: 'path/to/page.html',
})
// since the webview window is created asynchronously,
// Tauri emits the `tauri://created` and `tauri://error` to notify you of the creation response
webview.once('tauri://created', function () {
  // webview window successfully created
})
webview.once('tauri://error', function (e) {
  // an error occurred during webview window creation
})

Typescript:

import { WebviewWindow } from '@tauri-apps/api/window';

const webview = new WebviewWindow('theUniqueLabel', {
  url: 'path/to/page.html',
});

// since the webview window is created asynchronously,
// Tauri emits the `tauri://created` and `tauri://error` to notify you of the creation response
webview.once('tauri://created', () => {
  // webview window successfully created
  console.log('Webview window successfully created');
});

webview.once('tauri://error', (e) => {
  // an error occurred during webview window creation
  console.error('Error creating webview window:', e);
});

3. Backend: Rust

Creating a window using the AppBuilder:: struct either through an App instance, App handle, or tauri command:

App instance:

tauri::Builder::default()
  .setup(|app| {
    let docs_window = tauri::WindowBuilder::new(
      app,
      "external", /* the unique window label */
      tauri::WindowUrl::External("https://tauri.app/".parse().unwrap())
    ).build()?;
    let local_window = tauri::WindowBuilder::new(
      app,
      "local",
      tauri::WindowUrl::App("index.html".into())
    ).build()?;
    Ok(())
  })
  .run(tauri::generate_context!())
  .expect("error while running app");

App handle:

tauri::Builder::default()
  .setup(|app| {
    let handle = app.handle();
    std::thread::spawn(move || {
      let local_window = tauri::WindowBuilder::new(
        &handle,
        "local",
        tauri::WindowUrl::App("index.html".into())
      ).build()?;
    });
    Ok(())
  })

tauri commands:

\#\[tauri::command\]
async fn open_specific_window(handle: tauri::AppHandle) {
let specific_window = tauri::WindowBuilder::new(
&handle,
"external", /\* the unique window label \*/
tauri::WindowUrl::External("https://tauri.app/".parse().unwrap())
).build().unwrap();
}

Note: The command function needs to be an async function to avoid a deadlock on windows. (Refer to this stack to see how this gent solved it: (I am trying to create a new window using Tauri 1.2, Rust, React, and Typescript. I am facing some issues)

My approach

I used the third approach as, intuitively, it looked like the easiest approach to me and it optimize performance in a straight-forward manner. The other two methods worked excellently but the third required extra steps which I will highlight as an answer to this stack. Again, I wanted to use the third approach as I believed that it should have optimized performance and easy implementation. Anyone have a solution other than the one that I will post to this stack?

Also, this is my first stack on stack overflow. I wanted to open this question to aid other in not wasting their time like I did. Any suggestions are welcomed.

I tried the third approach as stipulated in my details of the problem.

Rust command

I created the rust command that creates the window:

#[tauri::command]
async fn open_settings_window(app: tauri::AppHandle) {
    
    let file_path = "src-tauri/src/Views/settings.html";
    
    let _settings_window = tauri::WindowBuilder::new(
        &app,
        "Settings", /* the unique window label */
        tauri::WindowUrl::App(file_path.into()),
    )
    .title("Settings")
    .build()
    .unwrap();

}

Note: It took me ages to figure out that the tauri::WindowUrl::App() only loads html. Please use html files for this.

Calling the rust command from frontend

I called the rust command when the user click on the button (id = settings-button):

Here is the App.tsx file:

import { useState } from "react";
import reactLogo from "./assets/react.svg";
import { invoke } from "@tauri-apps/api/tauri";
import "./App.css";

function App() {
  const [greetMsg, setGreetMsg] = useState("");
  const [name, setName] = useState("");

  async function open_settings() {
    await invoke("open_settings_window") ; 
    console.log("Window was opened") ; 
  }
  async function greet() {
    // Learn more about Tauri commands at https://tauri.app/v1/guides/features/command
    setGreetMsg(await invoke("greet", { name }));
  }

  async function TerminateApplication() {
    await invoke("terminate_application") ; 
  }

  return (
    <div className="container">
      <h1>Welcome to SSLT!</h1>

      <div className="eye-container align-justify-center">
       <div className="eye align-justify-center">
        <div className="iris align-justify-center">
         <div className="pupil align-justify-center"></div>
        </div>
       </div>
      </div>

      <form
        id="form"
        className="row"
        onSubmit={(e) => {
          e.preventDefault();
          greet();
        }}
      >
        <input
          id="greet-input"
          onChange={(e) => setName(e.currentTarget.value)}
          placeholder="Enter a name..."
        />
        <button type="submit">Greet</button>
      </form>

      <p>{greetMsg}</p>

      <div className="options-container align-justify-center">
        <button type="submit" className="option">AI model</button>
        <button className="option" onClick={open_settings} id="settings-button" >
        Settings
        </button>
        <button className="option" onClick={TerminateApplication}>Exit</button>
      </div>
    </div>
  );
}

export default App;

Which correctly invoked the rust command and created a file as programmed in the rust command. The only problem is that it does not actually display the contents of the connected typescript in the html file specified.

Contents of the html file:

<!doctype html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <link rel="icon" type="image/svg+xml" href="/vite.svg" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Tauri + React + TS</title>
  </head>

  <body>
    <div>Hello</div>
    <!-- Include the compiled TypeScript file -->
    <script type="module" src="settings-main.tsx"></script>
    
  </body>
</html>

If anyone has this problem, please aid. I eventually got the solution...

Upvotes: 4

Views: 2555

Answers (1)

Cat100
Cat100

Reputation: 61

I eventually found the solution...

The third approach worked with the tauri command in rust, which created the window correctly. It did render the html content but not the typescript content connect to the html script.

Welp, I found a solution. The typescript should have been rendered using a div accompanied by a React re-render.

Note: This could act as a template for the third approach, if you plan on using it:

1. Create an additional file, {specific window}-main.tsx


    import React from "react";
    import ReactDOM from "react-dom/client";
    import Settings_View from "./settings";
    
    ReactDOM.createRoot(document.getElementById("settings-root") as HTMLElement).render(
        <React.StrictMode>
          <Settings_View/>
        </React.StrictMode>,
      );

Note: Please make sure that your {specified} view or screen, in this case the Setting_View, matches your {specified}.tsx file's exported component and that the component is imported correctly. The document.getElementById("settings-root") is getting the container div by its id, and accordingly renders the aforementioned component into that div.

2. Connect the new script in the html file (The file being called in the tauri window creation command).


    <!doctype html>
    <html lang="en">
      <head>
        <meta charset="UTF-8" />
        <link rel="icon" type="image/svg+xml" href="/vite.svg" />
        <meta name="viewport" content="width=device-width, initial-scale=1.0" />
        <title>Tauri + React + TS</title>
      </head>
    
      <body>
        <div id="settings-root">Hello</div>
        <!-- Include the compiled TypeScript file -->
        <script type="module" src="settings-main.tsx"></script>
                                  <!--Here ^ -->>
      </body>
    </html>

Note: Ensure that the div has an id that you can associated with in the React part, refer to step 1's note.

Conclusion

That fixed my issue of creating the window using a rust command, finally loading the actual typescript into the container and displaying it's contents.

Here is an example of both of the windows open:

Link to the result of the solution

Short guide using approach 3

1. Create tauri command for a specific window


    #[tauri::command]
    async fn open_a_window(app: tauri::AppHandle) {
        
        let file_path = "path/window.html";
        
        let _settings_window = tauri::WindowBuilder::new(
            &app,
            "Window", /* the unique window label */
            tauri::WindowUrl::App(file_path.into()),
        )
        .title("Window")
        .build()
        .unwrap();
    
    }

Note: Make sure that the function is an async to avoid a deadlock on windows.

2. Create the window.tsx file containing the window content


    import { useState } from "react";
    import { invoke } from "@tauri-apps/api/tauri";
    import React from "react";
    import "./window.css";
    
    function Window_View() {
     
      return (
        <div className="container">
          <h1>Window</h1>
    
          <button>Hello</button>
        </div>
      );
    }
    
    export default Window_View;

3. Create the html file


    <!doctype html>
    <html lang="en">
      <head>
        <meta charset="UTF-8" />
        <link rel="icon" type="image/svg+xml" href="/vite.svg" />
        <meta name="viewport" content="width=device-width, initial-scale=1.0" />
        <title>Tauri + React + TS</title>
      </head>
    
      <body>
        <div id="window-root"></div>
        <!-- Include the compiled TypeScript file that handles the React render -->
        <script type="module" src="window-main.tsx"></script>
        
      </body>
    </html>

Note: Ensure that the div has an id so that you can reference it in the window-main.tsx file that you will create in the next step. Also, ensure that the script is of a module type. Please...

4. Create the window-main.tsx file


    import React from "react";
    import ReactDOM from "react-dom/client";
    import Window_View from "./window";
    
    ReactDOM.createRoot(document.getElementById("window-root") as HTMLElement).render(
        <React.StrictMode>
          <Window_View/>
        </React.StrictMode>,
      );

Note: Please check that the component, in this case Window_View(), is imported correctly and that the component is being rendered in the procedure, ReactDOM.createRoot(document.getElementById("window-root") as HTMLElement).render();. Also, just check that the procedure is getting the correct div by its id, it needs to be the div declared in the html file we create.

Building the App

It just occurred to me that I need to test the build. It did not work. I bundled, installed, and ran the application. It ran fine but when I clicked on a button, it did create the window/view but did not physically render the content relative to the {window}.tsx file contents. I did get the solution to this though.

Note: If you have been following the guide to link the windows and render them, this should solve the build problem issue of the related {window/view}.tsx content not displaying.

Tauri documentation on MultiWindows

According to the tauri documentation surrounding the Mutliwindows, it stipulates this:

Creating additional HTML pages

If you want to create additional pages beyond index.html, you will need to make sure they are present in the dist directory at build time. How you do this depends on your frontend setup. If you use Vite, create an additional input for the second HTML page in vite.config.js.

This mean that you need to specific the relative {window}.html in the vite.config.js file before the content can be rendered.

From testing, I also found out that Tauri wants to seperate its web assets from its src-tauri folder due to the src-tauri folder being included in the distDir during the build process. Meaning, you'll need to create an additional folder that has all the {window}.html files in it.

Resolving the build issue

1. Create a new folder that holds all the .html files

web folder

  • {window}.html

2. Change the module paths in the html files to fit the {window}-main.tsx file

   <!doctype html>
    <html lang="en">
      <head>
        <meta charset="UTF-8" />
        <link rel="icon" type="image/svg+xml" href="/vite.svg" />
        <meta name="viewport" content="width=device-width, initial-scale=1.0" />
        <title>Tauri + React + TS</title>
      </head>

      <body>
        <div id="settings-root">Hello</div>
        <!-- Include the compiled TypeScript file -->
        <script type="module" src="path/window-main.tsx"></script>
                                  <!--Here ^ -->>
      </body>
    </html>

3. Change the file path in the tauri commands to the new paths


    #[tauri::command]
        async fn open_a_window(app: tauri::AppHandle) {
    
            let file_path = "web folder path/window.html"; //Change this line to fit the new path where the html file is.
    
            let _settings_window = tauri::WindowBuilder::new(
                &app,
                "Window", /* the unique window label */
                tauri::WindowUrl::App(file_path.into()),
            )
            .title("Window")
            .build()
            .unwrap();
    
        }

Vite additions

You will need to add the import { resolve } from 'path'; to the vite.config.js file later but you can't do that until you install the node types and add it to the ts.config.js.

Install the node types from a command line

npm install --save-dev @types/node

Update the ts.config.js file


    {
      "compilerOptions": {
        // other options...
        "types": ["node"] // add the type here
      },
      // other configurations...
    }

Now you can continue with the guided steps.

4. Add the html files in the vite.config.js

import { resolve } from 'path'; //Add this import to ensure access to the __dirname and resolve() procedure
import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";
// https://vitejs.dev/config/
export default defineConfig(async () => ({
  plugins: [react()],

  // Vite options tailored for Tauri development and only applied in `tauri dev` or `tauri build`
  //
  // 1. prevent vite from obscuring rust errors
  clearScreen: false,
  // 2. tauri expects a fixed port, fail if that port is not available
  server: {
    port: 1420,
    strictPort: true,
    watch: {
      // 3. tell vite to ignore watching `src-tauri`
      ignored: ["**/src-tauri/**"],
    },
  },
  //Add this build with the rollupOptions and input to reference the html files 
  build: {
    rollupOptions: {
      input: {
        main: resolve( __dirname, 'index.html'),
        windows: resolve( __dirname , "path/window.html"),
      }
    }
  }
}));

5. Build the tauri application

To build the tauri application, you need to run this command:

npm run tauri build

That should create the build and you'll be able to download the relative OS application installer or run the .exe directly from the the src-tauri/target/release folder after the building is complete.

In general

Just check if all imports, scripts, and anything with a path is correct. Please, you don't want to waste your time...

Thank you

Thank you for your time and I truly hope this assist anyone having the same problem. This was difficult for me to even get any video tutorials or official documentation to assist in this issue. I hope this helps :)

Upvotes: 0

Related Questions