Tauri ↔ Leptos integration (M-INT.2)
The chunk that joins the Leptos shell to the Tauri webview, then forwards OS-level drag-drop events into a Leptos signal.
Data flow
sequenceDiagram
participant OS
participant Shell as crates/app/src/main.rs<br/>(Tauri shell)
participant Bridge as crates/app-ui/index.html<br/>(JS bridge)
participant App as crates/app-ui/src/app.rs<br/>(Leptos)
participant View as <App> view
OS ->> Shell: drag-drop event
Note over Shell: on_window_event(<br/>WindowEvent::DragDrop)
Shell -->> Bridge: window.emit("file-dropped", path)
Note over Bridge: __TAURI__.event.listen
Bridge -->> App: window.dispatchEvent(<br/>CustomEvent "file-dropped")
Note over App: install_file_drop_listener()<br/>addEventListener
App ->> App: set_loaded.set(Some(path))
App -->> View: swap drop-zone view → player view
Every hop is a one-liner. No tauri-sys crate, no JS-side state — the
bridge is one addEventListener per direction.
Tauri side (crates/app/src/main.rs)
#![allow(unused)] fn main() { .on_window_event(|window, event| { if let WindowEvent::DragDrop(DragDropEvent::Drop { paths, .. }) = event && let Some(path) = paths.first() { let payload = path.to_string_lossy().into_owned(); if let Err(err) = window.emit("file-dropped", payload) { eprintln!("failed to emit file-dropped event: {err}"); } } }) }
Tauri 2's WindowEvent::DragDrop
fires automatically when tauri.conf.json has
"dragDropEnabled": true (the default for our window).
JS bridge (crates/app-ui/index.html)
<script>
window.addEventListener("DOMContentLoaded", () => {
if (window.__TAURI__ && window.__TAURI__.event) {
window.__TAURI__.event.listen("file-dropped", (event) => {
window.dispatchEvent(new CustomEvent("file-dropped", {
detail: event.payload
}));
});
}
});
</script>
Why a CustomEvent instead of calling Leptos directly: it keeps the WASM
bundle dependency-free of Tauri's JS API. Leptos's web-sys listener
works against any browser; the bridge degrades to a no-op when
window.__TAURI__ is absent (e.g. running under trunk serve for
component review).
Leptos side (crates/app-ui/src/app.rs)
#![allow(unused)] fn main() { fn install_file_drop_listener(set_loaded: WriteSignal<Option<String>>) { let Some(window) = web_sys::window() else { return; }; let closure = Closure::wrap(Box::new(move |event: web_sys::Event| { if let Ok(ce) = event.dyn_into::<CustomEvent>() && let Some(path) = ce.detail().as_string() { set_loaded.set(Some(path)); } }) as Box<dyn FnMut(_)>); let _ = window.add_event_listener_with_callback( "file-dropped", closure.as_ref().unchecked_ref(), ); closure.forget(); // app-lifetime listener — never removed } }
Closure::forget is intentional: the listener lives for the whole app
lifetime. Dropping it would silently de-register the handler.
Tauri config (tauri.conf.json)
{
"build": {
"frontendDist": "../app-ui/dist",
"beforeDevCommand": "cd ../app-ui && trunk serve --port 1420",
"beforeBuildCommand": "cd ../app-ui && trunk build --release",
"devUrl": "http://localhost:1420"
}
}
cargo tauri dev automatically runs trunk serve in crates/app-ui/
and waits for it on port 1420 before opening the webview.
cargo tauri build runs trunk build --release first; the resulting
crates/app-ui/dist/ becomes the bundled webview content.
How to run
# Dev — hot-reload Leptos via Trunk + reload the Tauri webview on save.
cd crates/app && cargo tauri dev
# Production — Trunk build + Tauri build, single binary.
cd crates/app && cargo tauri build
Drop any file on the running window; the path appears under "Preview surface · " in the player view.
Drag-over visual feedback (M-POLISH.1)
Tauri 2 fires four DragDropEvent variants — Enter, Over, Drop,
Leave. M-INT.2 only handled Drop. M-POLISH.1 adds Enter and
Leave so the drop-zone shows the user "yes, this drop will land here":
#![allow(unused)] fn main() { // crates/app/src/main.rs match drag { DragDropEvent::Enter { .. } => emit("file-drag-enter"), DragDropEvent::Leave => emit("file-drag-leave"), DragDropEvent::Drop { paths, .. } => { emit("file-dropped", path); emit("file-drag-leave"); // reset visual after drop } _ => {} // Over and any future variants } }
The JS bridge re-emits as browser CustomEvents (parallel to
file-dropped / player-status). Leptos installs an is_dragging
signal that flips on enter/leave; the existing
DropZoneState::Active
variant the storybook already shipped (M-UI.1) provides the visual.
Tier-2 e2e (crates/app-e2e/tests/golden_path.rs) uses
__test_drag_enter and __test_drag_leave debug-only commands —
parallel to __test_drop_file since WebDriver can't synthesize
OS-level drag events.
App-shell visual references
The drop-zone surface (idle) at boot — exactly what the user sees before any file is dropped: