Давайте воспользуемся фреймворком Rust

Состояние результатов JS за 2022 год побудило меня присмотреться к Таури как к альтернативе Электрону. Одним из главных преимуществ фреймворка, несомненно, является размер конечного пакета приложений.

Основным препятствием, которое остановило меня от создания/конвертации приложения с Electron на Tauri, было отсутствие у меня знаний о Rust. Но просматривая список приложений в репозитории awesome-tauri, я увидел пару приложений, живущих в строке меню, и, наконец, решил создать что-то свое.

Поскольку я не смог найти ничего похожего на модуль менюбар для Tauri, я хочу поделиться в этой статье своим опытом создания простого приложения меню/трея с нуля. Часть кода JavaScript трогать не будем, так как весь необходимый код мы добавим в файлы Rust.

Для тех, кто хочет сразу взглянуть на весь код, проверьте этот репозиторий.

1. Создание нового приложения

Как предлагает документация по быстрому запуску Tauri, давайте возьмем команду npm create и создадим новое приложение:

$ npm create tauri-app

Введите имя приложения, выберите любой менеджер пакетов и базовый фреймворк. Я взял pnpm и vue-ts.

Команда создаст новую папку с базовой структурой приложения Tauri.

Теперь запустите приложение с помощью команды $ pnpm tauri dev и перейдите к коду.

Кстати, вот некоторые рекомендуемые расширения VS Code:

- Tauri — добавляет автозаполнение и проверку конфигурационных файлов, а также команды для запуска, сборки и т.д.
- rust-analyzer — поддержка Rust

2. Добавление функциональности панели меню, также известной как System Tray API.

Как и в Electron, System Tray API отображает значки в строке меню/трее. Что мне нравится в Tauri на данный момент, так это то, что базовая конфигурация приложения хранится в отдельном файле JSON.

Откройте tauri.conf.json и добавьте в него конфигурацию для systemTray:

"systemTray": {
  "iconPath": "icons/icon.png",
  "iconAsTemplate": true
}

Теперь давайте перейдем к коду Rust. Для тех, кто, как и я, новичок в Rust, в официальной документации есть базовые примеры обработчиков лотков, которые мы возьмем для начала. Откройте src-tauri/main.ts и добавьте код, отвечающий за событие щелчка левой кнопкой мыши. Вот код для этого:

use tauri::{Manager, SystemTray, SystemTrayEvent, SystemTrayMenu};
fn main() {
    let system_tray_menu = SystemTrayMenu::new();
    tauri::Builder::default()
        .system_tray(SystemTray::new().with_menu(system_tray_menu))
        .on_system_tray_event(|app, event| match event {
            SystemTrayEvent::LeftClick {
                position: _,
                size: _,
                ..
            } => {
                let window = app.get_window("main").unwrap();
                // toggle application window
                if window.is_visible().unwrap() {
                    window.hide().unwrap();
                } else {
                    window.show().unwrap();
                    window.set_focus().unwrap();
                }
            },
            _ => {}
        })
        .run(tauri::generate_context!())
        .expect("error while running tauri application");
}

Теперь в строке меню должен появиться значок по умолчанию, и вы можете переключать окно приложения, щелкнув по нему.

Git коммит с изменениями

3. Установка правильного положения окна приложения

Это неплохо, но нам нужно изменить положение окна, чтобы оно было рядом с самим значком. Вы можете попробовать рассчитать позицию самостоятельно, но для этого мы пойдем другим путем и воспользуемся для этого пакетом tauri_plugin_positioner. Заодно разберемся, как добавлять плагины в Tauri/Rust.

Это действительно просто, просто откройте cargo.toml и добавьте эту строку с зависимостью:

[dependencies]
tauri-plugin-positioner = { version = "1.0.4", features = ["system-tray"] }

Теперь вернитесь к main.rs и используйте зависимость в нашем коде:

use tauri::{Manager, SystemTray, SystemTrayEvent, SystemTrayMenu};
use tauri_plugin_positioner::{Position, WindowExt};

fn main() {
    let system_tray_menu = SystemTrayMenu::new();
    tauri::Builder::default()
        .plugin(tauri_plugin_positioner::init())
        .system_tray(SystemTray::new().with_menu(system_tray_menu))
        .on_system_tray_event(|app, event| {
            tauri_plugin_positioner::on_tray_event(app, &event);
            match event {
                SystemTrayEvent::LeftClick {
                    position: _,
                    size: _,
                    ..
                } => {
                    let window = app.get_window("main").unwrap();
                    // use TrayCenter as initial window position
                    let _ = window.move_window(Position::TrayCenter);
                    if window.is_visible().unwrap() {
                        window.hide().unwrap();
                    } else {
                        window.show().unwrap();
                        window.set_focus().unwrap();
                    }
                }
                _ => {}
            }
        })
        .run(tauri::generate_context!())
        .expect("error while running tauri application");
}

Git коммит с изменениями

4. Изменение внешнего вида окна

Отлично, теперь наше окно отображается в нужном нам месте. Теперь нам нужно исправить некоторые свойства окна. Например, нам нужно удалить titlebar с кнопками, расположенными слишком близко, и развернуть и свернуть приложение. Для этого снова откройте tauri.conf.json и найдите его window настройки объекта.

Я использовал следующую конфигурацию:

{
  "fullscreen": false,
  "height": 600,
  "resizable": false,
  "title": "menubar",
  "width": 600,
  "visible": false,
  "hiddenTitle": true,
  "decorations": false,
  "focus": false,
  "transparent": false
}

Полный список поддерживаемых свойств доступен в официальной документации.

Git коммит с изменениями

5. Скройте окно, нажав «Снаружи»

В нашем приложении отсутствует еще одна деталь: возможность скрыть приложение, когда вы щелкаете за пределами окна. Это легко сделать, потому что вы можете проверить состояние фокуса окна, используя on_window_event():

fn main() {
    ...
    
    tauri::Builder::default()
        .plugin(tauri_plugin_positioner::init())
        .system_tray(SystemTray::new().with_menu(system_tray_menu))
        .on_system_tray_event(|app, event| {
            ...
        })
        .on_window_event(|event| match event.event() {
            tauri::WindowEvent::Focused(is_focused) => {
                // detect click outside of the focused window and hide the app
                if !is_focused {
                    event.window().hide().unwrap();
                }
            }
            _ => {}
        })
        .run(tauri::generate_context!())
        .expect("error while running tauri application");
}

Git коммит с изменениями

6. Добавление контекстного меню

Конфигурация API системного трея содержит еще один полезный параметр — menuOnLeftClick со значением по умолчанию true. Поскольку левый щелчок уже используется для переключения окна, в нашем случае давайте деактивируем это свойство с помощью следующего кода:

"systemTray": {
  "iconPath": "icons/icon.png",
  "iconAsTemplate": true,
  "menuOnLeftClick": false
}

А теперь мы будем использовать следующий код для создания простого контекстного меню с одним пунктом меню, который открывается при щелчке правой кнопкой мыши по значку menubar:

fn main() {
    let quit = CustomMenuItem::new("quit".to_string(), "Quit").accelerator("Cmd+Q");
    let system_tray_menu = SystemTrayMenu::new().add_item(quit);
    
    tauri::Builder::default()
      ...
      .on_system_tray_event(|app, event| {
        tauri_plugin_positioner::on_tray_event(app, &event);
        match event {
            ...
            SystemTrayEvent::MenuItemClick { id, .. } => match id.as_str() {
                "quit" => {
                    std::process::exit(0);
                }
                _ => {}
            },
            _ => {}
        }
    })

Кроме id и title можно также указать горячую клавишу клавиатуры, которая автоматически появится в контекстном меню рядом с заголовком.

Git коммит с изменениями

7. Смотри и чувствуй

На последнем шаге мы снова позаботимся о внешнем виде нашего окна, а именно добавим стрелку поверх приложения, чтобы придать ему раскрывающийся вид, и закруглим углы окна.

Для этого нам нужно будет активировать свойство прозрачности и свойство macOSPrivateApi, чтобы оно работало в macOS. Снова откройте tauri.conf.json и добавьте следующие две строки:

...,
"macOSPrivateApi": true,
"windows": [
  {
    ...,
    "transparent": true
  }

Для отображения стрелки воспользуемся одним из примеров из menubar module Electron и добавим (адаптируем) в style.css следующий код:

body {
  margin: 0; 
}

.arrow {
  position: relative;
  padding: 12px 0;
}

.arrow:before {
  content: '';
  height: 0;
  width: 0;
  border-width: 0 8px 12px 8px;
  border-style: solid;
  border-color: transparent transparent #2f2f2f transparent;
  position: absolute;
  top: 0px;
  left: 50%;
  transform: translateX(-50%);
}

@media (prefers-color-scheme: dark) {
  .container {
    border-radius: 7px;
    padding: 10px;
    color: #f6f6f6;
    background-color: #2f2f2f;
  }
}

Наконец, добавьте класс arrow в тег body в index.html. Вот как это выглядит:

...
<body class="arrow">
...
</body>

Git коммит с изменениями

Вот и все. Я буду рад вашим отзывам и надеюсь, что эти несколько шагов помогут вам приступить к созданию приложения menubar с помощью Tauri :-)

💻 Нажмите здесь, чтобы увидеть репозиторий проекта.