「Ace」を使って「TAURI」で「テキストエディタ」アプリを作ろう

2024年2月6日(火)
大西 武 (オオニシ タケシ)
第9回の今回は、JavaScriptライブラリ「Ace」を使って、「TAURI」でテキストエディタのデスクトップアプリを開発していきます。

メニューの実行

これまでの連載ではJavaScript側からRustを呼び出していましたが、今回は図3のようにRust側からJavaScriptを呼び出します。それにはJavaScriptでRust側からのメッセージ呼び出しの受け取り状態をセット(listen)し、RustからのメッセージをJavaScriptで受け取ったときに処理を実行します。

ここではバックエンドのRust側で新たな機能として「メニューバー」を実装し、「メニュー」が実行された際にフロントエンドのWebページのJavaScriptにメッセージを送ります。

図3:JavaScript側でメッセージ受け取り状態の図

JavaScript側でメッセージを受け取る

まず、例として「New」メニューを実行したらJavaScriptに「new-file」メッセージを送り、Aceエディタの文字列を空(から)にして初期化します。そのためにはJavaScript側でTAURIのイベントの「listen」メソッドを使います。文字通りlistenメソッドで聞き耳を立てておくわけです。

なお、メッセージが呼ばれる前にメッセージの受け取り状態をセットしておかなければ、Rustからメッセージが送られてきても受け取れないため何も処理されません。

・「src」→「main.js」ファイルのサンプルコード
01const { invoke } = window.__TAURI__.tauri;
02 
03let editor;
04 
05window.addEventListener("DOMContentLoaded", () => {
06  editor = ace.edit('text_edit');
07  editor.focus();
08  editor.setFontSize(16);
09  //Newメニューから呼ばれる設定
10  setNew();
11  editor.session.setMode('ace/mode/html');
12});
13//Newメニューから呼ばれるセットをする関数
14async function setNew() {
15  await window.__TAURI__.event.listen('new-file', () => {
16    editor.session.getDocument().setValue("");
17  });
18}

【サンプルコードの解説】
DOMが全て読み込み完了したら「setNew」関数を呼び出します。
setNew関数で、Rustからの「new-file」メッセージを受け取った際に、Aceエディタのドキュメントの値を「""」(空)にするようにセットします。

Rustからメニューを実行

「New」と「Quit」のカスタムメニューを作成して「File」サブメニューに追加し、さらにメニューにも追加すればメニューバーの準備は完了です。TAURIビルダーの「menu」メソッドでメニューバーをウィンドウにセットします。「on_menu_event」メソッドでメニューがクリックされてメニューイベントを実行します。"new"メニューが実行されたらJavaScript側に"new-file"メッセージを送り(emit_all)ます。"quit"メニューが実行されたならプロセスから「exit」します。

お気づきかと思いますが、基本的にサンプルコードにコメントがある部分やその関数内、繋がりのあるコードなどが追記部分です。主にRust(main.rs)やJavaScript(main.js)のコードだけコメントしています(追記するコメントが抜けていたら申し訳ありません)。

・「src-tauri」→「src」→「main.rs」ファイルのサンプルコード
01#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
02//メニューを扱うクレート
03use tauri::{CustomMenuItem,Menu,MenuItem,Submenu,Manager};
04 
05fn main() {
06  //Newメニュー
07  let new_file = CustomMenuItem::new(
08    "new".to_string(), "New");
09  //Quitメニュー
10  let quit = CustomMenuItem::new(
11    "quit".to_string(), "Quit");
12  //Fileメニュー
13  let menu_file = Submenu::new(
14    "File",
15    Menu::new()
16      .add_item(new_file)
17      .add_native_item(MenuItem::Separator)
18      .add_item(quit));
19  //全メニューにFileメニューを追加
20  let menu = Menu::new()
21    .add_submenu(menu_file);
22 
23  tauri::Builder::default()
24    .invoke_handler(tauri::generate_handler![])
25  //メニューのセット
26  .menu(menu)
27    .on_menu_event(|event| {
28      match event.menu_item_id() {
29        //Newメニューを実行する時
30        "new" => {
31          event
32            .window()
33            .emit_all("new-file", "")
34            .unwrap();
35        }
36        //Quitメニューを実行する時
37        "quit" => {
38          std::process::exit(0);
39        }
40        _ => {}
41      }
42    })
43    .run(tauri::generate_context!())
44    .expect("error while running tauri application");
45}

【サンプルコードの解説】
「use」文で「CustomMenuItem」「Menu」「MenuItem」「Submenu」「Manager」構造体を使えるようにします。
「CustomMenuItem」構造体でサブメニュー内の「New」「Quit」カスタムメニューのインスタンスを作成します。
「Submenu」構造体でメニュー内の「File」サブメニューのインスタンスを作成し、カスタムメニューやセパレータを追加します。
「Menu」構造体でメニューバーに「File」サブメニューを追加したインスタンスを生成し「menu」変数に代入します。
TAURIビルダーの「menu」メソッドで「menu」変数をメニューバーにセットします。
TAURIビルダーの「on_menu_event」メソッドで「new」「quit」メニューを実行するメッセージの送信設定をします。

「Font」「Mode」「Theme」メニュー

先ほど「New」メニューを実装した要領で「Font」「Mode」「Theme」メニューも実装します。「Font」メニューにはフォントサイズ「12」「16」「24」ピクセルのメニュー、「Mode」メニューには「Text」「HTML」「JavaScript」メニュー、「Theme」メニューには「dracula」「terminal」「chrome」メニューがあります。

「New」メニューと同様のやり方なので、ここでは一気にまとめて「Font」「Mode」「Theme」メニューも実装します(図4)。たくさんコードを書きますが、簡単ですね。

図4:本文のサンプルコードを開いたmain.jsファイルをキャプチャしたフォントサイズ24、JavaScriptモード、draculaテーマのAceエディタ(図のソースコードにある「フォントサイズ16」や「ace/mode/html」は起動時の値)

「Font」「Mode」「Theme」メニューのJavaScript

「Font」メニューの「12」「16」「24」メニューの'font-size'メッセージを受け取ったら、Aceエディタの「setFontSize」メソッドでメニュー名の数値と同じフォントサイズをセットします。

「Mode」メニューの「Text」「HTML」「JavaScript」メニューの'mode'メッセージを受け取ったら、Aceエディタの「setMode」メソッドでシンタックスハイライトのモードをセットします。

「Theme」メニューの「dracula」「terminal」「chrome」メニューの'theme'メッセージを受け取ったら、Aceエディタの「setTheme」メソッドで背景色などの色の組み合わせのテーマをセットします。

・「src」→「main.js」ファイルのサンプルコード
01const { invoke } = window.__TAURI__.tauri;
02 
03let editor;
04 
05window.addEventListener("DOMContentLoaded", () => {
06  editor = ace.edit('text_edit');
07  editor.focus();
08  editor.setFontSize(16);
09  //Fontサイズメニューが呼ばれる設定
10  setFontSize();
11  setNew();
12  //Modeメニューが呼ばれる設定
13  setMode();
14  //Themeメニューが呼ばれる設定
15  setTheme();
16  editor.session.setMode('ace/mode/html');
17});
18 
19async function setNew() {
20  await window.__TAURI__.event.listen('new-file', () => {
21    editor.session.getDocument().setValue("");
22  });
23}
24//Fontサイズメニューが呼ばれるセットをする関数
25async function setFontSize() {
26  await window.__TAURI__.event.listen('font-size', event => {
27    editor.setFontSize(event.payload);
28  });
29}
30//Modeメニューが呼ばれるセットをする関数
31async function setMode() {
32  await window.__TAURI__.event.listen('mode', event => {
33    editor.session.setMode('ace/mode/'+event.payload);
34  });
35}
36//Themeメニューが呼ばれるセットをする関数
37async function setTheme() {
38  await window.__TAURI__.event.listen('theme', event => {
39    editor.setTheme('ace/theme/'+event.payload);
40  });
41}

【サンプルコードの解説】
DOMが全て読み込み完了したら「setFontSize」「setMode」「setTheme」関数を呼び出します。
setFontSize関数でRustからの「font-size」メッセージを受け取った際に、Aceエディタのフォントサイズをメニューから受け取った引数の値(event.payload)をそのままセットします。
setMode関数でRustからの「mode」メッセージを受け取った際に、Aceエディタのモードを'ace/mode/'にメニューから受け取った引数の値(event.payload)をそのまま繋げてセットします。
setTheme関数でRustからの「font-size」メッセージを受け取った際に、Aceエディタのテーマを'ace/theme/'にメニューから受け取った引数の値(event.payload)をそのまま繋げてセットします。

「Font」「Mode」「Theme」メニューの実行

「New」メニューと同様に「Font」メニューの「12」「16」「24」メニュー、「Mode」メニューの「Text」「HTML」「JavaScript」メニュー、「Theme」メニューの「dracula」「terminal」「chrome」メニューを一気に実装します。

フォントサイズは任意の正の整数でもOKで、モードも「CSS」など様々なファイル形式に対応しており、テーマも「xcode」などいろいろあります。メニューを増やして改造してみると良いでしょう。

・「src-tauri」→「src」→「main.rs」ファイルのサンプルコード
001#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
002 
003use tauri::{CustomMenuItem, Menu, MenuItem, Submenu,Manager};
004 
005fn main() {
006  let new_file = CustomMenuItem::new(
007    "new".to_string(), "New");
008  let quit = CustomMenuItem::new(
009    "quit".to_string(), "Quit");
010  //フォントサイズ12メニュー
011  let font12 = CustomMenuItem::new(
012    "font12".to_string(), "12");
013  //フォントサイズ16メニュー
014  let font16 = CustomMenuItem::new(
015    "font16".to_string(), "16");
016  //フォントサイズ24メニュー
017  let font24 = CustomMenuItem::new(
018    "font24".to_string(), "24");
019  //Textモードメニュー
020  let mode_text = CustomMenuItem::new(
021    "mode_text".to_string(), "Text");
022  //HTMLモードメニュー
023  let mode_html = CustomMenuItem::new(
024    "mode_html".to_string(), "HTML");
025  //JavaScriptモードメニュー
026  let mode_js = CustomMenuItem::new(
027    "mode_js".to_string(), "JavaScript");
028  //draculaテーマメニュー
029  let theme_dracula = CustomMenuItem::new(
030    "theme_dracula".to_string(), "dracula");
031  //terminalテーマメニュー
032  let theme_terminal = CustomMenuItem::new(
033    "theme_terminal".to_string(), "terminal");
034  //chromeテーマメニュー
035  let theme_chrome = CustomMenuItem::new(
036    "theme_chrome".to_string(), "chrome");
037  let menu_file = Submenu::new(
038    "File",
039    Menu::new()
040      .add_item(new_file)
041      .add_native_item(MenuItem::Separator)
042      .add_item(quit));
043  //Fontサイズメニュー
044  let menu_font = Submenu::new(
045    "Font",
046    Menu::new()
047      .add_item(font12)
048      .add_item(font16)
049      .add_item(font24));
050  //Modeメニュー
051  let menu_mode = Submenu::new(
052    "Mode",
053    Menu::new()
054      .add_item(mode_text)
055      .add_item(mode_html)
056      .add_item(mode_js));
057  //Themeメニュー
058  let menu_theme = Submenu::new(
059    "Theme",
060    Menu::new()
061      .add_item(theme_dracula)
062      .add_item(theme_terminal)
063      .add_item(theme_chrome));
064   //メニューバーにFile・Font・Mode・Themeメニューを追加
065  let menu = Menu::new()
066    .add_submenu(menu_file)
067    .add_submenu(menu_font)
068    .add_submenu(menu_mode)
069    .add_submenu(menu_theme);
070 
071  tauri::Builder::default()
072    .invoke_handler(tauri::generate_handler![])
073  .menu(menu)
074    .on_menu_event(|event| {
075      match event.menu_item_id() {
076        "new" => {
077          event
078            .window()
079            .emit_all("new-file", "")
080            .unwrap();
081        }
082        "quit" => {
083          std::process::exit(0);
084        }
085        //Font12メニューを実行する時
086        "font12" => {
087          event
088            .window()
089            .emit_all("font-size", 12)
090            .unwrap();
091        }
092        //Font16メニューを実行する時
093        "font16" => {
094          event
095            .window()
096            .emit_all("font-size", 16)
097            .unwrap();
098        }
099        //Font24メニューを実行する時
100        "font24" => {
101          event
102            .window()
103            .emit_all("font-size", 24)
104            .unwrap();
105        }
106        //Textモードメニューを実行する時
107        "mode_text" => {
108          event
109            .window()
110            .emit_all("mode", "text")
111            .unwrap();
112        }
113        //HTMLモードメニューを実行する時
114        "mode_html" => {
115          event
116            .window()
117            .emit_all("mode", "html")
118            .unwrap();
119        }
120        //JavaScriptモードメニューを実行する時
121        "mode_js" => {
122          event
123            .window()
124            .emit_all("mode", "javascript")
125            .unwrap();
126        }
127        //draculaテーマメニューを実行する時
128        "theme_dracula" => {
129          event
130            .window()
131            .emit_all("theme", "dracula")
132            .unwrap();
133        }
134        //terminalテーマメニューを実行する時
135        "theme_terminal" => {
136          event
137            .window()
138            .emit_all("theme", "terminal")
139            .unwrap();
140        }
141        //chromeテーマメニューを実行する時
142        "theme_chrome" => {
143          event
144            .window()
145            .emit_all("theme", "chrome")
146            .unwrap();
147        }
148        _ => {}
149      }
150    })
151    .run(tauri::generate_context!())
152    .expect("error while running tauri application");
153}

【サンプルコードの解説】
「CustomMenuItem」構造体でサブメニュー内の「12」「16」「24」「Text」「HTML」「JavaScript」「dracula」「terminal」「chrome」カスタムメニューのインスタンスを作成します。

「Submenu」構造体でメニュー内の「Font」「Mode」「Theme」サブメニューのインスタンスを作成し、カスタムメニューを追加します。

「Menu」構造体でメニューバーに「Font」「Mode」「Theme」サブメニューを追加するインスタンスを生成し「menu」変数に代入します。

TAURIビルダーの「menu」メソッドで「menu」変数をセットします。

TAURIビルダーの「on_menu_event」メソッドで「font12」「font16」「font24」「mode_text」「mode_html」「mode_js」「theme_dracula」「theme_terminal」「theme_chrome」メニューを実行した際のメッセージ送信設定をします。

著者
大西 武 (オオニシ タケシ)
1975年香川県生まれ。大阪大学経済学部経営学科中退。プログラミング入門書など30冊以上を商業出版する作家。Microsoftで大賞やNTTドコモでグランプリなど20回以上全国区のコンテストに入賞するアーティスト。オリジナルの間違い探し「3Dクイズ」が全国放送のTVで約10回出題。
https://profile.vixar.jp

連載バックナンバー

開発言語技術解説
第15回

「TAURI」でデータベースを使ってみよう

2024/5/10
第15回の今回は「TAURI」でオープンソースのデータベース「SQLite3」を使用して、テーブル表に表示する解説をしていきます。
開発言語技術解説
第14回

「TAURI」で気象庁の「CSVデータ」を解析する

2024/5/1
第14回の今回は気象庁のWebサイトから指定した地域の1年間の気象データをダウンロードして「TAURI」で解析していきます。
開発言語技術解説
第13回

「TAURI」で「簡易RSSリーダー」を開発してみよう

2024/4/16
第13回の今回は「TAURI」で「RSSフィード」を読み込んでWebページに一覧表示し、リンクのページを開くための新規ウィンドウを作成するところまでを解説します。

Think ITメルマガ会員登録受付中

Think ITでは、技術情報が詰まったメールマガジン「Think IT Weekly」の配信サービスを提供しています。メルマガ会員登録を済ませれば、メルマガだけでなく、さまざまな限定特典を入手できるようになります。

Think ITメルマガ会員のサービス内容を見る

他にもこの記事が読まれています