🐈
RustでWebViewGUI
はじめに
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の使用方法
Rustでのweb-viewのソース
javascriptのイベントリスナなどのドキュメント
Discussion