🐈

RustでWebViewGUI

2021/01/31に公開

はじめに

RustでGUIやろうとしてorb-tkを使おうとしたんですが、message.rs回りのサンプルがそのままだと動かないとか、日本語が通らないとかだったのでweb-viewでやることにしました。
wasmも考えたんですが、nativeとライブラリ的なjsの組み合わせでいったん考えることにしました。

問題点

Rust(web-view)+javascriptの問題点は、webview.evalでjsの値を取ってこれないことです。
(方法はあるのかも。自分には見つけられませんでした)
そのため、メッセージを投げるメソッドを作り、handlerで受け取るという形にしました。

本来ならRust側にweb-viewGUIのデータイメージを持ち差分を反映しあう、Vuexみたいな構成がもっともよいと思いますが、現状そこまではやっていません。

ソース

javascript側のlib.jsとrust側のweb-view用のコード、Cargo.tomlに依存ライブラリを設定しました。

Cargo.toml

Cargo.tomlには、以下を追加しています。

  • web-view (GUI環境として )
  • serde,serde_json ( Jsonパーサとして )
  • regex (改行コードの削除用)

以下はdependencesの抜粋です。

[dependencies]
web-view = { version = "0.7" }
serde = { version = "1.0", features = ["derive"] }
serde_json="1.0"
regex="1"

lib.js

javascript側のソースになります。このコードはinclude_str!でhtmlにinjectされます。

//Elementを追加する
function callCreateElement(parent_id, create_type,id,inner_html) {
  // alert(inner_html);
  var node = document.createElement(create_type);
  node.id = id;
  node.innerHTML = inner_html;
  var current = document.getElementById(parent_id);
  current.appendChild(node);
}
// from:https://github.com/jserz/js_piece/blob/master/DOM/ChildNode/remove()/remove().md
(function (arr) {
  arr.forEach(function (item) {
    if (item.hasOwnProperty('remove')) {
      return;
    }
    Object.defineProperty(item, 'remove', {
      configurable: true,
      enumerable: true,
      writable: true,
      value: function remove() {
        this.parentNode.removeChild(this);
      }
    });
  });
})([Element.prototype, CharacterData.prototype, DocumentType.prototype]);
//Elementを削除する
function callRemoveElement(id) {
  // alert(inner_html);
  var node = document.getElementById(id);
  node.remove();
}
//リスナを追加する
function callAddEventListener(id,event_id,handler_id) {
  var elm = document.getElementById(id);
  elm.addEventListener(event_id,function(e){
    var ans_data = { handler: handler_id,
                     id: id,
                     arg: JSON.stringify(e)
                   };
    external.invoke(JSON.stringify(ans_data));
  });
}
// Attributeを設定する
function callSetElementAttribute(id,attribute,value) {
  document.getElementById(id).setAttribute(attribute,value);
}
// Attributeを得る
function callGetElementAttribute(id,attribute,invoke_handler) {
  var elm = document.getElementById(id);
  var ans_data = { handler: invoke_handler,
                   id: id,
                   arg: elm.getAttribute(attribute)
                 };
  external.invoke(JSON.stringify(ans_data));
}

main.rs

Rust側のコードです。
やっていることはjsonで渡されたHandlerをもとにdocumentにinput要素を追加したり削除したりしています。
文字列はquoteしたものと改行を削除したものが必要なのでquoteを行っています。

use regex::Regex;
use serde::{Deserialize, Serialize};
use web_view::*;

#[derive(Serialize, Deserialize, Debug)]
struct Handler {
    //ハンドラID
    handler: String,
    //elementのID
    id: String,
    //ハンドラの引数
    arg: String,
}
//""つきに文字列を変換
pub fn add_double_quote(s: &str) -> String {
    format!("\"{}\"", s)
}

//jsonを色々考えないでいい文字列に変換する
pub fn json_to_encodestr(s: &str) -> String {
    let _quote = Regex::new("\"").unwrap();
    _quote.replace_all(s, "&q;").to_string()
}
//色々考えないでいい文字列からjsonに戻す
pub fn encodestr_to_json(s: &str) -> String {
    let _quote = Regex::new("&q;").unwrap();
    _quote.replace_all(s, "\"").to_string()
}

//改行削除
pub fn string_strip_ret(s: &str) -> String {
    let ret_dos = Regex::new(r"\x0d\x0a").unwrap();
    let ret_mac = Regex::new(r"\x0a\x0d").unwrap();
    let ret_unix = Regex::new(r"[\x0a|\x0d]").unwrap();
    let ret_dos_ans = ret_dos.replace_all(s, "").to_string();
    let ret_mac_ans = ret_mac.replace_all(&ret_dos_ans, "").to_string();
    let ans = ret_unix.replace_all(&ret_mac_ans, "").to_string();
    ans
}
//""つきで、改行を削除
pub fn add_double_quote_by_string_strip_ret(s: &str) -> String {
    let strip_str = string_strip_ret(s);
    add_double_quote(&strip_str)
}
//element作成
fn create_element<T>(
    webview: &mut WebView<T>,
    parent_id: &str,
    create_type: &str,
    id: &str,
    innerhtml: &str,
) -> WVResult {
    webview.eval(&format!(
        "callCreateElement({},{}, {},{})",
        add_double_quote(parent_id),
        add_double_quote(create_type),
        add_double_quote(id),
        add_double_quote_by_string_strip_ret(innerhtml)
    ))
}
//element削除
fn remove_element<T>(webview: &mut WebView<T>, id: &str) -> WVResult {
    webview.eval(&format!("callRemoveElement({})", add_double_quote(id),))
}
//event_listener追加
fn add_event_listener<T>(
    webview: &mut WebView<T>,
    id: &str,
    event_id: &str,
    handler_id: &str,
) -> WVResult {
    webview.eval(&format!(
        "callAddEventListener({},{}, {})",
        add_double_quote(id),
        add_double_quote(event_id),
        add_double_quote(handler_id)
    ))
}
//attributeを設定する
fn set_element_attribute<T>(
    webview: &mut WebView<T>,
    id: &str,
    attribute: &str,
    value: &str,
) -> WVResult {
    webview.eval(&format!(
        "callSetElementAttribute({},{},{})",
        add_double_quote(id),
        add_double_quote(attribute),
        add_double_quote(value)
    ))
}
//attributeを返すコールバックを呼び出してもらう(evalが値をreturnしないので)
fn request_get_element_attribute<T>(
    webview: &mut WebView<T>,
    id: &str,
    attribute: &str,
    invoke_handler_id: &str,
) -> WVResult {
    webview.eval(&format!(
        "callGetElementAttribute({},{},{})",
        add_double_quote(id),
        add_double_quote(attribute),
        add_double_quote(invoke_handler_id)
    ))
}

pub fn main() {
    let html_content = std::format!(
        "<html><body><h1>ボタン追加</h1><div id=top></div><script>{}</script></body></html>",
        include_str!("lib.js") //Javascript側のコールバック集
    );
    let mut webview = web_view::builder()
        .title("My Project")
        .content(Content::Html(html_content))
        .size(320, 480)
        .resizable(false)
        .debug(true)
        .user_data(())
        .invoke_handler(|_webview, _arg| {
            let decode_arg = encodestr_to_json(_arg);
            println!("===> invoke_handler : arg: {}", decode_arg);
            let deserialized: Handler = serde_json::from_str(&decode_arg).unwrap();
            let handler: &str = &deserialized.handler;
            let handler_arg: &str = &deserialized.arg;
            match handler {
                "test_handler" => {
                    //add_btnというIDのボタンを追加する
                    //    topにadd_btnというinputのElementを追加する
                    create_element(_webview, "top", "input", "add_btn", "")?;
                    //    add_btnをbuttonにする
                    set_element_attribute(_webview, "add_btn", "type", "button")?;
                    //    add_btnの名前をtest_handler2にする
                    set_element_attribute(_webview, "add_btn", "value", "test_handler2")?;
                    //    add_btnのclickイベントでtest_handler2が呼び出されるようにする。
                    add_event_listener(_webview, "add_btn", "click", "test_handler2")?;

                    //remove_btnというボタンを追加する
                    //    topにremove_btnというinputのElementを追加する
                    create_element(_webview, "top", "input", "remove_btn", "")?;
                    //    remove_btnをbuttonにする
                    set_element_attribute(_webview, "remove_btn", "type", "button")?;
                    //    remove_btnの名前をtest_handler2にする
                    set_element_attribute(_webview, "remove_btn", "value", "remove_handler")?;
                    //    remove_btnのclickイベントでremove_handlerが呼び出されるようにする。
                    add_event_listener(_webview, "remove_btn", "click", "remove_handler")?
                }
                "test_handler2" => {
                    println!("test_handler2: {}", handler_arg);
                    //add_btnのvalue属性の値を得て、get_element_valueハンドラを呼び出すようにする
                    request_get_element_attribute(
                        _webview,
                        "add_btn",
                        "value",
                        "get_element_value",
                    )?;
                    //add_btnのtype属性の値を得て、get_element_typeハンドラを呼び出すようにする
                    request_get_element_attribute(_webview, "add_btn", "type", "get_element_type")?;
                }
                "remove_handler" => {
                    println!("remove_handler: {}", handler_arg);
                    //top_btnを削除する
                    remove_element(_webview, "top_btn")?
                }
                "get_element_value" => {
                    //valueを取得する
                    println!("get_element_value: {}", handler_arg);
                }
                "get_element_type" => {
                    //typeを取得する
                    println!("get_element_type: {}", handler_arg);
                }
                _ => unimplemented!(),
            }
            Ok(())
        })
        .build()
        .unwrap();

    //top_btnボタンを追加。外ではresultを扱えないのでokを指定
    //    topにtop_btnというinputのElementを追加する
    create_element(&mut webview, "top", "input", "top_btn", "").ok();
    //    top_btnをbuttonにする
    set_element_attribute(&mut webview, "top_btn", "type", "button").ok();
    //    top_btnの名前をtest_handlerにする
    set_element_attribute(&mut webview, "top_btn", "value", "test_handler").ok();
    //    top_btnのclickイベントでtest_handlerが呼び出されるようにする。
    add_event_listener(&mut webview, "top_btn", "click", "test_handler").ok();
    //Webviewを実行
    webview.run().unwrap();
}

おわりに

rustでweb-viewによるgui操作の基本を作成しました。
基本attributeの操作で行えるので、必要な関数はそれほど実装しなくてよさそうでした。
ただ、以下がこれからの課題になります。

  • 上記コードではremoveを再度行えてしまうので、その対応が必要です。addEventListenerにonceを指定して1度だけコールできるものをlib.jsに追加してみましたが、うまくいきませんでした(2度めもコールされる)。
  • EventListenerは関数を渡す必要があるので、removeEventListenerを実装する場合、関数自体をdict(Map?)に入れて管理する必要があるかもしれません。
  • Attributeを返す用に、argにAttirute名も設定したほうが良いように思います。
  • handlerを動的に登録できるようにしたいです。
  • handler経由で値を得るのは手間がかかるので、view用のModelをrust側に持つ方がよいです。その上で差分をやり取りするvuexのような形式にするべきだと思います。
  • wasmでの使用の場合、もっと単純になる可能性があります。ただ、以下の点に考慮が必要です。
    • nodejsと絡ませると必要な手順が増えるのでwasm-packのみなどbuildを単純にしたいです。
    • wasmはnativeに比べて制約がある分、native版はnative版として必要かもしれません。

参考

Rustのでのweb-viewの使用方法

https://qiita.com/osanshouo/items/7966ecbd41bc3ce611dd

Rustでのweb-viewのソース

https://github.com/Boscop/web-view

javascriptのイベントリスナなどのドキュメント

https://developer.mozilla.org/ja/docs/Web/API

Discussion