commit
This commit is contained in:
+310
@@ -0,0 +1,310 @@
|
||||
//! Глобальный API приложения.
|
||||
//!
|
||||
//! Единственная точка входа для команд управления окном и редактором: UI-код
|
||||
//! не работает с `ViewportCommand` и состоянием редактора напрямую, а вызывает
|
||||
//! методы [`AppApi`].
|
||||
|
||||
use std::cell::RefCell;
|
||||
use std::rc::Rc;
|
||||
|
||||
use eframe::egui;
|
||||
|
||||
use crate::editor::EditorState;
|
||||
use crate::keymap::{KeyBinding, KeyMap};
|
||||
use crate::syntax::Language;
|
||||
|
||||
thread_local! {
|
||||
/// Текущий экземпляр API. `thread_local`, потому что `egui::Context` привязан
|
||||
/// к потоку с event loop; заодно это делает тесты независимыми друг от друга.
|
||||
static INSTANCE: RefCell<Option<AppApi>> = const { RefCell::new(None) };
|
||||
}
|
||||
|
||||
/// Краткая информация об открытой вкладке.
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct TabInfo {
|
||||
/// Номер вкладки (с нуля), слева направо.
|
||||
pub index: usize,
|
||||
/// Название — то же, что показано в заголовке вкладки.
|
||||
pub title: String,
|
||||
}
|
||||
|
||||
/// Глобальный объект с функциями управления приложением.
|
||||
///
|
||||
/// Использование: `AppApi::global().close()`, `AppApi::global().tabs()`.
|
||||
/// Экземпляр устанавливается в начале каждого кадра (см. [`AppApi::install`]),
|
||||
/// поэтому всегда указывает на актуальные `egui::Context` и состояние редактора.
|
||||
///
|
||||
/// Методы этого `impl`-блока автоматически экспортируются в Lua через
|
||||
/// `#[mlua::userdata_impl]`: в сценариях они доступны как `app:method(...)`.
|
||||
/// Чтобы не экспортировать метод, пометьте его `#[lua(skip)]`.
|
||||
#[derive(Clone, mlua::UserData)]
|
||||
pub struct AppApi {
|
||||
#[lua(skip)]
|
||||
ctx: egui::Context,
|
||||
#[lua(skip)]
|
||||
editor: Rc<RefCell<EditorState>>,
|
||||
#[lua(skip)]
|
||||
keymap: Rc<RefCell<KeyMap>>,
|
||||
}
|
||||
|
||||
#[mlua::userdata_impl]
|
||||
impl AppApi {
|
||||
/// Делает переданные контекст, состояние редактора и карту клавиш текущими
|
||||
/// для [`AppApi::global`].
|
||||
#[lua(skip)]
|
||||
pub fn install(
|
||||
ctx: egui::Context,
|
||||
editor: Rc<RefCell<EditorState>>,
|
||||
keymap: Rc<RefCell<KeyMap>>,
|
||||
) -> Self {
|
||||
let api = Self {
|
||||
ctx,
|
||||
editor,
|
||||
keymap,
|
||||
};
|
||||
INSTANCE.with(|slot| *slot.borrow_mut() = Some(api.clone()));
|
||||
api
|
||||
}
|
||||
|
||||
/// Глобальный экземпляр API.
|
||||
///
|
||||
/// # Panics
|
||||
/// Если [`AppApi::install`] ещё не вызывался (ни одного кадра не отрисовано).
|
||||
#[lua(skip)]
|
||||
pub fn global() -> Self {
|
||||
INSTANCE.with(|slot| {
|
||||
slot.borrow()
|
||||
.clone()
|
||||
.expect("AppApi не инициализирован: сначала вызовите AppApi::install")
|
||||
})
|
||||
}
|
||||
|
||||
/// Контекст egui, к которому привязан API.
|
||||
#[lua(skip)]
|
||||
pub fn context(&self) -> &egui::Context {
|
||||
&self.ctx
|
||||
}
|
||||
|
||||
/// Все открытые вкладки в порядке слева направо.
|
||||
#[allow(dead_code)]
|
||||
// Часть публичного API приложения: вызывается внешним кодом и тестами.
|
||||
#[lua(infallible)]
|
||||
pub fn tabs(&self) -> Vec<TabInfo> {
|
||||
self.editor
|
||||
.borrow()
|
||||
.documents()
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(index, doc)| TabInfo {
|
||||
index,
|
||||
title: doc.name.clone(),
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Индекс активной вкладки в списке [`AppApi::tabs`].
|
||||
#[allow(dead_code)]
|
||||
// Часть публичного API приложения: вызывается внешним кодом и тестами.
|
||||
#[lua(infallible, name = "active_tab")]
|
||||
pub fn active_tab_index(&self) -> usize {
|
||||
self.editor.borrow().active_index()
|
||||
}
|
||||
|
||||
/// Переключает на следующую вкладку (по кругу) и возвращает её индекс.
|
||||
#[allow(dead_code)]
|
||||
// Часть публичного API приложения: вызывается внешним кодом и тестами.
|
||||
#[lua(infallible)]
|
||||
pub fn next_tab(&self) -> usize {
|
||||
let mut editor = self.editor.borrow_mut();
|
||||
let next = (editor.active_index() + 1) % editor.documents().len();
|
||||
editor.set_active(next);
|
||||
next
|
||||
}
|
||||
|
||||
/// Переключает на предыдущую вкладку (по кругу) и возвращает её индекс.
|
||||
#[allow(dead_code)]
|
||||
// Часть публичного API приложения: вызывается внешним кодом и тестами.
|
||||
#[lua(infallible)]
|
||||
pub fn previous_tab(&self) -> usize {
|
||||
let mut editor = self.editor.borrow_mut();
|
||||
let count = editor.documents().len();
|
||||
let previous = (editor.active_index() + count - 1) % count;
|
||||
editor.set_active(previous);
|
||||
previous
|
||||
}
|
||||
|
||||
/// Закрывает вкладку по её номеру в списке [`AppApi::tabs`].
|
||||
///
|
||||
/// Возвращает `false`, если вкладки с таким номером нет. Если закрывается
|
||||
/// последняя вкладка, вместо неё открывается пустой документ.
|
||||
#[lua(infallible)]
|
||||
pub fn close_tab(&self, index: usize) -> bool {
|
||||
let mut editor = self.editor.borrow_mut();
|
||||
if index >= editor.documents().len() {
|
||||
return false;
|
||||
}
|
||||
editor.close(index);
|
||||
true
|
||||
}
|
||||
|
||||
/// Сохраняет содержимое активной вкладки в файл, названный как сама вкладка.
|
||||
///
|
||||
/// Путь считается относительно текущего рабочего каталога процесса.
|
||||
/// При успешной записи с вкладки снимается маркер несохранённых изменений.
|
||||
#[lua(skip)]
|
||||
pub fn save(&self) -> std::io::Result<()> {
|
||||
let mut editor = self.editor.borrow_mut();
|
||||
let doc = editor.active_mut();
|
||||
std::fs::write(&doc.name, &doc.content)?;
|
||||
doc.mark_saved();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Lua-версия [`AppApi::save`]: ошибка ввода-вывода становится ошибкой Lua.
|
||||
#[lua(name = "save")]
|
||||
fn lua_save(&self) -> mlua::Result<()> {
|
||||
self.save()
|
||||
.map_err(|err| mlua::Error::RuntimeError(err.to_string()))
|
||||
}
|
||||
|
||||
/// Назначает обработчик на нажатие или отпускание клавиши.
|
||||
///
|
||||
/// `keys` записывается в духе nvim:
|
||||
///
|
||||
/// * `a`, `1` — нажатие клавиши без модификаторов;
|
||||
/// * `<S-c>`, `<C-z>`, `<A-r>` — нажатие с Shift, Ctrl или Alt;
|
||||
/// * `!a` — отпускание клавиши;
|
||||
/// * `<A-!r>` — отпускание клавиши при зажатом Alt.
|
||||
///
|
||||
/// Клавиша определяется по физическому коду, поэтому Caps Lock и раскладка
|
||||
/// не важны. Повторное назначение той же клавиши заменяет обработчик.
|
||||
///
|
||||
/// # Errors
|
||||
/// Возвращает ошибку Lua, если запись привязки некорректна.
|
||||
pub fn map_key(&self, keys: String, callback: mlua::Function) -> mlua::Result<()> {
|
||||
let binding = KeyBinding::parse(&keys).map_err(mlua::Error::RuntimeError)?;
|
||||
self.keymap.borrow_mut().bind(binding, callback);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Содержимое активной вкладки.
|
||||
#[lua(infallible)]
|
||||
pub fn active_text(&self) -> String {
|
||||
self.editor.borrow().active().content.clone()
|
||||
}
|
||||
|
||||
/// Заменяет содержимое активной вкладки.
|
||||
#[lua(skip)]
|
||||
pub fn set_active_text(&self, text: impl Into<String>) {
|
||||
self.editor.borrow_mut().active_mut().content = text.into();
|
||||
}
|
||||
|
||||
/// Lua-версия [`AppApi::set_active_text`] (generic-методы не экспортируются).
|
||||
#[lua(infallible, name = "set_active_text")]
|
||||
fn lua_set_active_text(&self, text: String) {
|
||||
self.set_active_text(text);
|
||||
}
|
||||
|
||||
/// Открывает новую пустую вкладку, делает её активной и возвращает её индекс.
|
||||
#[lua(infallible)]
|
||||
pub fn open_tab(&self) -> usize {
|
||||
let mut editor = self.editor.borrow_mut();
|
||||
editor.open_new();
|
||||
editor.active_index()
|
||||
}
|
||||
|
||||
/// Делает вкладку активной по её номеру в списке [`AppApi::tabs`].
|
||||
///
|
||||
/// Возвращает `false`, если вкладки с таким номером нет.
|
||||
#[lua(infallible)]
|
||||
pub fn set_active_tab(&self, index: usize) -> bool {
|
||||
let mut editor = self.editor.borrow_mut();
|
||||
if index >= editor.documents().len() {
|
||||
return false;
|
||||
}
|
||||
editor.set_active(index);
|
||||
true
|
||||
}
|
||||
|
||||
/// Включает подсветку синтаксиса для вкладки `index`.
|
||||
///
|
||||
/// `language` — название языка: `rust`, `go`, `js`, `json` или `python`
|
||||
/// (принимаются также `rs`, `golang`, `javascript`, `py`; регистр не важен).
|
||||
/// Подсветка по умолчанию выключена и включается только этим методом.
|
||||
///
|
||||
/// Возвращает `false`, если вкладки нет или язык не поддерживается.
|
||||
#[lua(skip)]
|
||||
pub fn set_language(&self, index: usize, language: &str) -> bool {
|
||||
let Some(language) = Language::from_name(language) else {
|
||||
return false;
|
||||
};
|
||||
let mut editor = self.editor.borrow_mut();
|
||||
let Some(doc) = editor.document_mut(index) else {
|
||||
return false;
|
||||
};
|
||||
doc.language = Some(language);
|
||||
true
|
||||
}
|
||||
|
||||
/// Lua-версия [`AppApi::set_language`].
|
||||
#[lua(infallible, name = "set_language")]
|
||||
fn lua_set_language(&self, index: usize, language: String) -> bool {
|
||||
self.set_language(index, &language)
|
||||
}
|
||||
|
||||
/// Язык подсветки вкладки: `"rust"`, `"go"`, `"js"`, `"json"`, `"python"`
|
||||
/// или `nil`, если подсветка выключена / вкладки нет.
|
||||
#[lua(infallible)]
|
||||
pub fn language(&self, index: usize) -> Option<String> {
|
||||
self.editor
|
||||
.borrow()
|
||||
.document(index)
|
||||
.and_then(|doc| doc.language)
|
||||
.map(|language| language.name().to_owned())
|
||||
}
|
||||
|
||||
/// Выключает подсветку синтаксиса для вкладки.
|
||||
///
|
||||
/// Возвращает `false`, если вкладки с таким номером нет.
|
||||
#[lua(infallible)]
|
||||
pub fn clear_language(&self, index: usize) -> bool {
|
||||
let mut editor = self.editor.borrow_mut();
|
||||
let Some(doc) = editor.document_mut(index) else {
|
||||
return false;
|
||||
};
|
||||
doc.language = None;
|
||||
true
|
||||
}
|
||||
|
||||
/// Закрывает окно приложения.
|
||||
#[lua(infallible)]
|
||||
pub fn close(&self) {
|
||||
self.ctx.send_viewport_cmd(egui::ViewportCommand::Close);
|
||||
}
|
||||
|
||||
/// Разворачивает окно на весь экран или возвращает его прежний размер.
|
||||
#[lua(infallible)]
|
||||
pub fn toggle_maximize(&self) {
|
||||
self.set_maximized(!self.is_maximized());
|
||||
}
|
||||
|
||||
/// Разворачивает (`true`) или восстанавливает (`false`) окно.
|
||||
#[lua(infallible)]
|
||||
pub fn set_maximized(&self, maximized: bool) {
|
||||
self.ctx
|
||||
.send_viewport_cmd(egui::ViewportCommand::Maximized(maximized));
|
||||
}
|
||||
|
||||
/// Сворачивает окно.
|
||||
#[lua(infallible)]
|
||||
pub fn minimize(&self) {
|
||||
self.ctx
|
||||
.send_viewport_cmd(egui::ViewportCommand::Minimized(true));
|
||||
}
|
||||
|
||||
/// Развёрнуто ли окно прямо сейчас.
|
||||
#[lua(infallible)]
|
||||
pub fn is_maximized(&self) -> bool {
|
||||
self.ctx.input(|i| i.viewport().maximized.unwrap_or(false))
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user