🧸

Tauri2.0デスクトップアプリの基本カスタム実装メモ

2024/10/16に公開


Tauri2.0でデスクトップアプリを作成する前に、よく使うであろう基本的なカスタマイズをどのように実装すればよいか下調べした結果メモです。最初はTauri独自の書き方に慣れませんでしたが、慣れてくれば特に問題なく使用できそうでした。ただ、まだ2.0がリリースされて日が浅いからか英語でも情報が断片的にしか見つからなかったりで、結局doc.rsやGitHubのソースを確認する場面がそこそこありなかなか苦労しました。フロントエンドはSvelte5を使用しています。下調べの内容は色々実装した後に追記するかもしれません。

前提

環境

$ cargo tauri info

[] Environment
    - OS: Debian 12.0.0 x86_64 (X64)
    ✔ webkit2gtk-4.1: 2.44.3
    ✔ rsvg2: 2.54.7
    ✔ rustc: 1.81.0 (eeb90cda1 2024-09-04)
    ✔ cargo: 1.81.0 (2dbb1af80 2024-08-20)
    ✔ rustup: 1.27.1 (54dd3d00f 2024-04-24)
    ✔ Rust toolchain: stable-x86_64-unknown-linux-gnu (environment override by RUSTUP_TOOLCHAIN)
    - node: 20.18.0
    - pnpm: 9.12.0
    - yarn: 1.22.22
    - npm: 10.8.2

[-] Packages
    - tauri 🦀: 2.0.2
    - tauri-build 🦀: 2.0.1
    - wry 🦀: 0.44.1
    - tao 🦀: 0.30.3
    - tauri-cli 🦀: 2.0.2
    - @tauri-apps/api : 2.0.2
    - @tauri-apps/cli : 2.0.2

[-] Plugins
    - tauri-plugin-window-state 🦀: 2.0.1
    - @tauri-apps/plugin-window-state : 2.0.0
    - tauri-plugin-fs 🦀: 2.0.1
    - @tauri-apps/plugin-fs : 2.0.0
    - tauri-plugin-dialog 🦀: 2.0.1
    - @tauri-apps/plugin-dialog : 2.0.0
    - tauri-plugin-log 🦀: 2.0.1
    - @tauri-apps/plugin-log : not installed!
    - tauri-plugin-global-shortcut 🦀: 2.0.1
    - @tauri-apps/plugin-global-shortcut : 2.0.0
...

表記

  • フロントエンドとバックエンド
    • "View" : フロントエンドのJavaScript側
    • "Core" : バックエンドのRust側
  • カレントディレクトリはプロジェクトディレクトリ

各種基本カスタムの実装

ViewからCore機能呼出

実装機能

img_view_to_core

実装コード

./src/routes/+page.svelte
<script lang="ts">
  import { invoke } from "@tauri-apps/api/core";
  let coreMsg = $state("");

  function invokeGreeting() {
    invoke<string>("greeting", { argName: "Svelte" })
      .then(v => coreMsg = v)
      .catch(e => coreMsg = e);
    }
</script>

<h3>server message</h3>
<input type="text" bind:value={coreMsg} />

<h3>invoke core function</h3>
<button type="button" onclick={invokeGreeting}>greeting</button>
./src-tauri/src/lib.rs
#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
    tauri::Builder::default()
        .invoke_handler(tauri::generate_handler![greeting])
        .run(tauri::generate_context!())
        .expect("error while running tauri application");
}
#[tauri::command]
async fn greeting(arg_name: String) -> String {
    format!("Hello, {}", arg_name)
}

メモ

  • 色々なパターンを盛り込んだ例
  • 上記のInvokeを用いる方法はCommandと呼ばれている
  • ViewからCoreへの通信は他にemitTo等を用いる方法もある

CoreからViewに情報送出

実装機能

img_core_to_view

実装コード

./src/routes/+page.svelte
<script lang="ts">
  import { onDestroy } from "svelte";
  import { invoke } from "@tauri-apps/api/core";
  import { getCurrentWebviewWindow } from "@tauri-apps/api/webviewWindow";

  let coreMsg = $state("");

  function invokeGreeting() {
    invoke<string>("greeting").then(null);
  }

  const appWebview = getCurrentWebviewWindow();
  const unlisten = appWebview.listen<string>("core-msg", (ev: { payload: string; }) => {
    coreMsg = ev.payload;
  });

  onDestroy(() => { unlisten.then(null); });
</script>

<h3>server message</h3>
<input type="text" bind:value={coreMsg} />

<h3>invoke core function</h3>
<button type="button" onclick={invokeGreeting}>greeting</button>
./src-tauri/src/lib.rs
use tauri::AppHandle;
#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
    tauri::Builder::default()
        .invoke_handler(tauri::generate_handler![greeting])
        .run(tauri::generate_context!())
        .expect("error while running tauri application");
}
#[tauri::command]
async fn greeting(app_handle: AppHandle) {
    use tauri::Emitter;
    let _ = app_handle.emit_to("main", "core-msg", "Hello!!");
}

メモ

  • 上記のEmitter::emit_toを用いる方法はEventと呼ばれている
    • EventはViewからCoreへの伝送も可能
  • CoreからViewへの通信は他にChannel::sendを用いる方法もある
    • 順序データを高速伝送可能らしい
    • Channels
  • unmount時にunlistenするコードを書く必要がある
    • 書かないと適切に後処理が実施されない
  • emit_to"main"は以下ファイルの値(のはず)
    ./src-tauri/capabilities/default.json
    {
      "windows": [
        "main"
      ],
    }
    

メニューバー

実装機能

img_window_menu

実装コード

./src/routes/+page.svelte
<script lang="ts">
  import { onDestroy } from "svelte";
  import { getCurrentWebviewWindow } from "@tauri-apps/api/webviewWindow";

  let coreMsg = $state("");

  const appWebview = getCurrentWebviewWindow();
  const showMenuMsg = (ev: { payload: string; }) => coreMsg = ev.payload;
  const evNew = appWebview.listen<string>("ev-new", showMenuMsg);
  const evOpen = appWebview.listen<string>("ev-open", showMenuMsg);
  const evSp = appWebview.listen<string>("ev-sp", showMenuMsg);

  onDestroy(() => {
    evNew.then(null);
    evOpen.then(null);
    evSp.then(null);
  });
</script>

<p>core message: <input type="text" bind:value={coreMsg} /></p>
./src-tauri/src/lib.rs
use tauri::{menu::*, AppHandle, Wry, Error};
#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
    tauri::Builder::default()
        .setup(|app| {
            let _ = app.set_menu(get_menu(app.handle())?);
            app.on_menu_event(|handle, ev| { run_menu_process(handle, ev.id().as_ref()) });
            Ok(())
        })
        .run(tauri::generate_context!())
        .expect("error while running tauri application");
}

fn get_menu(handle: &AppHandle) -> Result<Menu<Wry>, Error> {
    let menu = Menu::new(handle)?;
    let _ = menu.append(&get_menu_file(handle)?)?;
    let _ = menu.append(&get_menu_edit(handle)?)?;
    Ok(menu)
}
fn get_menu_file(handle: &AppHandle) -> Result<Submenu<Wry>, Error> {
    SubmenuBuilder::new(handle, "File")
        .items(&[
            &MenuItem::with_id(handle, MenuId::new("id_new"), "New", true, Some("Ctrl+N"))?,
            &MenuItem::with_id(handle, MenuId::new("id_open"), "Open", true, Some("Ctrl+O"))?,
        ])
        .separator()
        .text(MenuId::new("id_quit"), "Quit")
        .build()
}
fn get_menu_edit(handle: &AppHandle) -> Result<Submenu<Wry>, Error> {
    SubmenuBuilder::new(handle, "Edit")
        .cut()
        .copy()
        .paste()
        .separator()
        .item(&MenuItem::with_id(handle,MenuId::new("id_sp"), "Special Edit", true, None::<&str>)?)
        .build()
}
fn run_menu_process(handle: &AppHandle, id: &str) {
    use tauri::Emitter;
    match id {
        "id_new" => { let _ = handle.emit_to("main", "ev-new", "trigger new menu process"); },
        "id_open" => { let _ = handle.emit_to("main", "ev-open", "trigger open menu process"); },
        "id_sp" => { let _ = handle.emit_to("main", "ev-sp", "trigger sp menu process"); },
        "id_quit" => {
            handle.cleanup_before_exit();
            std::process::exit(0);
        },
        _ => {},
    }
}

メモ

  • 各メニュー内容をサブメニューとして構成してから表示メニューに登録するイメージ
    • メニュー構成時のSome("Ctrl+N")等は単なる表記指定のみ
      • 機能させるには別途ショートカットキーとして実装する必要がある
  • 基本的なメニューはcut()等の指定だけで追加可能
    • これらはそれだけで機能し、かつショートカットキーも登録不要
    • ただしOSによりサポート状況が異なる
    • サポート状況はdoc.rs参照
  • 自前実装の中でプロセス終了させる場合はcleanup_before_exit()を実行する

右クリックメニュー

実装機能

img_context_menu

実装コード

./src/routes/+page.svelte
<script lang="ts">
  import { emitTo } from '@tauri-apps/api/event';

  let coreMsg = $state("");

  function oncontextmenu(ev: Event) {
    ev.preventDefault();
    emitTo("main", "right-click");
  }
</script>

<svelte:document {oncontextmenu} />
<p>core message: <input type="text" bind:value={coreMsg} /></p>
./src-tauri/src/lib.rs
use tauri::{menu::*, AppHandle, Manager, Listener,Wry, Error};
#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
    tauri::Builder::default()
        .setup(|app| {
            use std::sync::Arc;
            let context_menu = get_menu_context(app.handle())?;
            let main_view = Arc::new(app.get_webview_window("main").ok_or("could not find main window")?);
            let mv_for_listen = main_view.clone();
            main_view.listen("right-click", move |_ev| {
                let _ = mv_for_listen.popup_menu(&context_menu);
            });
            Ok(())
        })
        .run(tauri::generate_context!())
        .expect("error while running tauri application");
}
fn get_menu_context(handle: &AppHandle) -> Result<Menu<Wry>, Error> {
    MenuBuilder::new(handle).cut().copy().paste().build()
}

メモ

  • 右クリックメニュー(コンテキストメニュー)も基本はメニューと同じ
    • app.set_menuのような右クリックメニュー専用登録メソッドは無い
    • ViewからのEventを受けてpopup_menuメソッドで表示させる

ウィンドウ状態保存

実装機能

img_window_state

実装コード

./src-tauri/capabilities/default.json
{
  "permissions": [
    "window-state:default"
  ]
}
./src-tauri/tauri.conf.json
{
  "app": {
    "windows": [
      {
        "visible": false
      }
    ]
  }
}
./src/routes/+page.svelte
<script lang="ts">
  let coreMsg = $state("");
</script>

<p>core message: <input type="text" bind:value={coreMsg} /></p>
./src-tauri/src/lib.rs
use tauri::Manager;
#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
    tauri::Builder::default()
        .plugin(tauri_plugin_window_state::Builder::new().build())
        .setup(|app| {
            let main_view = app.get_webview_window("main")
                .ok_or("could not find main window")?;
            let _ = main_view.show();
            Ok(())
        })
        .run(tauri::generate_context!())
        .expect("error while running tauri application");
}

メモ

  • Window Stateプラグインを用いる
  • Automaticでインストールするとpermissionsdesktop.jsonに追記される
    • 概ねうまくいくがdefault.jsonにも追記した方が無難
      • プラグインによってはなぜかdesktop.jsonの記載が効かないものがあった
      • プラグインのplatform対応状況によって自動で切り替わっているように思える
      • default.jsonに追記することで自身の責任において確実に制御可能になる
  • プラグインを使用しただけだと微妙にうまくいかない
    • 不定位置に描画されてから前回の状態に復元される
    • tauri.conf.jsonで標準非表示にして描画タイミングを制御して回避する
  • ウィンドウ状態の情報は以下に保存される
    • $env:AppData\[identifier of tauri.conf.json]
    • 例: C:\Users\scirexs\AppData\Roaming\dev.scirexs.sandbox

ショートカットキー

実装機能

img_shortcut_key

実装コード

./src-tauri/capabilities/default.json
{
  "permissions": [
    "global-shortcut:default"
  ]
}
./src/routes/+page.svelte
<script lang="ts">
  import { onDestroy } from "svelte";
  import { getCurrentWebviewWindow } from "@tauri-apps/api/webviewWindow";

  let coreMsg = $state("");

  const appWebview = getCurrentWebviewWindow();
  const showMsg = (ev: { payload: string; }) => coreMsg = ev.payload;
  const evCN = appWebview.listen<string>("ev-cn", showMsg);
  const evAB = appWebview.listen<string>("ev-ab", showMsg);

  onDestroy(() => {
    evCN.then(null);
    evAB.then(null);
  });
</script>

<p>core message: <input type="text" bind:value={coreMsg} /></p>
./src-tauri/src/lib.rs
use tauri::AppHandle;
#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
    tauri::Builder::default()
        .setup(|app| {
            let handle = app.handle();
            use tauri_plugin_global_shortcut::{Shortcut, ShortcutState};

            // let ctrl_n = Shortcut::new(Some(Modifiers::CONTROL), Code::KeyN);  // strict define
            let ctrl_n = Shortcut::try_from("ctrl+n")?;
            let alt_b = Shortcut::try_from("alt+b")?;
            handle.plugin(
                tauri_plugin_global_shortcut::Builder::new()
                    // .with_shortcuts(["ctrl+n", "alt+b"])?  // possible to pass str directly
                    .with_shortcuts([ctrl_n, alt_b])?
                    .with_handler(move |handle, shortcut, event| {
                        if event.state() == ShortcutState::Released { return; }  // on keyup
                        // if ShortcutState::Pressed (on keydown), run process
                        run_shortcut_process(handle, shortcut.into_string().as_str());
                    })
                    .build(),
            )?;
            // app.global_shortcut().register(ctrl_n)?;  // if need to add key later
            Ok(())
        })
        .run(tauri::generate_context!())
        .expect("error while running tauri application");
}
fn run_shortcut_process(handle: &AppHandle, id: &str) {
    use tauri::Emitter;
    match id {
        // Shortcut::new(Some(Modifiers::CONTROL), Code::KeyN).into_string().as_str() => {},
        "control+KeyN" => { let _ = handle.emit_to("main", "ev-cn", "trigger ctrl+n"); },
        "alt+KeyB" => { let _ = handle.emit_to("main", "ev-ab", "trigger alt+b"); },
        _ => {},
    }
}

メモ

  • Global Shortcutプラグインを用いる
  • Automaticでインストールするとlib.rsに以下が自動で追記されるので削除する
    • .plugin(tauri_plugin_global_shortcut::Builder::new().build())
    • .plugin()内で処理を記述することも可能だが?が使用できないため不便
  • 構造体ShortcutHotKeyは同じなのか不明
    • doc.rsでは"Struct Shortcut"と表示されている
    • しかし同ページのsourceリンクで辿るとpub struct HotKeyがハイライトされる
    • rust-analyzerではShortcut::new()HotKeyで型表示される
    • useuse tauri_plugin_global_shortcut::Shortcutになる
    • aliasでもないようだ?
    • 簡単に使えないプラグインは意味がないので深く考えないことにした
  • 文字列による有効なキー表現
  • 文字列パース処理
    • 文字列で指定する場合、各キーのデリミタは+
    • doc.rs source

ダイアログ表示

実装機能

img_open_dialog

実装コード

./src-tauri/capabilities/default.json
{
  "permissions": [
    "dialog:default"
  ]
}
./src/routes/+page.svelte
<script lang="ts">
  import { ask, open, save, type ConfirmDialogOptions, type DialogFilter, type OpenDialogOptions, type SaveDialogOptions } from "@tauri-apps/plugin-dialog";
  // confirm => Ok/Cancel dialog
  // message => only Ok dialog

  let coreMsg = $state("");
  const filters: DialogFilter[] = [
    { name: "my image filter png", extensions: ["png"] },
    { name: "my image filter jpg", extensions: ["jpg", "jpeg"] },
  ];

  function openAskDialog() {
    const optionDialog: ConfirmDialogOptions = {
      title: "ask dialog",
      kind: "info",
    }
    ask("Appear Yes/No dialog as shown", optionDialog)
      .then(v => coreMsg = v.toString())
      .catch(_ => coreMsg = "error")
  }
  function openFileDialog() {
    const optionDialog: OpenDialogOptions = {
      title: "file dialog",
      filters,
      multiple: false,
      directory: false,
      defaultPath: coreMsg,
    };
    open(optionDialog)
      .then(v => coreMsg = v === null ? "null" : v)
      .catch(_ => coreMsg = "error");
  }
  function openSaveDialog() {
    const optionDialog: SaveDialogOptions = {
      title: "",
      filters,
      defaultPath: coreMsg,
    }
    save(optionDialog)
      .then(v => coreMsg = v === null ? "null": v)
      .catch(_ => coreMsg = "error");
  }
</script>

<p>core message: <input type="text" bind:value={coreMsg} /></p>
<ul>
  <li><button type="button" onclick={openAskDialog}>yesno dialog</button></li>
  <li><button type="button" onclick={openFileDialog}>file dialog</button></li>
  <li><button type="button" onclick={openSaveDialog}>save dialog</button></li>
</ul>
./src-tauri/src/lib.rs
#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
    tauri::Builder::default()
        .plugin(tauri_plugin_dialog::init())
        .run(tauri::generate_context!())
        .expect("error while running tauri application");
}

メモ

  • Dialogプラグインを用いる
  • 右上の閉じるボタンを押すとnullが返る
    • askの場合はfalseが返る
  • saveで既存のファイルパスを選択すると自動で上書き確認が出る
    • これだけでは実際の上書きはされない
    • 実際の保存処理の中で適切に処理する必要がある

画像読み込み

実装機能

img_read_image

実装コード

./src-tauri/Cargo.toml
[dependencies]
tauri = { version = "2.0.2", features = ["protocol-asset"]}
./src-tauri/tauri.conf.json
{
  "app": {
    "security": {
      "assetProtocol": {
        "enable": true,
        "scope": ["**"]
      },
      "csp": "default-src 'self'; img-src 'self' asset: http://asset.localhost"
    }
  },
}
./src/routes/+page.svelte
<script lang="ts">
  import { convertFileSrc } from "@tauri-apps/api/core";

  let coreMsg = $state("");
  const imgPath = convertFileSrc("C:/sandbox/img_png.png".replaceAll("/","\\"));

  coreMsg = imgPath;
</script>

<p>core message: <input type="text" bind:value={coreMsg} size="50" /></p>
<img src={imgPath} alt="sample" />
./src-tauri/src/lib.rs
#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
    tauri::Builder::default()
        .run(tauri::generate_context!())
        .expect("error while running tauri application");
}

メモ

  • Coreの処理を通さず直接ファイルを読み込む
    • 画像や動画等のコンテンツはこの手法で読み込むのが最速(のはず)

テキスト読み込み

実装機能

img_read_text

実装コード

./src/routes/+page.svelte
<script lang="ts">
  import { invoke } from "@tauri-apps/api/core";

  let coreMsg = $state("");
  const txtPath = "C:/sandbox/sample.txt".replaceAll("/","\\");

  invoke<string>("read_text_file", { path: txtPath })
    .then(v => coreMsg = v)
    .catch(e => coreMsg = e);
</script>

<p>core message: <input type="text" bind:value={coreMsg} size="50" /></p>
./src-tauri/src/lib.rs
use std::path::PathBuf;
#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
    tauri::Builder::default()
        .invoke_handler(tauri::generate_handler![read_text_file])
        .run(tauri::generate_context!())
        .expect("error while running tauri application");
}
#[tauri::command]
fn read_text_file(path: String) -> Result<String, String> {
    use std::fs::File;
    use std::io::Read;
    let file_path = get_valid_file_path(&path)?;

    let mut file = File::open(file_path).map_err(|e| format!("unable to open file as readonly: {e}"))?;
    let mut whole_string = String::new();
    file.read_to_string(&mut whole_string).map_err(|e| format!("unable to read file as text: {e}"))?;
    Ok(whole_string)
}
fn get_valid_file_path(path: &str) -> Result<PathBuf, String> {
    let file_path = PathBuf::from(path);
    let _ = file_path.try_exists().map_err(|e| format!("file not exists: {e}"))?;
    if !file_path.is_file() { return Err("the path is not a file".to_string()); }
    Ok(file_path)
}
plugin version
./src-tauri/capabilities/default.json
{
  "permissions": [
    "fs:allow-appconfig-read"
  ]
}
./src/routes/+page.svelte
<script lang="ts">
  import { readTextFile, BaseDirectory, type ReadFileOptions } from "@tauri-apps/plugin-fs"

  let coreMsg = $state("");

  const options: ReadFileOptions = {
    baseDir: BaseDirectory.AppConfig  // $env:AppData\[identifier of tauri.conf.json]
  }
  readTextFile("sample.txt", options)
    .then(v => coreMsg = v)
    .catch(e => coreMsg = e)
</script>

<p>core message: <input type="text" bind:value={coreMsg} size="50" /></p>
./src-tauri/src/lib.rs
#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
    tauri::Builder::default()
        .plugin(tauri_plugin_fs::init())
        .run(tauri::generate_context!())
        .expect("error while running tauri application");
}

メモ

  • 使用する場合はFile Systemプラグインを用いる
    • このプラグインを用いると取り扱えるディレクトリに強力な制限がかかる
    • 決まった位置にあるファイルを使用する場合は基本プラグインを用いる方が良い
      • ファイル位置によってpermissionsを細かく追加する必要がある

クリップボード読み書き

実装機能

img_control_clipboard

実装コード

./src-tauri/capabilities/default.json
{
  "permissions": [
    "clipboard-manager:allow-read-text",
    "clipboard-manager:allow-write-text",
    "clipboard-manager:allow-clear"
  ]
}
./src/routes/+page.svelte
<script lang="ts">
  import { readText, writeText, clear } from "@tauri-apps/plugin-clipboard-manager";

  let coreMsg = $state("");

  function readClipboard() {
    readText()
      .then(v => coreMsg = v)
      .catch(e => coreMsg = e);
  }
  function writeClipboard() {
    writeText("write to clipboard by view")
      .then(null)
      .catch(e => coreMsg = e);
  }
  function clearClipboard() {
    clear()
    .then(null)
    .catch(e => coreMsg = e);
  }
</script>

<p>core message: <input type="text" bind:value={coreMsg} size="50" /></p>
<ul>
  <li><button type="button" onclick={readClipboard}>read</button></li>
  <li><button type="button" onclick={writeClipboard}>write</button></li>
  <li><button type="button" onclick={clearClipboard}>clear</button></li>
</ul>
./src-tauri/src/lib.rs
#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
    tauri::Builder::default()
        .plugin(tauri_plugin_clipboard_manager::init())
        .setup(|app| {
            use tauri_plugin_clipboard_manager::ClipboardExt;
            let _ = app.clipboard().write_text("write to clipboard by core");
            Ok(())
        })
        .run(tauri::generate_context!())
        .expect("error while running tauri application");
}

メモ

  • Clipboardプラグインを用いる
  • 使用する機能に応じてpermissionsを細かく追加する必要がある

動的ウィンドウ設定

実装機能

img_dynamic_window

実装コード

./src/routes/+page.svelte
<script lang="ts">
  import { invoke } from "@tauri-apps/api/core";

  let coreMsg = $state("");

  function toggleDecoration() {
    invoke<string>("toggle_decoration").then(null);
  }
</script>

<p>core message: <input type="text" bind:value={coreMsg} size="50" /></p>
<button type="button" onclick={toggleDecoration}>toggle</button>
./src-tauri/src/lib.rs
#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
    tauri::Builder::default()
        .invoke_handler(tauri::generate_handler![toggle_decoration])
        .run(tauri::generate_context!())
        .expect("error while running tauri application");
}

#[tauri::command]
async fn toggle_decoration(window: tauri::Window) {
    if let Ok(is_decorated) = window.is_decorated() {
      let _ = window.set_decorations(!is_decorated);
    }
}

メモ

  • tauri::Windowを操作することでウィンドウ設定を動的に変更可能

ドラッグ&ドロップ

実装機能

img_drag_drop

実装コード

./src/routes/+page.svelte
<script lang="ts">
  import { onDestroy } from "svelte";
  import { getCurrentWebview } from "@tauri-apps/api/webview";
  import { type UnlistenFn } from "@tauri-apps/api/event";

  let coreMsg = $state("");
  let color = $state("text-black");

  async function startListenDragDrop(): Promise<UnlistenFn> {
    return await getCurrentWebview().onDragDropEvent((ev) => {
      if (ev.payload.type === "over") {
        coreMsg = "trigger drag over";
      } else if (ev.payload.type === "enter") {
        coreMsg = "trigger drag enter";
        color = "text-red-500";
      } else if (ev.payload.type === "drop") {
        coreMsg = "trigger drag drop: " + ev.payload.paths.join(";");
        color = "text-black";
      } else if (ev.payload.type === "leave") {
        coreMsg = "trigger drag leave";
        color = "text-black";
      }
    });
  }

  const unlisten = startListenDragDrop().then(null);
  onDestroy(() => {
    unlisten.then(null);
  });
</script>

<p class={color}>core message: <input type="text" bind:value={coreMsg} size="50" /></p>
./src-tauri/src/lib.rs
#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
    tauri::Builder::default()
        .run(tauri::generate_context!())
        .expect("error while running tauri application");
}

メモ

  • 公式のサンプル参考
    • 実際は取り扱うevent.payload.typeの値がサンプルの記載と異なるため注意
      • とはいえ型からのコード補完で選択するだけでよい
  • Tauri機能を使用せずWeb技術だけで対応する場合、以下設定が必要
    • tauri.conf.jsonWindowConfigdragDropEnabledfalse設定

CLI引数取得

実装機能

img_cli_args

実装コード

./src-tauri/capabilities/default.json
{
  "permissions": [
    "cli:default"
  ]
}
./src-tauri/tauri.conf.json
{
  "plugins": {
    "cli": {
      "description": "CLI test",
      "args": [
        {
          "name": "source",
          "index": 1,
          "takesValue": true
        }
      ]
    }
  }
}
<script lang="ts">
  import { getMatches } from "@tauri-apps/plugin-cli";

  let coreMsg = $state("");
  getArgValueByName("source").then(v => coreMsg = v);

  async function getArgValueByName(name: string): Promise<string> {
    const source = (await getMatches().catch(() => undefined))?.args[name]?.value;
    if (typeof source === "string") {
      return source;
    } else if (typeof source === "boolean") {
      return source.toString();
    } else if (Array.isArray(source)) {
      return source.join(",");
    } else {
      return "";
    }
  }
</script>

<p>core message: <input type="text" bind:value={coreMsg} size="50" /></p>
#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
    tauri::Builder::default()
        .plugin(tauri_plugin_cli::init())
        .run(tauri::generate_context!())
        .expect("error while running tauri application");
}

メモ

雑記

右クリックメニューのカスタマイズが英語ですらgoogle検索で情報がほぼ出てこなかったので特に苦労しました。みんな普通にカスタマイズするだろうに、ドンピシャの例を公式に載せておいてほしかったです・・・。

参考文献

Discussion