122 lines
4.7 KiB
Rust
122 lines
4.7 KiB
Rust
//! Иконки на поле редактора (gutter).
|
||
//!
|
||
//! Gutter — узкая колонка слева от редактора. Сценарий ставит на строку
|
||
//! иконку через `api.editor:set_gutter_icon`, а вид рисует её на той же высоте, что и
|
||
//! строка в редакторе, учитывая прокрутку.
|
||
//!
|
||
//! Состояние принадлежит документу: у каждой вкладки свои иконки, и при
|
||
//! переключении вкладок показываются иконки активного документа. Номера строк
|
||
//! нумеруются с нуля и не сдвигаются автоматически при правке текста:
|
||
//! сценарий сам решает, когда переставить иконку.
|
||
|
||
use std::collections::BTreeMap;
|
||
|
||
use gpui_kit::{Hsla, Pixels};
|
||
|
||
/// Иконка одной строки.
|
||
#[derive(Clone, Debug, PartialEq)]
|
||
pub struct GutterMarker {
|
||
/// Каноническое имя иконки Lucide.
|
||
pub icon: String,
|
||
/// Цвет иконки; `None` — цвет текста темы.
|
||
pub color: Option<Hsla>,
|
||
}
|
||
|
||
/// Иконки gutter документа по номеру строки.
|
||
///
|
||
/// Одна строка несёт не больше одной иконки: повторный вызов заменяет её.
|
||
/// `BTreeMap` держит строки отсортированными, поэтому вид рисует их сверху
|
||
/// вниз без дополнительной сортировки.
|
||
#[derive(Clone, Debug, Default, PartialEq)]
|
||
pub struct GutterState {
|
||
markers: BTreeMap<usize, GutterMarker>,
|
||
}
|
||
|
||
impl GutterState {
|
||
/// Создаёт пустое поле.
|
||
pub fn new() -> Self {
|
||
Self::default()
|
||
}
|
||
|
||
/// Ставит иконку на строку `line`, заменяя прежнюю.
|
||
///
|
||
/// Имя иконки проверяет API: здесь хранится уже каноническое имя.
|
||
pub fn set(&mut self, line: usize, icon: impl Into<String>, color: Option<Hsla>) {
|
||
self.markers.insert(
|
||
line,
|
||
GutterMarker {
|
||
icon: icon.into(),
|
||
color,
|
||
},
|
||
);
|
||
}
|
||
|
||
/// Убирает иконку со строки.
|
||
///
|
||
/// Возвращает `false`, если на этой строке иконки не было.
|
||
pub fn remove(&mut self, line: usize) -> bool {
|
||
self.markers.remove(&line).is_some()
|
||
}
|
||
|
||
/// Убирает все иконки.
|
||
pub fn clear(&mut self) {
|
||
self.markers.clear();
|
||
}
|
||
|
||
/// Иконка строки, если она есть.
|
||
pub fn get(&self, line: usize) -> Option<&GutterMarker> {
|
||
self.markers.get(&line)
|
||
}
|
||
|
||
/// Иконки в порядке возрастания номеров строк.
|
||
pub fn iter(&self) -> impl Iterator<Item = (usize, &GutterMarker)> {
|
||
self.markers.iter().map(|(line, marker)| (*line, marker))
|
||
}
|
||
}
|
||
|
||
/// Вертикальный отступ видимой строки `row` от верхнего края gutter.
|
||
///
|
||
/// `scroll_y` — смещение прокрутки редактора (отрицательное при прокрутке
|
||
/// вниз), `line_height` — высота строки из раскладки редактора. `row` — не
|
||
/// номер строки буфера, а номер видимой строки: свёрнутые строки не занимают
|
||
/// места.
|
||
pub fn row_offset(row: usize, line_height: Pixels, scroll_y: Pixels) -> Pixels {
|
||
scroll_y + line_height * row as f32
|
||
}
|
||
|
||
#[cfg(test)]
|
||
mod tests {
|
||
use super::*;
|
||
use gpui_kit::{hsla, px};
|
||
|
||
#[test]
|
||
fn markers_are_keyed_by_line_and_sorted() {
|
||
let mut gutter = GutterState::new();
|
||
assert_eq!(gutter.iter().count(), 0);
|
||
|
||
gutter.set(5, "file", None);
|
||
gutter.set(1, "play", Some(hsla(0.5, 1., 0.5, 1.)));
|
||
let lines: Vec<usize> = gutter.iter().map(|(line, _)| line).collect();
|
||
assert_eq!(lines, [1, 5]);
|
||
assert_eq!(
|
||
gutter.get(1).map(|marker| marker.icon.as_str()),
|
||
Some("play")
|
||
);
|
||
|
||
assert!(gutter.remove(1));
|
||
assert!(!gutter.remove(1));
|
||
assert!(gutter.get(1).is_none());
|
||
assert_eq!(gutter.iter().count(), 1);
|
||
|
||
gutter.clear();
|
||
assert_eq!(gutter.iter().count(), 0);
|
||
}
|
||
|
||
#[test]
|
||
fn row_offset_follows_scroll_and_line_height() {
|
||
assert_eq!(row_offset(0, px(20.), px(0.)), px(0.));
|
||
assert_eq!(row_offset(3, px(20.), px(-40.)), px(20.));
|
||
assert_eq!(row_offset(1, px(18.), px(-9.)), px(9.));
|
||
}
|
||
}
|