This commit is contained in:
2026-10-30 00:31:49 +03:00
parent dce595fbc0
commit e00f63e5da
17 changed files with 6479 additions and 3200 deletions
+87 -78
View File
@@ -1,22 +1,57 @@
//! Глобальный API приложения.
//!
//! Единственная точка входа для команд управления окном и редактором: UI-код
//! не работает с `ViewportCommand` и состоянием редактора напрямую, а вызывает
//! и Lua-сценарии не трогают `Window` и состояние вида напрямую, а вызывают
//! методы [`AppApi`].
//!
//! Команды окна не выполняются в момент вызова: Lua-обработчик выполняется вне
//! контекста `App`/`Window`, поэтому [`AppApi`] складывает их в очередь, а вид
//! вычитывает её в начале кадра.
use std::cell::RefCell;
use std::cell::{Cell, RefCell};
use std::collections::VecDeque;
use std::rc::Rc;
use eframe::egui;
use crate::editor::EditorState;
use crate::editor::{Language, Workspace};
use crate::keymap::{KeyBinding, KeyMap};
use crate::syntax::Language;
thread_local! {
/// Текущий экземпляр API. `thread_local`, потому что `egui::Context` привязан
/// к потоку с event loop; заодно это делает тесты независимыми друг от друга.
/// Текущий экземпляр API. `thread_local`, потому что UI GPUI привязан к
/// потоку с event loop; заодно это делает тесты независимыми друг от друга.
static INSTANCE: RefCell<Option<AppApi>> = const { RefCell::new(None) };
/// Команды окна, ожидающие ближайшего кадра.
static WINDOW_COMMANDS: RefCell<VecDeque<WindowCommand>> = const { RefCell::new(VecDeque::new()) };
/// Развёрнуто ли окно. Вид обновляет значение в каждом кадре, чтобы
/// `app:is_maximized()` из Lua видел актуальное состояние.
static WINDOW_MAXIMIZED: Cell<bool> = const { Cell::new(false) };
}
/// Действие над окном, отложенное до ближайшего кадра.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum WindowCommand {
Close,
Minimize,
ToggleMaximize,
SetMaximized(bool),
}
/// Запоминает, развёрнуто ли окно сейчас.
pub fn set_window_maximized(maximized: bool) {
WINDOW_MAXIMIZED.with(|state| state.set(maximized));
}
/// Забирает накопленные команды окна.
///
/// Очередь очищается: выполнять команды нужно ровно один раз.
pub fn drain_window_commands() -> Vec<WindowCommand> {
WINDOW_COMMANDS.with(|queue| queue.borrow_mut().drain(..).collect())
}
/// Кладёт команду в очередь.
fn push_window_command(command: WindowCommand) {
WINDOW_COMMANDS.with(|queue| queue.borrow_mut().push_back(command));
}
/// Краткая информация об открытой вкладке.
@@ -31,8 +66,8 @@ pub struct TabInfo {
/// Глобальный объект с функциями управления приложением.
///
/// Использование: `AppApi::global().close()`, `AppApi::global().tabs()`.
/// Экземпляр устанавливается в начале каждого кадра (см. [`AppApi::install`]),
/// поэтому всегда указывает на актуальные `egui::Context` и состояние редактора.
/// Экземпляр устанавливается один раз при создании приложения (см.
/// [`AppApi::install`]) и указывает на общие с видом документы и карту клавиш.
///
/// Методы этого `impl`-блока автоматически экспортируются в Lua через
/// `#[mlua::userdata_impl]`: в сценариях они доступны как `app:method(...)`.
@@ -40,28 +75,18 @@ pub struct TabInfo {
#[derive(Clone, mlua::UserData)]
pub struct AppApi {
#[lua(skip)]
ctx: egui::Context,
#[lua(skip)]
editor: Rc<RefCell<EditorState>>,
workspace: Rc<RefCell<Workspace>>,
#[lua(skip)]
keymap: Rc<RefCell<KeyMap>>,
}
#[mlua::userdata_impl]
impl AppApi {
/// Делает переданные контекст, состояние редактора и карту клавиш текущими
/// для [`AppApi::global`].
/// Делает переданные документы и карту клавиш текущими для
/// [`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,
};
pub fn install(workspace: Rc<RefCell<Workspace>>, keymap: Rc<RefCell<KeyMap>>) -> Self {
let api = Self { workspace, keymap };
INSTANCE.with(|slot| *slot.borrow_mut() = Some(api.clone()));
api
}
@@ -69,7 +94,7 @@ impl AppApi {
/// Глобальный экземпляр API.
///
/// # Panics
/// Если [`AppApi::install`] ещё не вызывался (ни одного кадра не отрисовано).
/// Если [`AppApi::install`] ещё не вызывался.
#[lua(skip)]
pub fn global() -> Self {
INSTANCE.with(|slot| {
@@ -79,18 +104,12 @@ impl AppApi {
})
}
/// Контекст 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
self.workspace
.borrow()
.documents()
.iter()
@@ -107,7 +126,7 @@ impl AppApi {
// Часть публичного API приложения: вызывается внешним кодом и тестами.
#[lua(infallible, name = "active_tab")]
pub fn active_tab_index(&self) -> usize {
self.editor.borrow().active_index()
self.workspace.borrow().active_index()
}
/// Переключает на следующую вкладку (по кругу) и возвращает её индекс.
@@ -115,9 +134,9 @@ impl AppApi {
// Часть публичного 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);
let mut workspace = self.workspace.borrow_mut();
let next = (workspace.active_index() + 1) % workspace.documents().len();
workspace.set_active(next);
next
}
@@ -126,10 +145,10 @@ impl AppApi {
// Часть публичного 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);
let mut workspace = self.workspace.borrow_mut();
let count = workspace.documents().len();
let previous = (workspace.active_index() + count - 1) % count;
workspace.set_active(previous);
previous
}
@@ -139,11 +158,11 @@ impl AppApi {
/// последняя вкладка, вместо неё открывается пустой документ.
#[lua(infallible)]
pub fn close_tab(&self, index: usize) -> bool {
let mut editor = self.editor.borrow_mut();
if index >= editor.documents().len() {
let mut workspace = self.workspace.borrow_mut();
if index >= workspace.documents().len() {
return false;
}
editor.close(index);
workspace.close(index);
true
}
@@ -153,10 +172,13 @@ impl AppApi {
/// При успешной записи с вкладки снимается маркер несохранённых изменений.
#[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();
let (path, content) = {
let workspace = self.workspace.borrow();
let doc = workspace.active();
(doc.name.clone(), doc.content.clone())
};
std::fs::write(&path, content)?;
self.workspace.borrow_mut().mark_active_saved();
Ok(())
}
@@ -176,8 +198,7 @@ impl AppApi {
/// * `!a` — отпускание клавиши;
/// * `<A-!r>` — отпускание клавиши при зажатом Alt.
///
/// Клавиша определяется по физическому коду, поэтому Caps Lock и раскладка
/// не важны. Повторное назначение той же клавиши заменяет обработчик.
/// Повторное назначение той же клавиши заменяет обработчик.
///
/// # Errors
/// Возвращает ошибку Lua, если запись привязки некорректна.
@@ -190,13 +211,13 @@ impl AppApi {
/// Содержимое активной вкладки.
#[lua(infallible)]
pub fn active_text(&self) -> String {
self.editor.borrow().active().content.clone()
self.workspace.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();
self.workspace.borrow_mut().set_active_text(text);
}
/// Lua-версия [`AppApi::set_active_text`] (generic-методы не экспортируются).
@@ -208,9 +229,9 @@ impl AppApi {
/// Открывает новую пустую вкладку, делает её активной и возвращает её индекс.
#[lua(infallible)]
pub fn open_tab(&self) -> usize {
let mut editor = self.editor.borrow_mut();
editor.open_new();
editor.active_index()
let mut workspace = self.workspace.borrow_mut();
workspace.open_new();
workspace.active_index()
}
/// Делает вкладку активной по её номеру в списке [`AppApi::tabs`].
@@ -218,11 +239,11 @@ impl AppApi {
/// Возвращает `false`, если вкладки с таким номером нет.
#[lua(infallible)]
pub fn set_active_tab(&self, index: usize) -> bool {
let mut editor = self.editor.borrow_mut();
if index >= editor.documents().len() {
let mut workspace = self.workspace.borrow_mut();
if index >= workspace.documents().len() {
return false;
}
editor.set_active(index);
workspace.set_active(index);
true
}
@@ -238,12 +259,7 @@ impl AppApi {
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
self.workspace.borrow_mut().set_language(index, language)
}
/// Lua-версия [`AppApi::set_language`].
@@ -256,7 +272,7 @@ impl AppApi {
/// или `nil`, если подсветка выключена / вкладки нет.
#[lua(infallible)]
pub fn language(&self, index: usize) -> Option<String> {
self.editor
self.workspace
.borrow()
.document(index)
.and_then(|doc| doc.language)
@@ -268,43 +284,36 @@ impl AppApi {
/// Возвращает `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
self.workspace.borrow_mut().clear_language(index)
}
/// Закрывает окно приложения.
#[lua(infallible)]
pub fn close(&self) {
self.ctx.send_viewport_cmd(egui::ViewportCommand::Close);
push_window_command(WindowCommand::Close);
}
/// Разворачивает окно на весь экран или возвращает его прежний размер.
#[lua(infallible)]
pub fn toggle_maximize(&self) {
self.set_maximized(!self.is_maximized());
push_window_command(WindowCommand::ToggleMaximize);
}
/// Разворачивает (`true`) или восстанавливает (`false`) окно.
#[lua(infallible)]
pub fn set_maximized(&self, maximized: bool) {
self.ctx
.send_viewport_cmd(egui::ViewportCommand::Maximized(maximized));
push_window_command(WindowCommand::SetMaximized(maximized));
}
/// Сворачивает окно.
#[lua(infallible)]
pub fn minimize(&self) {
self.ctx
.send_viewport_cmd(egui::ViewportCommand::Minimized(true));
push_window_command(WindowCommand::Minimize);
}
/// Развёрнуто ли окно прямо сейчас.
#[lua(infallible)]
pub fn is_maximized(&self) -> bool {
self.ctx.input(|i| i.viewport().maximized.unwrap_or(false))
WINDOW_MAXIMIZED.with(Cell::get)
}
}