files
This commit is contained in:
+684
@@ -0,0 +1,684 @@
|
||||
//! Абстрактное дерево для блоков панелей.
|
||||
//!
|
||||
//! Редактор ничего не знает о файловой системе: он хранит узлы дерева с
|
||||
//! идентификатором, подписью, иконкой и флагом «раскрываемый», рисует их и
|
||||
//! сообщает Lua о раскрытии и кликах. Какой именно смысл у узлов — файлы,
|
||||
//! задачи, настройки — решает сценарий, который наполняет дерево через
|
||||
//! [`TreeApi`](crate::api::TreeApi).
|
||||
//!
|
||||
//! Дерево хранится плоско: у каждого узла есть родитель и список детей.
|
||||
//! [`TreeState::rows`] разворачивает видимые узлы в отсортированный по
|
||||
//! подписи список, который и рисует вид.
|
||||
|
||||
use std::cell::RefCell;
|
||||
use std::collections::HashMap;
|
||||
use std::rc::Rc;
|
||||
|
||||
use mlua::Function;
|
||||
|
||||
use gpui_kit::{App, FocusHandle, ScrollHandle};
|
||||
|
||||
/// Общая ссылка на состояние блока дерева.
|
||||
pub type TreeHandle = Rc<RefCell<TreeState>>;
|
||||
|
||||
/// Создаёт пустое дерево и общую ссылку на него.
|
||||
pub fn new_tree() -> TreeHandle {
|
||||
Rc::new(RefCell::new(TreeState::new()))
|
||||
}
|
||||
|
||||
/// Один узел, переданный из Lua.
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct TreeNode {
|
||||
/// Условный идентификатор: путь файла или произвольный ключ.
|
||||
pub id: String,
|
||||
/// Текст, который видит пользователь.
|
||||
pub label: String,
|
||||
/// Имя иконки Lucide; `None` — иконка по умолчанию.
|
||||
pub icon: Option<String>,
|
||||
/// Раскрываемая ли нода (папка или иной контейнер).
|
||||
pub expandable: bool,
|
||||
/// Раскрыта ли нода сейчас.
|
||||
pub expanded: bool,
|
||||
}
|
||||
|
||||
/// Узел с сохранёнными связями между элементами дерева.
|
||||
struct NodeRecord {
|
||||
node: TreeNode,
|
||||
parent: Option<String>,
|
||||
children: Vec<String>,
|
||||
/// В узел уже добавляли дочерние элементы хотя бы один раз.
|
||||
loaded: bool,
|
||||
}
|
||||
|
||||
/// Строка, которую вид рисует в текущем кадре.
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct TreeRow {
|
||||
pub id: String,
|
||||
pub label: String,
|
||||
pub icon: Option<String>,
|
||||
/// Уровень вложенности: 0 — корень.
|
||||
pub depth: usize,
|
||||
/// Нода раскрывается.
|
||||
pub expandable: bool,
|
||||
/// Нода раскрыта прямо сейчас.
|
||||
pub expanded: bool,
|
||||
}
|
||||
|
||||
/// Что должен сделать вид после клика по узлу.
|
||||
pub enum TreeAction {
|
||||
/// Раскрыть или свернуть контейнер.
|
||||
Toggle {
|
||||
id: String,
|
||||
expanded: bool,
|
||||
/// Были ли в узел уже загружены дочерние элементы до этого клика.
|
||||
loaded: bool,
|
||||
callback: Option<Function>,
|
||||
},
|
||||
/// Клик по конечной ноде.
|
||||
Click {
|
||||
id: String,
|
||||
callback: Option<Function>,
|
||||
},
|
||||
}
|
||||
|
||||
/// Что вернула обработка клавиши дерева.
|
||||
pub enum TreeKeyResult {
|
||||
/// Клавиша не относится к навигации по дереву.
|
||||
Ignored,
|
||||
/// Клавиша обработана (выделение изменилось или осталось на краю).
|
||||
Handled,
|
||||
/// Нужно вызвать обработчик раскрытия или клика.
|
||||
Action(TreeAction),
|
||||
}
|
||||
|
||||
/// Состояние абстрактного дерева.
|
||||
pub struct TreeState {
|
||||
nodes: HashMap<String, NodeRecord>,
|
||||
roots: Vec<String>,
|
||||
toggle_callback: Option<Function>,
|
||||
click_callback: Option<Function>,
|
||||
/// Выбранная строка; сохраняется, пока панель закрыта.
|
||||
selected: Option<String>,
|
||||
/// Фокус создаётся лениво: состояние дерева может появиться до первого
|
||||
/// кадра, когда `App` ещё недоступен.
|
||||
focus_handle: Option<FocusHandle>,
|
||||
/// Следующий кадр должен передать фокус этому дереву.
|
||||
focus_requested: bool,
|
||||
/// Прокрутка списка: выбранная строка всегда прокручивается в видимую часть.
|
||||
scroll: ScrollHandle,
|
||||
}
|
||||
|
||||
impl Default for TreeState {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
nodes: HashMap::new(),
|
||||
roots: Vec::new(),
|
||||
toggle_callback: None,
|
||||
click_callback: None,
|
||||
selected: None,
|
||||
focus_handle: None,
|
||||
focus_requested: false,
|
||||
scroll: ScrollHandle::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl TreeState {
|
||||
/// Создаёт пустое дерево.
|
||||
pub fn new() -> Self {
|
||||
Self::default()
|
||||
}
|
||||
|
||||
/// Обработчик раскрытия/сворачивания: `(expanded, id, loaded)`.
|
||||
pub fn set_toggle_callback(&mut self, callback: Option<Function>) {
|
||||
self.toggle_callback = callback;
|
||||
}
|
||||
|
||||
/// Обработчик клика по конечной ноде: `(id)`.
|
||||
pub fn set_click_callback(&mut self, callback: Option<Function>) {
|
||||
self.click_callback = callback;
|
||||
}
|
||||
|
||||
/// Идентификатор выбранной строки.
|
||||
pub fn selected(&self) -> Option<&str> {
|
||||
self.selected.as_deref()
|
||||
}
|
||||
|
||||
/// Раскрывает всех закрытых родителей `id`, выбирает узел и просит вид
|
||||
/// передать ему фокус в следующем кадре.
|
||||
///
|
||||
/// Возвращает `false`, если узла с таким `id` нет.
|
||||
pub fn focus(&mut self, id: &str) -> bool {
|
||||
if !self.nodes.contains_key(id) {
|
||||
return false;
|
||||
}
|
||||
self.expand_ancestors(id);
|
||||
self.selected = Some(id.to_owned());
|
||||
self.focus_requested = true;
|
||||
if let Some(index) = self.rows().iter().position(|row| row.id == id) {
|
||||
self.scroll.scroll_to_item(index);
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
/// Обработчик прокрутки списка дерева.
|
||||
pub fn scroll_handle(&self) -> ScrollHandle {
|
||||
self.scroll.clone()
|
||||
}
|
||||
|
||||
/// Фокус-обработчик дерева; создаётся при первом кадре.
|
||||
pub fn focus_handle(&mut self, cx: &App) -> FocusHandle {
|
||||
self.focus_handle
|
||||
.get_or_insert_with(|| cx.focus_handle())
|
||||
.clone()
|
||||
}
|
||||
|
||||
/// Забирает отложенный запрос фокуса.
|
||||
pub fn take_focus_request(&mut self) -> bool {
|
||||
std::mem::take(&mut self.focus_requested)
|
||||
}
|
||||
|
||||
/// Обрабатывает клавишу навигации по дереву.
|
||||
pub fn handle_key(&mut self, key: &str) -> TreeKeyResult {
|
||||
match key.trim().to_lowercase().as_str() {
|
||||
"up" | "arrowup" => {
|
||||
self.move_selection(-1);
|
||||
TreeKeyResult::Handled
|
||||
}
|
||||
"down" | "arrowdown" => {
|
||||
self.move_selection(1);
|
||||
TreeKeyResult::Handled
|
||||
}
|
||||
"enter" | "return" | "kp_enter" => match self.activate_selected() {
|
||||
Some(action) => TreeKeyResult::Action(action),
|
||||
None => TreeKeyResult::Ignored,
|
||||
},
|
||||
"left" | "arrowleft" => self.collapse_or_select_parent(),
|
||||
"right" | "arrowright" => self.expand_or_select_child(),
|
||||
_ => TreeKeyResult::Ignored,
|
||||
}
|
||||
}
|
||||
|
||||
/// Добавляет узлы к `parent`; пустой `parent` — корень дерева.
|
||||
///
|
||||
/// Существующие узлы не удаляются и не дублируются: повторная передача
|
||||
/// того же `id` обновляет описание узла. После вызова контейнер считается
|
||||
/// загруженным, поэтому при следующем раскрытии в обработчик придёт
|
||||
/// `loaded = true`.
|
||||
///
|
||||
/// # Errors
|
||||
/// Возвращает текст ошибки для некорректного `id` (пустой идентификатор),
|
||||
/// неизвестного родителя или попытки сделать узел потомком самого себя.
|
||||
pub fn append(&mut self, parent: &str, nodes: Vec<TreeNode>) -> Result<(), String> {
|
||||
if !parent.is_empty() && !self.nodes.contains_key(parent) {
|
||||
return Err(format!("родительская нода {parent:?} не найдена"));
|
||||
}
|
||||
for node in &nodes {
|
||||
if node.id.is_empty() {
|
||||
return Err("идентификатор ноды не может быть пустым".to_owned());
|
||||
}
|
||||
if node.id == parent || self.would_cycle(&node.id, parent) {
|
||||
return Err(format!(
|
||||
"нода {:?} не может быть потомком самой себя",
|
||||
node.id
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
for node in nodes {
|
||||
let requested_parent = (!parent.is_empty()).then(|| parent.to_owned());
|
||||
let id = node.id.clone();
|
||||
|
||||
if self.nodes.contains_key(&id) {
|
||||
let old_parent = self.nodes.get(&id).and_then(|record| record.parent.clone());
|
||||
// Повторный append обновляет описание узла, но не создаёт копию.
|
||||
if let Some(record) = self.nodes.get_mut(&id) {
|
||||
record.node = node;
|
||||
}
|
||||
if old_parent != requested_parent {
|
||||
self.detach(&id);
|
||||
self.attach(&requested_parent, &id);
|
||||
}
|
||||
} else {
|
||||
self.nodes.insert(
|
||||
id.clone(),
|
||||
NodeRecord {
|
||||
node,
|
||||
parent: requested_parent.clone(),
|
||||
children: Vec::new(),
|
||||
loaded: false,
|
||||
},
|
||||
);
|
||||
self.attach(&requested_parent, &id);
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(record) = self.nodes.get_mut(parent) {
|
||||
record.loaded = true;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Удаляет узел и все его дочерние узлы рекурсивно.
|
||||
///
|
||||
/// Возвращает `false`, если узла с таким `id` нет.
|
||||
pub fn remove(&mut self, id: &str) -> bool {
|
||||
if !self.nodes.contains_key(id) {
|
||||
return false;
|
||||
}
|
||||
self.detach(id);
|
||||
|
||||
let mut stack = vec![id.to_owned()];
|
||||
while let Some(current) = stack.pop() {
|
||||
if let Some(record) = self.nodes.remove(¤t) {
|
||||
stack.extend(record.children);
|
||||
}
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
/// Видимые строки дерева в порядке отрисовки.
|
||||
///
|
||||
/// Дети каждого уровня сортируются по отображаемому тексту без учёта
|
||||
/// регистра; дети свёрнутых узлов не показываются.
|
||||
pub fn rows(&self) -> Vec<TreeRow> {
|
||||
let mut rows = Vec::new();
|
||||
for id in self.sorted(&self.roots) {
|
||||
self.push_rows(&id, 0, &mut rows);
|
||||
}
|
||||
rows
|
||||
}
|
||||
|
||||
/// Обрабатывает клик по узлу и возвращает действие для вида.
|
||||
pub fn activate(&mut self, id: &str) -> Option<TreeAction> {
|
||||
let toggle_callback = self.toggle_callback.clone();
|
||||
let click_callback = self.click_callback.clone();
|
||||
|
||||
// Раскрытие или открытие файла может изменить документы и заставить
|
||||
// редактор забрать фокус; дерево возвращает его себе в следующем кадре.
|
||||
self.focus_requested = true;
|
||||
|
||||
let record = self.nodes.get_mut(id)?;
|
||||
if record.node.expandable {
|
||||
let loaded = record.loaded;
|
||||
record.node.expanded = !record.node.expanded;
|
||||
let expanded = record.node.expanded;
|
||||
Some(TreeAction::Toggle {
|
||||
id: id.to_owned(),
|
||||
expanded,
|
||||
loaded,
|
||||
callback: toggle_callback,
|
||||
})
|
||||
} else {
|
||||
Some(TreeAction::Click {
|
||||
id: id.to_owned(),
|
||||
callback: click_callback,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// Дочерние `id`, отсортированные по подписи: сначала раскрываемые узлы
|
||||
/// (папки), затем конечные; внутри группы — по алфавиту без учёта регистра.
|
||||
fn sorted(&self, ids: &[String]) -> Vec<String> {
|
||||
let mut entries = ids
|
||||
.iter()
|
||||
.filter_map(|id| {
|
||||
self.nodes.get(id).map(|record| {
|
||||
(
|
||||
record.node.expandable,
|
||||
record.node.label.to_lowercase(),
|
||||
id.clone(),
|
||||
)
|
||||
})
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
entries.sort_by(|a, b| {
|
||||
b.0.cmp(&a.0)
|
||||
.then_with(|| a.1.cmp(&b.1))
|
||||
.then_with(|| a.2.cmp(&b.2))
|
||||
});
|
||||
entries.into_iter().map(|(_, _, id)| id).collect()
|
||||
}
|
||||
|
||||
/// Сдвигает выборку по видимым строкам.
|
||||
fn move_selection(&mut self, delta: isize) {
|
||||
let rows = self.rows();
|
||||
if rows.is_empty() {
|
||||
self.selected = None;
|
||||
return;
|
||||
}
|
||||
let current = self
|
||||
.selected
|
||||
.as_deref()
|
||||
.and_then(|id| rows.iter().position(|row| row.id == id));
|
||||
let next = match current {
|
||||
Some(ix) if delta < 0 => ix.saturating_sub(1),
|
||||
Some(ix) => (ix + 1).min(rows.len() - 1),
|
||||
None if delta < 0 => rows.len() - 1,
|
||||
None => 0,
|
||||
};
|
||||
self.selected = Some(rows[next].id.clone());
|
||||
self.scroll.scroll_to_item(next);
|
||||
}
|
||||
|
||||
/// Enter: раскрывает папку или вызывает обработчик клика конечной ноды.
|
||||
fn activate_selected(&mut self) -> Option<TreeAction> {
|
||||
let id = self.selected.clone()?;
|
||||
self.activate(&id)
|
||||
}
|
||||
|
||||
/// Стрелка влево: сворачивает папку, иначе выбирает родителя.
|
||||
fn collapse_or_select_parent(&mut self) -> TreeKeyResult {
|
||||
let Some(id) = self.selected.clone() else {
|
||||
return TreeKeyResult::Ignored;
|
||||
};
|
||||
let expanded = self
|
||||
.nodes
|
||||
.get(&id)
|
||||
.is_some_and(|record| record.node.expandable && record.node.expanded);
|
||||
if expanded {
|
||||
return match self.activate(&id) {
|
||||
Some(action) => TreeKeyResult::Action(action),
|
||||
None => TreeKeyResult::Handled,
|
||||
};
|
||||
}
|
||||
if let Some(parent) = self.nodes.get(&id).and_then(|record| record.parent.clone()) {
|
||||
self.selected = Some(parent.clone());
|
||||
if let Some(index) = self.rows().iter().position(|row| row.id == parent) {
|
||||
self.scroll.scroll_to_item(index);
|
||||
}
|
||||
}
|
||||
TreeKeyResult::Handled
|
||||
}
|
||||
|
||||
/// Стрелка вправо: раскрывает папку, иначе выбирает первого ребёнка.
|
||||
fn expand_or_select_child(&mut self) -> TreeKeyResult {
|
||||
let Some(id) = self.selected.clone() else {
|
||||
return TreeKeyResult::Ignored;
|
||||
};
|
||||
let Some(record) = self.nodes.get(&id) else {
|
||||
return TreeKeyResult::Ignored;
|
||||
};
|
||||
if record.node.expandable && !record.node.expanded {
|
||||
return match self.activate(&id) {
|
||||
Some(action) => TreeKeyResult::Action(action),
|
||||
None => TreeKeyResult::Handled,
|
||||
};
|
||||
}
|
||||
if record.node.expanded
|
||||
&& let Some(child) = self.sorted(&record.children).first().cloned()
|
||||
{
|
||||
let index = self.rows().iter().position(|row| row.id == child);
|
||||
self.selected = Some(child);
|
||||
if let Some(index) = index {
|
||||
self.scroll.scroll_to_item(index);
|
||||
}
|
||||
}
|
||||
TreeKeyResult::Handled
|
||||
}
|
||||
|
||||
/// Раскрывает всех родителей узла, не трогая его собственное состояние.
|
||||
fn expand_ancestors(&mut self, id: &str) {
|
||||
let mut parents = Vec::new();
|
||||
let mut current = self.nodes.get(id).and_then(|record| record.parent.clone());
|
||||
while let Some(parent) = current {
|
||||
current = self
|
||||
.nodes
|
||||
.get(&parent)
|
||||
.and_then(|record| record.parent.clone());
|
||||
parents.push(parent);
|
||||
}
|
||||
for parent in parents {
|
||||
if let Some(record) = self.nodes.get_mut(&parent) {
|
||||
record.node.expanded = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Добавляет `id` и его раскрытых потомков в плоский список.
|
||||
fn push_rows(&self, id: &str, depth: usize, rows: &mut Vec<TreeRow>) {
|
||||
let Some(record) = self.nodes.get(id) else {
|
||||
return;
|
||||
};
|
||||
rows.push(TreeRow {
|
||||
id: record.node.id.clone(),
|
||||
label: record.node.label.clone(),
|
||||
icon: record.node.icon.clone(),
|
||||
depth,
|
||||
expandable: record.node.expandable,
|
||||
expanded: record.node.expanded,
|
||||
});
|
||||
|
||||
if record.node.expanded {
|
||||
for child in self.sorted(&record.children) {
|
||||
self.push_rows(&child, depth + 1, rows);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Проверяет, окажется ли `parent` внутри поддерева `node_id`.
|
||||
fn would_cycle(&self, node_id: &str, parent: &str) -> bool {
|
||||
let mut current = parent;
|
||||
while !current.is_empty() {
|
||||
if current == node_id {
|
||||
return true;
|
||||
}
|
||||
match self
|
||||
.nodes
|
||||
.get(current)
|
||||
.and_then(|record| record.parent.as_deref())
|
||||
{
|
||||
Some(next) => current = next,
|
||||
None => break,
|
||||
}
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
/// Присоединяет узел к родителю или корню.
|
||||
fn attach(&mut self, parent: &Option<String>, id: &str) {
|
||||
match parent {
|
||||
Some(parent) => {
|
||||
if let Some(record) = self.nodes.get_mut(parent) {
|
||||
record.children.push(id.to_owned());
|
||||
}
|
||||
}
|
||||
None => self.roots.push(id.to_owned()),
|
||||
}
|
||||
}
|
||||
|
||||
/// Отсоединяет узел от родителя или корня.
|
||||
fn detach(&mut self, id: &str) {
|
||||
let Some(parent) = self.nodes.get(id).and_then(|record| record.parent.clone()) else {
|
||||
self.roots.retain(|root| root != id);
|
||||
return;
|
||||
};
|
||||
if let Some(record) = self.nodes.get_mut(&parent) {
|
||||
record.children.retain(|child| child != id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn node(id: &str, label: &str, expandable: bool) -> TreeNode {
|
||||
TreeNode {
|
||||
id: id.to_owned(),
|
||||
label: label.to_owned(),
|
||||
icon: None,
|
||||
expandable,
|
||||
expanded: false,
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rows_are_sorted_by_label_and_follow_expansion() {
|
||||
let mut tree = TreeState::new();
|
||||
tree.append(
|
||||
"",
|
||||
vec![
|
||||
node("b", "banana", false),
|
||||
node("folder", "Alpha", true),
|
||||
node("a", "apple", false),
|
||||
],
|
||||
)
|
||||
.unwrap();
|
||||
tree.append(
|
||||
"folder",
|
||||
vec![node("z", "zebra", false), node("c", "cat", false)],
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
// Свёрнутая папка не показывает детей.
|
||||
let rows = tree.rows();
|
||||
assert_eq!(
|
||||
rows.iter()
|
||||
.map(|row| row.label.as_str())
|
||||
.collect::<Vec<_>>(),
|
||||
vec!["Alpha", "apple", "banana"]
|
||||
);
|
||||
|
||||
assert!(matches!(
|
||||
tree.activate("folder"),
|
||||
Some(TreeAction::Toggle {
|
||||
expanded: true,
|
||||
loaded: true,
|
||||
..
|
||||
})
|
||||
));
|
||||
let rows = tree.rows();
|
||||
assert_eq!(
|
||||
rows.iter()
|
||||
.map(|row| (row.label.as_str(), row.depth))
|
||||
.collect::<Vec<_>>(),
|
||||
vec![
|
||||
("Alpha", 0),
|
||||
("cat", 1),
|
||||
("zebra", 1),
|
||||
("apple", 0),
|
||||
("banana", 0)
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn append_does_not_erase_and_marks_the_parent_loaded() {
|
||||
let mut tree = TreeState::new();
|
||||
tree.append("", vec![node("src", "src", true)]).unwrap();
|
||||
assert!(matches!(
|
||||
tree.activate("src"),
|
||||
Some(TreeAction::Toggle {
|
||||
expanded: true,
|
||||
loaded: false,
|
||||
..
|
||||
})
|
||||
));
|
||||
|
||||
// Сворачиваем, чтобы проверить флаг загрузки после append.
|
||||
tree.activate("src");
|
||||
tree.append("src", vec![node("main.rs", "main.rs", false)])
|
||||
.unwrap();
|
||||
assert!(matches!(
|
||||
tree.activate("src"),
|
||||
Some(TreeAction::Toggle {
|
||||
expanded: true,
|
||||
loaded: true,
|
||||
..
|
||||
})
|
||||
));
|
||||
assert_eq!(tree.rows().len(), 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn remove_deletes_the_subtree() {
|
||||
let mut tree = TreeState::new();
|
||||
tree.append(
|
||||
"",
|
||||
vec![node("src", "src", true), node("readme", "README.md", false)],
|
||||
)
|
||||
.unwrap();
|
||||
tree.append("src", vec![node("ui", "ui", true)]).unwrap();
|
||||
tree.append("ui", vec![node("tree", "tree.rs", false)])
|
||||
.unwrap();
|
||||
|
||||
assert!(tree.remove("src"));
|
||||
let rows = tree.rows();
|
||||
assert_eq!(
|
||||
rows.iter()
|
||||
.map(|row| row.label.as_str())
|
||||
.collect::<Vec<_>>(),
|
||||
vec!["README.md"]
|
||||
);
|
||||
assert!(!tree.remove("src"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn focus_expands_ancestors_and_selects_the_node() {
|
||||
let mut tree = TreeState::new();
|
||||
tree.append("", vec![node("src", "src", true)]).unwrap();
|
||||
tree.append("src", vec![node("ui", "ui", true)]).unwrap();
|
||||
tree.append("ui", vec![node("tree", "tree.rs", false)])
|
||||
.unwrap();
|
||||
|
||||
// Все родители свёрнуты — видна только корневая папка.
|
||||
assert_eq!(tree.rows().len(), 1);
|
||||
assert!(!tree.focus("missing"));
|
||||
|
||||
assert!(tree.focus("tree"));
|
||||
assert_eq!(tree.selected(), Some("tree"));
|
||||
assert!(tree.take_focus_request());
|
||||
assert_eq!(
|
||||
tree.rows()
|
||||
.iter()
|
||||
.map(|row| row.label.as_str())
|
||||
.collect::<Vec<_>>(),
|
||||
vec!["src", "ui", "tree.rs"]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn keyboard_moves_selection_and_activates_nodes() {
|
||||
let mut tree = TreeState::new();
|
||||
tree.append(
|
||||
"",
|
||||
vec![node("src", "src", true), node("readme", "README.md", false)],
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
// Папки идут раньше файлов, поэтому первая строка — src.
|
||||
assert!(matches!(tree.handle_key("down"), TreeKeyResult::Handled));
|
||||
assert_eq!(tree.selected(), Some("src"));
|
||||
assert!(matches!(tree.handle_key("down"), TreeKeyResult::Handled));
|
||||
assert_eq!(tree.selected(), Some("readme"));
|
||||
|
||||
assert!(matches!(
|
||||
tree.handle_key("enter"),
|
||||
TreeKeyResult::Action(TreeAction::Click { .. })
|
||||
));
|
||||
|
||||
assert!(matches!(tree.handle_key("up"), TreeKeyResult::Handled));
|
||||
assert_eq!(tree.selected(), Some("src"));
|
||||
assert!(matches!(
|
||||
tree.handle_key("right"),
|
||||
TreeKeyResult::Action(TreeAction::Toggle { expanded: true, .. })
|
||||
));
|
||||
assert!(matches!(
|
||||
tree.handle_key("left"),
|
||||
TreeKeyResult::Action(TreeAction::Toggle {
|
||||
expanded: false,
|
||||
..
|
||||
})
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn invalid_parents_and_cycles_are_rejected() {
|
||||
let mut tree = TreeState::new();
|
||||
assert!(tree.append("missing", vec![node("a", "a", false)]).is_err());
|
||||
tree.append("", vec![node("a", "a", true)]).unwrap();
|
||||
tree.append("a", vec![node("b", "b", true)]).unwrap();
|
||||
assert!(tree.append("b", vec![node("a", "a", true)]).is_err());
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user