use gpui
This commit is contained in:
+87
-78
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user