use crate::content_tree::parse::TreeNode;
use std::collections::HashSet;
pub struct ContentTreeState {
pub note_id: String,
pub note_title: String,
pub nodes: Vec<TreeNode>,
pub selected: usize, pub expanded: HashSet<usize>, pub load_error: bool,
}
impl ContentTreeState {
pub fn error(note_id: String) -> Self {
Self {
note_id,
note_title: String::new(),
nodes: vec![],
selected: 0,
expanded: HashSet::new(),
load_error: true,
}
}
pub fn new(note_id: String, title: &str, content: &str) -> Self {
let nodes = crate::content_tree::parse::parse_outline(title, content);
let mut expanded = HashSet::new();
for (i, n) in nodes.iter().enumerate() {
if matches!(n.kind, crate::content_tree::parse::NodeKind::Header { .. }) {
expanded.insert(i); }
}
Self {
note_id,
note_title: title.to_string(),
nodes,
selected: 0,
expanded,
load_error: false,
}
}
pub fn visible_indices(&self) -> Vec<usize> {
let mut visible = Vec::new();
let mut skip_until_depth = None;
for (i, n) in self.nodes.iter().enumerate() {
if let Some(limit_depth) = skip_until_depth {
if n.depth > limit_depth {
continue; } else {
skip_until_depth = None; }
}
visible.push(i);
if self.is_header(i) && !self.expanded.contains(&i) {
skip_until_depth = Some(n.depth);
}
}
visible
}
pub fn is_header(&self, i: usize) -> bool {
self.nodes
.get(i)
.is_some_and(|n| matches!(n.kind, crate::content_tree::parse::NodeKind::Header { .. }))
}
pub fn move_up(&mut self) {
let visible = self.visible_indices();
if visible.is_empty() {
return;
}
let pos = visible
.iter()
.position(|&x| x == self.selected)
.unwrap_or_else(|| {
visible
.iter()
.position(|&x| x > self.selected)
.unwrap_or(visible.len())
.saturating_sub(1)
});
if pos > 0 {
self.selected = visible[pos - 1];
} else {
self.selected = visible[0];
}
}
pub fn move_down(&mut self) {
let visible = self.visible_indices();
if visible.is_empty() {
return;
}
let pos = visible
.iter()
.position(|&x| x == self.selected)
.unwrap_or_else(|| {
visible
.iter()
.position(|&x| x > self.selected)
.unwrap_or(visible.len())
.saturating_sub(1)
});
if pos + 1 < visible.len() {
self.selected = visible[pos + 1];
} else {
self.selected = visible[visible.len() - 1];
}
}
pub fn toggle_collapse(&mut self) {
if self.is_header(self.selected) {
if self.expanded.contains(&self.selected) {
self.expanded.remove(&self.selected);
} else {
self.expanded.insert(self.selected);
}
}
}
pub fn expand_all(&mut self) {
for i in 0..self.nodes.len() {
if self.is_header(i) {
self.expanded.insert(i);
}
}
}
pub fn collapse_all(&mut self) {
self.expanded.clear();
if !self.nodes.is_empty() {
self.expanded.insert(0);
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_content_tree_state() {
let content = "
# H1
Some intro.
## H2
- Item 1
";
let mut state = ContentTreeState::new("id".to_string(), "Title", content);
assert!(!state.load_error);
assert_eq!(state.note_title, "Title");
assert_eq!(state.nodes.len(), 5);
assert!(state.expanded.contains(&0));
assert!(state.expanded.contains(&1));
assert!(state.expanded.contains(&3));
let visible = state.visible_indices();
assert_eq!(visible, vec![0, 1, 2, 3, 4]);
state.selected = 3;
state.toggle_collapse();
assert!(!state.expanded.contains(&3));
let visible_after_collapse = state.visible_indices();
assert_eq!(visible_after_collapse, vec![0, 1, 2, 3]);
state.selected = 3;
state.move_down(); assert_eq!(state.selected, 3);
state.move_up(); assert_eq!(state.selected, 2);
state.collapse_all();
assert!(state.expanded.contains(&0));
assert!(!state.expanded.contains(&1));
assert!(!state.expanded.contains(&3));
let visible_collapsed_all = state.visible_indices();
assert_eq!(visible_collapsed_all, vec![0, 1]);
}
}