toolbar
This commit is contained in:
+202
@@ -18,6 +18,8 @@ use std::time::Duration;
|
||||
use mlua::IntoLua;
|
||||
use serde_json::Number;
|
||||
|
||||
use gpui_kit::{Hsla, Rgba};
|
||||
|
||||
use crate::constants::{BOTTOM_PANEL_HEIGHT, LEFT_PANEL_WIDTH, RIGHT_PANEL_WIDTH};
|
||||
use crate::editor::{FocusAfterSync, Language, Workspace};
|
||||
use crate::icons;
|
||||
@@ -26,6 +28,7 @@ use crate::lsp::{
|
||||
DocumentDiagnostics, LspConfig, LspManager, WatcherConfig, document_uri, position_to_offset,
|
||||
};
|
||||
use crate::panel::{PanelHandle, PanelState};
|
||||
use crate::toolbar::{ToolbarHandle, ToolbarItem, ToolbarKind, ToolbarSide, ToolbarState};
|
||||
use crate::tree::{TreeHandle, TreeNode};
|
||||
|
||||
thread_local! {
|
||||
@@ -124,6 +127,8 @@ pub struct AppApi {
|
||||
#[lua(skip)]
|
||||
bottom_panel: PanelHandle,
|
||||
#[lua(skip)]
|
||||
toolbar: ToolbarHandle,
|
||||
#[lua(skip)]
|
||||
lsp: Arc<LspManager>,
|
||||
#[lua(skip)]
|
||||
diagnostics_handler: Rc<RefCell<Option<DiagnosticsHandler>>>,
|
||||
@@ -147,6 +152,23 @@ impl PanelApi {
|
||||
}
|
||||
}
|
||||
|
||||
/// API верхней панели редактора.
|
||||
///
|
||||
/// В Lua доступно как `app.toolbar`. Элементы добавляются в левую или правую
|
||||
/// область панели и опознаются по `id`, чтобы их можно было скрыть и снова
|
||||
/// показать.
|
||||
#[derive(mlua::UserData)]
|
||||
pub struct ToolbarApi {
|
||||
#[lua(skip)]
|
||||
state: ToolbarHandle,
|
||||
}
|
||||
|
||||
impl ToolbarApi {
|
||||
fn new(state: ToolbarHandle) -> Self {
|
||||
Self { state }
|
||||
}
|
||||
}
|
||||
|
||||
/// API одного блока дерева.
|
||||
///
|
||||
/// Объект возвращает `app.left_panel:add_tree()`. Редактор не знает, что
|
||||
@@ -287,6 +309,86 @@ impl PanelApi {
|
||||
}
|
||||
}
|
||||
|
||||
#[mlua::userdata_impl]
|
||||
impl ToolbarApi {
|
||||
/// Добавляет текстовый лейбл в левую или правую область панели.
|
||||
///
|
||||
/// `text` — отображаемый текст;
|
||||
/// `icon` — имя иконки Lucide или `nil`;
|
||||
/// `background` — цвет фона в виде `#RRGGBB`, `#RRGGBBAA` или `nil`;
|
||||
/// `id` — идентификатор для `hide`/`show`;
|
||||
/// `side` — `"left"` или `"right"`.
|
||||
///
|
||||
/// Элемент с уже существующим `id` заменяется.
|
||||
pub fn add_label(
|
||||
&self,
|
||||
text: String,
|
||||
icon: Option<String>,
|
||||
background: Option<String>,
|
||||
id: String,
|
||||
side: String,
|
||||
) -> mlua::Result<()> {
|
||||
let id = parse_toolbar_id(id)?;
|
||||
let side = parse_toolbar_side(&side)?;
|
||||
let icon = parse_toolbar_icon(icon)?;
|
||||
let background = parse_toolbar_color(background)?;
|
||||
self.state.borrow_mut().push(
|
||||
side,
|
||||
ToolbarItem::new(id, text, icon, background, ToolbarKind::Label),
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Добавляет кнопку в левую или правую область панели.
|
||||
///
|
||||
/// Параметры совпадают с [`ToolbarApi::add_label`]; дополнительный
|
||||
/// `callback` вызывается при нажатии без аргументов.
|
||||
///
|
||||
/// `background = nil` оставляет стандартный фон кнопки.
|
||||
pub fn add_button(
|
||||
&self,
|
||||
text: String,
|
||||
icon: Option<String>,
|
||||
background: Option<String>,
|
||||
id: String,
|
||||
side: String,
|
||||
callback: mlua::Function,
|
||||
) -> mlua::Result<()> {
|
||||
let id = parse_toolbar_id(id)?;
|
||||
let side = parse_toolbar_side(&side)?;
|
||||
let icon = parse_toolbar_icon(icon)?;
|
||||
let background = parse_toolbar_color(background)?;
|
||||
self.state.borrow_mut().push(
|
||||
side,
|
||||
ToolbarItem::new(id, text, icon, background, ToolbarKind::Button { callback }),
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Скрывает элемент с идентификатором `id`.
|
||||
///
|
||||
/// Возвращает `false`, если элемента с таким `id` нет. Элемент остаётся
|
||||
/// в модели и снова показывается через [`ToolbarApi::show`].
|
||||
#[lua(infallible)]
|
||||
pub fn hide(&self, id: String) -> bool {
|
||||
self.state.borrow_mut().hide(&id)
|
||||
}
|
||||
|
||||
/// Снова показывает скрытый элемент с идентификатором `id`.
|
||||
///
|
||||
/// Возвращает `false`, если элемента с таким `id` нет.
|
||||
#[lua(infallible)]
|
||||
pub fn show(&self, id: String) -> bool {
|
||||
self.state.borrow_mut().show(&id)
|
||||
}
|
||||
|
||||
/// Удаляет все элементы верхней панели.
|
||||
#[lua(infallible)]
|
||||
pub fn clear(&self) {
|
||||
self.state.borrow_mut().clear();
|
||||
}
|
||||
}
|
||||
|
||||
#[mlua::userdata_impl]
|
||||
impl AppApi {
|
||||
/// Делает переданные документы и карту клавиш текущими для
|
||||
@@ -299,6 +401,7 @@ impl AppApi {
|
||||
left_panel: Rc::new(RefCell::new(PanelState::new(LEFT_PANEL_WIDTH))),
|
||||
right_panel: Rc::new(RefCell::new(PanelState::new(RIGHT_PANEL_WIDTH))),
|
||||
bottom_panel: Rc::new(RefCell::new(PanelState::new(BOTTOM_PANEL_HEIGHT))),
|
||||
toolbar: Rc::new(RefCell::new(ToolbarState::new())),
|
||||
lsp: Arc::new(LspManager::new()),
|
||||
diagnostics_handler: Rc::new(RefCell::new(None)),
|
||||
};
|
||||
@@ -337,6 +440,12 @@ impl AppApi {
|
||||
self.bottom_panel.clone()
|
||||
}
|
||||
|
||||
/// Состояние верхней панели (для вида).
|
||||
#[lua(skip)]
|
||||
pub fn toolbar_handle(&self) -> ToolbarHandle {
|
||||
self.toolbar.clone()
|
||||
}
|
||||
|
||||
/// Менеджер LSP-серверов (для вида и тестов).
|
||||
#[lua(skip)]
|
||||
pub fn lsp_handle(&self) -> Arc<LspManager> {
|
||||
@@ -751,6 +860,16 @@ impl AppApi {
|
||||
PanelApi::new(self.bottom_panel.clone())
|
||||
}
|
||||
|
||||
/// Верхняя панель редактора.
|
||||
///
|
||||
/// В Lua доступна как `app.toolbar` с методами `add_label`, `add_button`,
|
||||
/// `hide`, `show` и `clear`.
|
||||
#[allow(dead_code)]
|
||||
#[lua(getter, infallible)]
|
||||
pub fn toolbar(&self) -> ToolbarApi {
|
||||
ToolbarApi::new(self.toolbar.clone())
|
||||
}
|
||||
|
||||
/// Закрывает окно приложения.
|
||||
#[lua(infallible)]
|
||||
pub fn close(&self) {
|
||||
@@ -1070,6 +1189,89 @@ fn parse_key_handler(
|
||||
}
|
||||
}
|
||||
|
||||
/// Проверяет идентификатор элемента верхней панели.
|
||||
fn parse_toolbar_id(id: String) -> mlua::Result<String> {
|
||||
if id.trim().is_empty() {
|
||||
return Err(mlua::Error::RuntimeError(
|
||||
"id элемента верхней панели не может быть пустым".to_owned(),
|
||||
));
|
||||
}
|
||||
Ok(id)
|
||||
}
|
||||
|
||||
/// Разбирает сторону верхней панели: `left` или `right`.
|
||||
fn parse_toolbar_side(side: &str) -> mlua::Result<ToolbarSide> {
|
||||
ToolbarSide::parse(side).ok_or_else(|| {
|
||||
mlua::Error::RuntimeError(format!(
|
||||
"неизвестная сторона панели: {side}; ожидается \"left\" или \"right\""
|
||||
))
|
||||
})
|
||||
}
|
||||
|
||||
/// Проверяет имя иконки по каталогу Lucide.
|
||||
///
|
||||
/// `nil` и пустая строка означают «без иконки»; неизвестное имя — ошибка,
|
||||
/// чтобы опечатка в сценарии не пропадала молча.
|
||||
fn parse_toolbar_icon(icon: Option<String>) -> mlua::Result<Option<String>> {
|
||||
let Some(name) = icon else {
|
||||
return Ok(None);
|
||||
};
|
||||
if name.trim().is_empty() {
|
||||
return Ok(None);
|
||||
}
|
||||
icons::canonical_name(&name)
|
||||
.map(|canonical| Some(canonical.to_owned()))
|
||||
.ok_or_else(|| mlua::Error::RuntimeError(format!("неизвестная иконка: {name}")))
|
||||
}
|
||||
|
||||
/// Разбирает цвет фона элемента верхней панели.
|
||||
///
|
||||
/// Поддерживаются `#RGB`, `#RGBA`, `#RRGGBB` и `#RRGGBBAA`; `nil`,
|
||||
/// пустая строка и `transparent` означают прозрачный фон.
|
||||
fn parse_toolbar_color(color: Option<String>) -> mlua::Result<Option<Hsla>> {
|
||||
let Some(color) = color else {
|
||||
return Ok(None);
|
||||
};
|
||||
let color = color.trim();
|
||||
if color.is_empty() || color.eq_ignore_ascii_case("transparent") {
|
||||
return Ok(None);
|
||||
}
|
||||
parse_hex_color(color).map(Some).ok_or_else(|| {
|
||||
mlua::Error::RuntimeError(format!(
|
||||
"некорректный цвет фона: {color}; ожидается #RGB, #RGBA, #RRGGBB или #RRGGBBAA"
|
||||
))
|
||||
})
|
||||
}
|
||||
|
||||
/// Переводит шестнадцатеричный цвет в `Hsla`.
|
||||
fn parse_hex_color(value: &str) -> Option<Hsla> {
|
||||
let hex = value.strip_prefix('#').unwrap_or(value);
|
||||
if !hex.chars().all(|ch| ch.is_ascii_hexdigit()) {
|
||||
return None;
|
||||
}
|
||||
|
||||
let nibble = |index: usize| {
|
||||
u8::from_str_radix(&hex[index..index + 1], 16)
|
||||
.ok()
|
||||
.map(|value| value * 17)
|
||||
};
|
||||
let byte = |index: usize| u8::from_str_radix(&hex[index..index + 2], 16).ok();
|
||||
let (r, g, b, a) = match hex.len() {
|
||||
3 => (nibble(0)?, nibble(1)?, nibble(2)?, 255),
|
||||
4 => (nibble(0)?, nibble(1)?, nibble(2)?, nibble(3)?),
|
||||
6 => (byte(0)?, byte(2)?, byte(4)?, 255),
|
||||
8 => (byte(0)?, byte(2)?, byte(4)?, byte(6)?),
|
||||
_ => return None,
|
||||
};
|
||||
|
||||
Some(Hsla::from(Rgba {
|
||||
r: f32::from(r) / 255.0,
|
||||
g: f32::from(g) / 255.0,
|
||||
b: f32::from(b) / 255.0,
|
||||
a: f32::from(a) / 255.0,
|
||||
}))
|
||||
}
|
||||
|
||||
/// Разбирает массив узлов дерева, переданный в `TreeApi::append`.
|
||||
fn parse_tree_nodes(nodes: &mlua::Table) -> mlua::Result<Vec<TreeNode>> {
|
||||
let mut parsed = Vec::new();
|
||||
|
||||
Reference in New Issue
Block a user