「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」ファイルのサンプルコード
const { invoke } = window.__TAURI__.tauri;

let editor;

window.addEventListener("DOMContentLoaded", () => {
  editor = ace.edit('text_edit');
  editor.focus();
  editor.setFontSize(16);
  //Newメニューから呼ばれる設定
  setNew();
  editor.session.setMode('ace/mode/html');
});
//Newメニューから呼ばれるセットをする関数
async function setNew() {
  await window.__TAURI__.event.listen('new-file', () => {
    editor.session.getDocument().setValue("");
  });
}

【サンプルコードの解説】
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」ファイルのサンプルコード
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
//メニューを扱うクレート
use tauri::{CustomMenuItem,Menu,MenuItem,Submenu,Manager};

fn main() {
  //Newメニュー
  let new_file = CustomMenuItem::new(
    "new".to_string(), "New");
  //Quitメニュー
  let quit = CustomMenuItem::new(
    "quit".to_string(), "Quit");
  //Fileメニュー
  let menu_file = Submenu::new(
    "File",
    Menu::new()
      .add_item(new_file)
      .add_native_item(MenuItem::Separator)
      .add_item(quit));
  //全メニューにFileメニューを追加
  let menu = Menu::new() 
    .add_submenu(menu_file);

  tauri::Builder::default()
    .invoke_handler(tauri::generate_handler![])
  //メニューのセット
  .menu(menu)
    .on_menu_event(|event| {
      match event.menu_item_id() {
        //Newメニューを実行する時
        "new" => {
          event
            .window()
            .emit_all("new-file", "")
            .unwrap();
        }
        //Quitメニューを実行する時
        "quit" => {
          std::process::exit(0);
        }
        _ => {}
      }
    })
    .run(tauri::generate_context!())
    .expect("error while running tauri application");
}

【サンプルコードの解説】
「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」ファイルのサンプルコード
const { invoke } = window.__TAURI__.tauri;

let editor;

window.addEventListener("DOMContentLoaded", () => {
  editor = ace.edit('text_edit');
  editor.focus();
  editor.setFontSize(16);
  //Fontサイズメニューが呼ばれる設定
  setFontSize();
  setNew();
  //Modeメニューが呼ばれる設定
  setMode();
  //Themeメニューが呼ばれる設定
  setTheme();
  editor.session.setMode('ace/mode/html');
});

async function setNew() {
  await window.__TAURI__.event.listen('new-file', () => {
    editor.session.getDocument().setValue("");
  });
}
//Fontサイズメニューが呼ばれるセットをする関数
async function setFontSize() {
  await window.__TAURI__.event.listen('font-size', event => {
    editor.setFontSize(event.payload);
  });
}
//Modeメニューが呼ばれるセットをする関数
async function setMode() {
  await window.__TAURI__.event.listen('mode', event => {
    editor.session.setMode('ace/mode/'+event.payload);
  });
}
//Themeメニューが呼ばれるセットをする関数
async function setTheme() {
  await window.__TAURI__.event.listen('theme', event => {
    editor.setTheme('ace/theme/'+event.payload);
  });
}

【サンプルコードの解説】
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」ファイルのサンプルコード
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]

use tauri::{CustomMenuItem, Menu, MenuItem, Submenu,Manager};

fn main() {
  let new_file = CustomMenuItem::new(
    "new".to_string(), "New");
  let quit = CustomMenuItem::new(
    "quit".to_string(), "Quit");
  //フォントサイズ12メニュー
  let font12 = CustomMenuItem::new(
    "font12".to_string(), "12");
  //フォントサイズ16メニュー
  let font16 = CustomMenuItem::new(
    "font16".to_string(), "16");
  //フォントサイズ24メニュー
  let font24 = CustomMenuItem::new(
    "font24".to_string(), "24");
  //Textモードメニュー
  let mode_text = CustomMenuItem::new(
    "mode_text".to_string(), "Text");
  //HTMLモードメニュー
  let mode_html = CustomMenuItem::new(
    "mode_html".to_string(), "HTML");
  //JavaScriptモードメニュー
  let mode_js = CustomMenuItem::new(
    "mode_js".to_string(), "JavaScript");
  //draculaテーマメニュー
  let theme_dracula = CustomMenuItem::new(
    "theme_dracula".to_string(), "dracula");
  //terminalテーマメニュー
  let theme_terminal = CustomMenuItem::new(
    "theme_terminal".to_string(), "terminal");
  //chromeテーマメニュー
  let theme_chrome = CustomMenuItem::new(
    "theme_chrome".to_string(), "chrome");
  let menu_file = Submenu::new(
    "File",
    Menu::new()
      .add_item(new_file)
      .add_native_item(MenuItem::Separator)
      .add_item(quit));
  //Fontサイズメニュー
  let menu_font = Submenu::new(
    "Font",
    Menu::new()
      .add_item(font12)
      .add_item(font16)
      .add_item(font24));
  //Modeメニュー
  let menu_mode = Submenu::new(
    "Mode",
    Menu::new()
      .add_item(mode_text)
      .add_item(mode_html)
      .add_item(mode_js));
  //Themeメニュー
  let menu_theme = Submenu::new(
    "Theme",
    Menu::new()
      .add_item(theme_dracula)
      .add_item(theme_terminal)
      .add_item(theme_chrome));
   //メニューバーにFile・Font・Mode・Themeメニューを追加
  let menu = Menu::new()
    .add_submenu(menu_file)
    .add_submenu(menu_font)
    .add_submenu(menu_mode)
    .add_submenu(menu_theme);

  tauri::Builder::default()
    .invoke_handler(tauri::generate_handler![])
  .menu(menu)
    .on_menu_event(|event| {
      match event.menu_item_id() {
        "new" => {
          event
            .window()
            .emit_all("new-file", "")
            .unwrap();
        }
        "quit" => {
          std::process::exit(0);
        }
        //Font12メニューを実行する時
        "font12" => {
          event
            .window()
            .emit_all("font-size", 12)
            .unwrap();
        }
        //Font16メニューを実行する時
        "font16" => {
          event
            .window()
            .emit_all("font-size", 16)
            .unwrap();
        }
        //Font24メニューを実行する時
        "font24" => {
          event
            .window()
            .emit_all("font-size", 24)
            .unwrap();
        }
        //Textモードメニューを実行する時
        "mode_text" => {
          event
            .window()
            .emit_all("mode", "text")
            .unwrap();
        }
        //HTMLモードメニューを実行する時
        "mode_html" => {
          event
            .window()
            .emit_all("mode", "html")
            .unwrap();
        }
        //JavaScriptモードメニューを実行する時
        "mode_js" => {
          event
            .window()
            .emit_all("mode", "javascript")
            .unwrap();
        }
        //draculaテーマメニューを実行する時
        "theme_dracula" => {
          event
            .window()
            .emit_all("theme", "dracula")
            .unwrap();
        }
        //terminalテーマメニューを実行する時
        "theme_terminal" => {
          event
            .window()
            .emit_all("theme", "terminal")
            .unwrap();
        }
        //chromeテーマメニューを実行する時
        "theme_chrome" => {
          event
            .window()
            .emit_all("theme", "chrome")
            .unwrap();
        }
        _ => {}
      }
    })
    .run(tauri::generate_context!())
    .expect("error while running tauri application");
}

【サンプルコードの解説】
「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回出題。

連載バックナンバー

開発言語技術解説
第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メルマガ会員のサービス内容を見る

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