diff --git a/examples/08_tauri_paraview_ws/README.md b/examples/08_tauri_paraview_ws/README.md new file mode 100644 index 0000000..7aba445 --- /dev/null +++ b/examples/08_tauri_paraview_ws/README.md @@ -0,0 +1,105 @@ +# Tauri + WebSocket + +This example leverages tauri for just its WebView and lets trame act as the full HTTP server by serving its content over HTTP and WebSocket. Additionally, this example also demonstrates using ParaView for rendering and computation. + +## Tauri project + +Since Tauri is written in Rust, let's get setup with its dev environment. + +```bash +# Install rust +curl --proto '=https' --tlsv1.2 https://sh.rustup.rs -sSf | sh + +# Enable rust within shell +. "$HOME/.cargo/env" + +# Install tauri-cli +cargo install tauri-cli +``` + +Then, the infrastructure (./src-tauri) was generated by running the following commands. + +__Caution__: No need to redo it since we committed the generated structure to the repo. + +```bash +$ cargo tauri init + +✔ What is your app name? · Cone +✔ What should the window title be? · Cone +✔ Where are your web assets (HTML/CSS/JS) located, relative to the "/src-tauri/tauri.conf.json" file that will be created? · ./www +✔ What is the url of your dev server? · ./www +✔ What is your frontend dev command? · +✔ What is your frontend build command? · +``` + +From the default content, we edited the following set of files: + +- `./src-tauri/src/main.rs`: Generic main application designed for trame which can remain the same regardless of your application. +- `./src-tauri/sidecar/*`: OS specific launcher for a trame server. The mac and linux version are just plain bash scripts while the Windows one is a simple compiled C++ application that forward the args to the actual trame python executable generated by PyInstaller. Those can be re-used as-is for any trame app. A departure from the other examples, this app uses `pvpython` to launch the app instead of the packaged binary. +- `./src-tauri/www/*`: Basic content for splashscreen and temporary main window content before we apply a redirect. Those are static and could be adjusted by adding your own image as splashscreen. +- `./src-tauri/icons`: Remove default ones (`rm -rf ./src-tauri/icons`) +- `./src-tauri/Cargo.toml`: Added new dependencies and required feature. +- `./src-tauri/tauri.conf.json`: Edited the following sections + - tauri > allowlist: + shell/sidecar + - tauri > bundle > externalBin : ["sidecar/trame"] + - tauri > bundle > identifier : "trame.cone" + - tauri > bundle > resources : ["server"] # pyinstaller generated app + - tauri > bundle > targets : ["appimage", "nsis", "msi", "app", "dmg"] + - tauri > security > csp : "default-src 'self' 'unsafe-inline' ws: localhost; script-src 'unsafe-eval' 'self';" + - tauri > windows : main-not-visible + splashscreen + +## Copy application to container + +Since the app uses `pvpython` to launch the application, the python executables need to be copied over to the server directory for it to be bundled + +```bash +cp cone.py src-tauri/server +``` + +## Trame example + +We use a simple cone example since it does not have any complex python dependency. Since we're bundling a `ParaView` based application, and `ParaView` not being `pip` installable, this example uses `conda` + + +```bash +conda create --name tauri -c conda-forge python=3.13 +conda activate tauri +pip install trame trame-vtk trame-vuetify pyinstaller +conda install paraview=5.13.3 +``` + +Build bundle for tauri inside `./src-tauri/server/*` while skipping the web content. + +```bash +python -m PyInstaller + --clean --noconfirm \ + --distpath src-tauri \ + --name server --hidden-import pkgutil \ + --collect-all paraview + cone.py +``` + +Generate webcontent for tauri to bundle + +```bash +python -m trame.tools.www --output ./src-tauri/www +``` + +## Tauri bundle + +In order to build and bundle the application, just run + +```bash +# Generate icon for application using ./app-icon.png +cargo tauri icon + +# Generate application +cargo tauri build +``` + +## Running application + +### Linux and macOS +```bash +open ./src-tauri/target/release/bundle/macos/Cone.app +``` diff --git a/examples/08_tauri_paraview_ws/app-icon.png b/examples/08_tauri_paraview_ws/app-icon.png new file mode 100644 index 0000000..27ad0de Binary files /dev/null and b/examples/08_tauri_paraview_ws/app-icon.png differ diff --git a/examples/08_tauri_paraview_ws/cone.py b/examples/08_tauri_paraview_ws/cone.py new file mode 100644 index 0000000..00a7294 --- /dev/null +++ b/examples/08_tauri_paraview_ws/cone.py @@ -0,0 +1,84 @@ +import os +from trame.app import get_server +from trame.ui.vuetify3 import SinglePageLayout +from trame.widgets import vuetify3 as vuetify, paraview +from trame.decorators import TrameApp, change, life_cycle + +from paraview import simple + +# Configure ParaView for headless operation when using standard Python interpreter +# (pvpython sets this automatically, but regular python requires manual configuration) +from paraview.modules import vtkRemotingCore as rc + +rc.vtkProcessModule.GetProcessModule().UpdateProcessType( + rc.vtkProcessModule.PROCESS_BATCH, 0 +) + + +@TrameApp() +class Cone: + """ + This application uses the ParaView simple API to create a cone + and display it in a web application using Tauri. This application + uses remote rendering. + """ + + def __init__(self, server=None): + self.server = get_server(server) + self.state = self.server.state + self.ctrl = self.server.controller + + cone = simple.Cone() + representation = simple.Show(cone) + view = simple.Render() + + self.cone = cone + self.representation = representation + self.view = view + self.resolution = 6 + + self.ui = self._build_ui() + + @change("resolution") + def update_cone(self, resolution, **kwargs): + self.cone.Resolution = resolution + self.ctrl.view_update() + + def _build_ui(self): + with SinglePageLayout(self.server) as layout: + with layout.content: + with vuetify.VContainer(fluid=True, classes="pa-0 fill-height"): + html_view = paraview.VtkRemoteView(self.view, ref="view") + self.ctrl.view_reset_camera = html_view.reset_camera + self.ctrl.view_update = html_view.update + + with layout.toolbar: + vuetify.VSpacer() + vuetify.VSlider( + v_model=("resolution", 6), + min=3, + max=60, + step=1, + hide_details=True, + dense="compact", + style="max-width: 300px;", + ) + with vuetify.VBtn(icon=True, click=self.server.controller.reset_camera): + vuetify.VIcon("mdi-crop-free") + + return layout + + # Use os.write for immediate unbuffered output to ensure Tauri + # receives the port number before Python's stdout buffer flushes + @life_cycle.server_ready + def _tauri_ready(self, **_): + os.write(1, f"tauri-server-port={self.server.port}\n".encode()) + + @life_cycle.client_connected + def _tauri_show(self, **_): + os.write(1, "tauri-client-ready\n".encode()) + + +if __name__ == "__main__": + app = Cone() + app.server.start() diff --git a/examples/08_tauri_paraview_ws/config_app.sh b/examples/08_tauri_paraview_ws/config_app.sh new file mode 100755 index 0000000..5638e6d --- /dev/null +++ b/examples/08_tauri_paraview_ws/config_app.sh @@ -0,0 +1,13 @@ +#!/bin/bash + +set -e +set -x + +python -m PyInstaller \ + --clean --noconfirm \ + --distpath src-tauri \ + --name server --hidden-import pkgutil \ + --collect-all paraview + cone.py + +python -m trame.tools.www --output ./src-tauri/www \ No newline at end of file diff --git a/examples/08_tauri_paraview_ws/src-tauri/.gitignore b/examples/08_tauri_paraview_ws/src-tauri/.gitignore new file mode 100644 index 0000000..fcf481e --- /dev/null +++ b/examples/08_tauri_paraview_ws/src-tauri/.gitignore @@ -0,0 +1,7 @@ +# Generated by Cargo +# will have compiled files and executables +/target/ +icons +server +Cargo.lock +www \ No newline at end of file diff --git a/examples/08_tauri_paraview_ws/src-tauri/Cargo.toml b/examples/08_tauri_paraview_ws/src-tauri/Cargo.toml new file mode 100644 index 0000000..fbf8890 --- /dev/null +++ b/examples/08_tauri_paraview_ws/src-tauri/Cargo.toml @@ -0,0 +1,27 @@ +[package] +name = "app" +version = "0.1.0" +description = "A Tauri App" +authors = ["Kitware"] +license = "" +repository = "" +default-run = "app" +edition = "2021" +rust-version = "1.60" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[build-dependencies] +tauri-build = { version = "1.5.1", features = [] } + +[dependencies] +serde_json = "1.0" +serde = { version = "1.0", features = ["derive"] } +tauri = { version = "1.6.1", features = ["shell-sidecar"] } # Added sidecar +async-std = "1.12.0" # Added for sleep + +[features] +# this feature is used for production builds or when `devPath` points to the filesystem and the built-in dev server is disabled. +# If you use cargo directly instead of tauri's cli you can use this feature flag to switch between tauri's `dev` and `build` modes. +# DO NOT REMOVE!! +custom-protocol = [ "tauri/custom-protocol" ] diff --git a/examples/08_tauri_paraview_ws/src-tauri/build.rs b/examples/08_tauri_paraview_ws/src-tauri/build.rs new file mode 100644 index 0000000..795b9b7 --- /dev/null +++ b/examples/08_tauri_paraview_ws/src-tauri/build.rs @@ -0,0 +1,3 @@ +fn main() { + tauri_build::build() +} diff --git a/examples/08_tauri_paraview_ws/src-tauri/sidecar/trame-aarch64-apple-darwin b/examples/08_tauri_paraview_ws/src-tauri/sidecar/trame-aarch64-apple-darwin new file mode 100755 index 0000000..5c6aa66 --- /dev/null +++ b/examples/08_tauri_paraview_ws/src-tauri/sidecar/trame-aarch64-apple-darwin @@ -0,0 +1,5 @@ +#!/bin/bash +rootdir=`dirname $0` +rootdir=`cd $rootdir && cd .. && pwd` + +$rootdir/Resources/server/server $@ diff --git a/examples/08_tauri_paraview_ws/src-tauri/sidecar/trame-x86_64-apple-darwin b/examples/08_tauri_paraview_ws/src-tauri/sidecar/trame-x86_64-apple-darwin new file mode 100755 index 0000000..5c6aa66 --- /dev/null +++ b/examples/08_tauri_paraview_ws/src-tauri/sidecar/trame-x86_64-apple-darwin @@ -0,0 +1,5 @@ +#!/bin/bash +rootdir=`dirname $0` +rootdir=`cd $rootdir && cd .. && pwd` + +$rootdir/Resources/server/server $@ diff --git a/examples/08_tauri_paraview_ws/src-tauri/sidecar/trame-x86_64-unknown-linux-gnu b/examples/08_tauri_paraview_ws/src-tauri/sidecar/trame-x86_64-unknown-linux-gnu new file mode 100755 index 0000000..b529061 --- /dev/null +++ b/examples/08_tauri_paraview_ws/src-tauri/sidecar/trame-x86_64-unknown-linux-gnu @@ -0,0 +1,5 @@ +#!/bin/bash +rootdir=`dirname $0` +rootdir=`cd $rootdir && cd ../lib/cone && pwd` + +$rootdir/server/server $@ diff --git a/examples/08_tauri_paraview_ws/src-tauri/src/main.rs b/examples/08_tauri_paraview_ws/src-tauri/src/main.rs new file mode 100644 index 0000000..e983da6 --- /dev/null +++ b/examples/08_tauri_paraview_ws/src-tauri/src/main.rs @@ -0,0 +1,45 @@ +#![cfg_attr( + all(not(debug_assertions), target_os = "windows"), + windows_subsystem = "windows" +)] + +use tauri::api::process::{Command, CommandEvent}; +use tauri::Manager; +use std::time::Duration; +use async_std::task; + +fn main() { + tauri::Builder::default() + .setup(|app| { + let splashscreen_window = app.get_window("splashscreen").unwrap(); + let main_window = app.get_window("main").unwrap(); + + let (mut rx, _) = Command::new_sidecar("trame") + .expect("failed to create sidecar") + .args(["--server", "--port", "0", "--timeout", "1"]) + .spawn() + .expect("Failed to spawn server"); + + tauri::async_runtime::spawn(async move { + while let Some(event) = rx.recv().await { + if let CommandEvent::Stdout(line) = event { + if line.contains("tauri-server-port=") { + let tokens: Vec<&str> = line.split("=").collect(); + let port_token = tokens[1].to_string(); + let port = port_token.trim(); + // println!("window.location.replace('http://localhost:{}/')", port); + let _ = main_window.eval(&format!("window.location.replace('http://localhost:{}/')", port)); + } + if line.contains("tauri-client-ready") { + task::sleep(Duration::from_secs(2)).await; + splashscreen_window.close().unwrap(); + main_window.show().unwrap(); + } + } + } + }); + Ok(()) + }) + .run(tauri::generate_context!()) + .expect("error while running application"); +} diff --git a/examples/08_tauri_paraview_ws/src-tauri/tauri.conf.json b/examples/08_tauri_paraview_ws/src-tauri/tauri.conf.json new file mode 100644 index 0000000..f2a4a4d --- /dev/null +++ b/examples/08_tauri_paraview_ws/src-tauri/tauri.conf.json @@ -0,0 +1,86 @@ +{ + "build": { + "beforeBuildCommand": "", + "beforeDevCommand": "", + "devPath": "./www", + "distDir": "./www" + }, + "package": { + "productName": "Cone", + "version": "0.1.0" + }, + "tauri": { + "allowlist": { + "all": false, + "shell": { + "sidecar": true, + "scope": [ + { + "name": "sidecar/trame", + "sidecar": true + } + ] + } + }, + "bundle": { + "active": true, + "category": "DeveloperTool", + "copyright": "", + "deb": { + "depends": [] + }, + "externalBin": ["sidecar/trame"], + "icon": [ + "icons/32x32.png", + "icons/128x128.png", + "icons/128x128@2x.png", + "icons/icon.icns", + "icons/icon.ico" + ], + "identifier": "trame.cone", + "longDescription": "", + "macOS": { + "entitlements": null, + "exceptionDomain": "", + "frameworks": [], + "providerShortName": null, + "signingIdentity": null + }, + "resources": ["server"], + "shortDescription": "", + "targets": ["appimage", "nsis", "msi", "app", "dmg"], + "windows": { + "certificateThumbprint": null, + "digestAlgorithm": "sha256", + "timestampUrl": "" + } + }, + "security": { + "csp": "default-src 'self' 'unsafe-inline' ws: localhost; script-src 'unsafe-eval' 'self';" + }, + "updater": { + "active": false + }, + "windows": [ + { + "fullscreen": false, + "height": 600, + "resizable": true, + "title": "Cone", + "width": 800, + "visible": false + }, + { + "label": "splashscreen", + "width": 400, + "height": 200, + "center": true, + "decorations": false, + "resizable": false, + "visible": true, + "alwaysOnTop": true, + "url": "splashscreen.html" + } + ] + } +}