catalinaの備忘録

ソフトウェアやハードウェアの備忘録。後で逆引きできるように。

tauriでイメージビューアーを作ってみる

言語や技術に対する理解が深まってくると、できることが増えてきて楽しくなってきますね。かたりぃなです。

今回はtauriでローカルファイルのイメージビューアーを作ってみたいと思います。ローカルファイルのイメージリストは以前にpythonで作ったもの(https://catalina1344.hatenablog.jp/entry/2017/03/30/221959)があるので、これを使います。

今回はこのイメージリストをtauriで表示することが目標です。

完成品はこんな感じです。

技術要素としては、RustとReactJSそれぞれでこんな感じです。

Rust側 - ローカルに保存されたイメージリストファイルを読み込む - イメージファイルのリストを生成する - イメージファイルのリスト中の任意の範囲を取得できるコマンドを公開する

React側 - Rustのコマンドを呼び出して、イメージリストを取得する - 取得したイメージリストをもとに、画像を表示する - イメージリストは数が多いので、表示するために必要な範囲だけ読み出す

というわけでやっていこうと思います。

やってることは難しくはないので、慣れない言語で詰まったポイントをメモしていきます。 たぶん初心者あるある案件だろうと思います。

  • ローカルファイルをtauriのjsで表示できない
    • tauriの公式ドキュメントに従って対応しましょう
  • rustのテストがうまく動かない
    • 非同期関数をテストするときは非同期ランタイムに合わせた記述をしましょう
  • rustのテストでprintfデバッグしたい
  • スライスを使うとライフタイムでエラーが出る
    • 慣れないうちは一旦vecを複製する等の方法で書いてみて、後から修正していくほうが良さそう。
  • reactで無限スクロールしたい
    • ライブラリの指定どおりに対応しましょう

ローカルファイルをtauriで表示するには?

imgタグのsrcにURLとしてc:/~とか書いても普通にダメです。

公式ドキュメント:https://tauri.app/v1/api/js/tauri/#convertfilesrc

これに従って普通に処理することで解決できました。

必要な作業は、

  • react側で該当するローカルファイルパスに関する操作を追加する
  • 設定ファイルにそれを許可するよう書き加える

の2つです。 それぞれ見てみます。

reactの実装

convertFileSrcというのを使います。

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

こういう形でインポートして

<img src=convertFileSrc("local/file/path")></img>

こうやって読み込むだけでした。

設定ファイルの記述

/project-dir/src-tauri/tauri.conf.json をこんな感じに編集します。

{
  "tauri": {
    "allowlist": {
      "protocol": {
        "asset": true,
        "assetScope": [ "**" ]
      }
    },
    "security": {
      "csp": "default-src 'self'; img-src 'self' asset: https://asset.localhost"
    },
}

tauri/allowlistとtauri/securityは最初からあるはずなので、

  • protocol を追加
  • security を編集

の2つの作業をすることになるはずです。

rustの非同期関数のテストがうまく動かない

非同期関数をテストするときはこう書くみたいです

    #[tokio::test]
    async fn function_name(){

非同期ランタイムを指定するみたいですね。

rustのテストでprintデバッグができない

cargo testだとprintln!とかで出してる標準出力が捨てられるみたいです。 捨てないようにするオプションがあるのでそれを指定します。

cargo test -- --nocapture

スライスを返す関数でライフタイムでエラーが出る

見出しの文言だけだと「ローカル変数のポインタは返せないって何度言えば。。。」って感じですが、、、やりたいことはバイナリファイルのチャンク分割かそういう類のやつです。

rust的には「引数で受け取ったスライスの部分スライスを返したい」です。

C言語的にいうと「引数で与えられたバッファの特定のアドレスを返す」とかですね。

なので何も問題はないはずです。

最初は解決方法がわからなかったので、こんな感じでVecを複製して返してました。 引数もVecの参照です。

スライスを渡せないとか効率が悪いとか問題は多々あるのですが、動きます。

fn subset<T: std::clone::Clone>(cards:&Vec<T>, page:usize, page_size:usize) -> Vec<T>{
    let start:usize = page * page_size,
    let end:usize = start + page_size;

    if cards.len() < start {
        // 要素が取れないときはempty
        return cards[0..0].to_vec();
    }else if cards.len() < end{
        // 要素がとれるけど、最終ページ
        return cards[start..cards.len()].to_vec()
    }else{
        return cards[start..end].to_vec()
    }
}

次はこんな感じにしてみました。

fn subset<'a, T>(cards:& 'a [T], page:usize, page_size:usize) -> & 'a [T]{
    let start:usize = page * page_size,
    let end:usize = start + page_size;

    if cards.len() < start {
        // 要素が取れないときはempty
        return &cards[0..0];
    }else if cards.len() < end{
        // 要素がとれるけど、最終ページ
        return &cards[start..cards.len()]
    }else{
        return &cards[start..end]
    }
}
  • 引数にスライスの参照をとり、戻り値でもスライスの参照を返す。
    • 戻り値のライフタイムは第一引数のライフタイムと同じだよと伝える
  • 戻り値を作るところでto_vec(実質clone)していたのがすべて不要に。
    • cloneしなくなったので、ジェネリック型Tに対してcloneトレイトが不要になった

これで無駄な複製は行われなくなりました。

最終形。

ライフタイム指定子はいらないみたいです。

fn subset<T>(cards:&[T], page:usize, page_size:usize) -> &[T]{
    let start:usize = page * page_size,
    let end:usize = start + page_size;

    if cards.len() < start {
        // 要素が取れないときはempty
        return &cards[0..0];
    }else if cards.len() < end{
        // 要素がとれるけど、最終ページ
        return &cards[start..cards.len()]
    }else{
        return &cards[start..end]
    }
}

https://doc.rust-jp.rs/book-ja/ch10-03-lifetime-syntax.html 「ライフタイム引数が1つだけなので、そのライフタイムがすべての出力ライフタイムに代入される。」

pageとpage_sizeは参照型ではないのでライフタイムは持たないため、ライフタイム引数はcards1つだけです。 つまり、ライフタイム引数cardsは、出力ライフタイム -> &[T] に代入されるということで、前の例にある'aは不要でした。

reactで無限スクロール

ようつべの動画リストとかのアレですね。リストの最後まできたら、次が読み込まれるやつです。

react_infinite-scrollerってのを使えば簡単みたいなのでやってみます。

まずtauriのプロジェクトディレクトリで

npm install react-infinite-scroller

して、コードを書いていくだけです。

全体像としてはこんな感じです。

function App() {
  const [cardlist, setCardList] = useState([]);         // 表示するリスト
  const [hasMore, setHasMore] = useState(true);         // 再読み込み判定
  const [isFetching, setIsFetching] = useState(false);  // フェッチ中かどうか
  const [page, setPage] = useState(0);                  // 読み込み済み表示ページ

  //項目を読み込むときのコールバック
  const loadMore = async (page) => {
    setIsFetching(true);        // 次のコールバックは一旦呼ばれないようにする

    // rust側からデータを取ってくる
    let new_cardlist = await invoke("cardlist",{page:page, pageSize:1});
    setPage(page);
    console.log(new_cardlist);

    //データ件数が0件の場合、処理終了
    if (new_cardlist.length < 1) {
      setHasMore(false);
      return;
    }

    //取得データをリストに追加
    let newlist = new_cardlist.map((c) => {c.imageUrl=convertFileSrc(c.imageUrl); return c;});
    setCardList([...cardlist,  ...newlist]);

    setIsFetching(false);
  }

  return (
    <div className="container">
      <InfiniteScroll
        loadMore={loadMore}    // 項目を読み込むためのコールバックを指定
        hasMore={!isFetching && hasMore}      //読み込みを行うかどうかの判定。末尾まで読んだか、フェッチ中の場合は読まない
        loader={<div> Loading ...</div>}      //読み込み中に表示される
        >
        {cardlist.map((c) => <CardEntry name={c.name} color={c.color} image_url={c.imageUrl} key={c.id}></CardEntry>)}
      </InfiniteScroll>
    </div>
  );
}

詰まったポイントは1つだけ。

  • 項目を読み込むコールバックが短時間で連続して呼び出されることがある。

です。

GUIや入力系のプログラム書いてるとよくあるやつですね。

react詳しくないので、こちらの記事を参考に同じように処理しました。 https://qiita.com/joe-king-sh/items/32bf2973e9d52eeaf069

要は、 「項目読み込みコールバックが起動したら、それが終わるまでは次のコールバックは起動させないようにフラグで制御する」 ってことですね。

感想と今後の展望

というわけで、rustでイメージビューアーができました。 大したことはやってないのですが、グラフィック的なものが表示されると何かやってる感が出てモチベーション上がってきます。 それでは今回はこれくらいで。