From 8a8bebb5ac8d2751780cbbc664ffc237f316f965 Mon Sep 17 00:00:00 2001 From: ynqa Date: Sun, 12 Apr 2026 15:51:53 +0900 Subject: [PATCH 1/7] chore: Row based implementations for tree-widgets --- examples/tree/src/tree.rs | 5 +- promkit-widgets/src/tree.rs | 81 +++--- promkit-widgets/src/tree/document.rs | 78 ++++++ promkit-widgets/src/tree/node.rs | 362 -------------------------- promkit-widgets/src/tree/tree.rs | 81 ------ promkit-widgets/src/tree/treez.rs | 367 +++++++++++++++++++++++++++ promkit/src/preset/tree.rs | 10 +- promkit/src/preset/tree/evaluate.rs | 10 +- 8 files changed, 492 insertions(+), 502 deletions(-) create mode 100644 promkit-widgets/src/tree/document.rs delete mode 100644 promkit-widgets/src/tree/node.rs delete mode 100644 promkit-widgets/src/tree/tree.rs create mode 100644 promkit-widgets/src/tree/treez.rs diff --git a/examples/tree/src/tree.rs b/examples/tree/src/tree.rs index cf65ee61..5cc25f53 100644 --- a/examples/tree/src/tree.rs +++ b/examples/tree/src/tree.rs @@ -1,11 +1,12 @@ use std::path::Path; -use promkit::{preset::tree::Tree, widgets::tree::node::Node, Prompt}; +use promkit::{preset::tree::Tree, widgets::tree::Document, Prompt}; #[tokio::main] async fn main() -> anyhow::Result<()> { let root = Path::new(env!("CARGO_MANIFEST_DIR")).join("../../promkit/src"); - let ret = Tree::new(Node::try_from(&root)?) + let document = Document::from_path(&root)?; + let ret = Tree::new(document) .title("Select a directory or file") .tree_lines(10) .run() diff --git a/promkit-widgets/src/tree.rs b/promkit-widgets/src/tree.rs index afe81ed2..8fad2f62 100644 --- a/promkit-widgets/src/tree.rs +++ b/promkit-widgets/src/tree.rs @@ -1,12 +1,11 @@ use promkit_core::{Widget, grapheme::StyledGraphemes}; -pub mod node; -use node::Kind; -#[path = "tree/tree.rs"] -mod inner; -pub use inner::Tree; +mod document; +pub use document::Document; pub mod config; pub use config::Config; +pub mod treez; +pub use treez::Row; /// Represents the state of a tree structure within the application. /// @@ -17,31 +16,18 @@ pub use config::Config; /// for child items in the tree. #[derive(Clone)] pub struct State { - pub tree: Tree, + pub document: Document, /// Configuration for rendering and behavior. pub config: Config, } impl Widget for State { fn create_graphemes(&self, _width: u16, height: u16) -> StyledGraphemes { - let symbol = |kind: &Kind| -> &str { - match kind { - Kind::Folded { .. } => &self.config.folded_symbol, - Kind::Unfolded { .. } => &self.config.unfolded_symbol, - } - }; - - let indent = |kind: &Kind| -> usize { - match kind { - Kind::Folded { path, .. } | Kind::Unfolded { path, .. } => { - path.len() * self.config.indent - } - } - }; - - let id = |kind: &Kind| -> String { - match kind { - Kind::Folded { id, .. } | Kind::Unfolded { id, .. } => id.clone(), + let symbol = |row: &Row| -> &str { + if row.has_children && !row.collapsed { + &self.config.unfolded_symbol + } else { + &self.config.folded_symbol } }; @@ -50,29 +36,30 @@ impl Widget for State { None => height as usize, }; - let kinds = self.tree.kinds(); - let lines = kinds - .iter() - .enumerate() - .filter(|(i, _)| *i >= self.tree.position() && *i < self.tree.position() + height) - .map(|(i, kind)| { - if i == self.tree.position() { - StyledGraphemes::from_str( - format!("{}{}{}", symbol(kind), " ".repeat(indent(kind)), id(kind),), - self.config.active_item_style, - ) - } else { - StyledGraphemes::from_str( - format!( - "{}{}{}", - " ".repeat(StyledGraphemes::from(symbol(kind)).widths()), - " ".repeat(indent(kind)), - id(kind), - ), - self.config.inactive_item_style, - ) - } - }); + let rows = self.document.extract_rows_from_current(height); + let lines = rows.iter().enumerate().map(|(offset, row)| { + if offset == 0 { + StyledGraphemes::from_str( + format!( + "{}{}{}", + symbol(row), + " ".repeat(row.depth * self.config.indent), + row.id, + ), + self.config.active_item_style, + ) + } else { + StyledGraphemes::from_str( + format!( + "{}{}{}", + " ".repeat(StyledGraphemes::from(symbol(row)).widths()), + " ".repeat(row.depth * self.config.indent), + row.id, + ), + self.config.inactive_item_style, + ) + } + }); StyledGraphemes::from_lines(lines) } diff --git a/promkit-widgets/src/tree/document.rs b/promkit-widgets/src/tree/document.rs new file mode 100644 index 00000000..76e2e615 --- /dev/null +++ b/promkit-widgets/src/tree/document.rs @@ -0,0 +1,78 @@ +use super::{Row, treez::RowOperation}; + +/// Represents a navigable tree document, allowing for efficient row navigation and folding. +#[derive(Clone)] +pub struct Document { + rows: Vec, + position: usize, +} + +impl Document { + pub fn new(rows: Vec) -> Self { + Self { rows, position: 0 } + } + + pub fn from_path(path: &std::path::Path) -> anyhow::Result { + Ok(Self::new(super::treez::create_rows_from_path(path)?)) + } +} + +impl Document { + /// Returns a reference to the underlying vector of rows. + pub fn rows(&self) -> &[Row] { + &self.rows + } + + /// Returns the selected tree path represented by visible row labels. + pub fn get(&self) -> Vec { + self.rows + .get(self.position) + .map(|row| row.path.clone()) + .unwrap_or_default() + } + + /// Extract rows from the current cursor position. + pub fn extract_rows_from_current(&self, n: usize) -> Vec { + self.rows.extract(self.position, n) + } + + /// Toggles the visibility of a node at the cursor's current position. + pub fn toggle(&mut self) { + let index = self.rows.toggle(self.position); + self.position = index; + } + + /// Sets the visibility of all rows. + pub fn set_nodes_visibility(&mut self, collapsed: bool) { + self.rows.set_rows_visibility(collapsed); + self.position = self.rows.head(); + } + + /// Moves the cursor backward through rows. + pub fn up(&mut self) -> bool { + let index = self.rows.up(self.position); + let ret = index != self.position; + self.position = index; + ret + } + + /// Moves the cursor to the head position. + pub fn head(&mut self) -> bool { + self.position = self.rows.head(); + true + } + + /// Moves the cursor forward through rows. + pub fn down(&mut self) -> bool { + let index = self.rows.down(self.position); + let ret = index != self.position; + self.position = index; + ret + } + + /// Moves the cursor to the last position. + pub fn tail(&mut self) -> bool { + self.position = self.rows.tail(); + true + } +} diff --git a/promkit-widgets/src/tree/node.rs b/promkit-widgets/src/tree/node.rs deleted file mode 100644 index 54be896a..00000000 --- a/promkit-widgets/src/tree/node.rs +++ /dev/null @@ -1,362 +0,0 @@ -use std::{fs, path}; - -/// Represents the kind of a node in a tree structure. -/// -/// This enum is used to distinguish between nodes that are currently -/// visible in their "folded" state and those that are "unfolded" to reveal -/// their children, if any. -#[derive(Clone, Debug, PartialEq, Eq)] -pub enum Kind { - /// Represents a node that is folded (i.e., its children are not currently visible). - /// - `id`: A unique identifier for the node. - /// - `path`: The path from the root to this node, represented as a sequence of indices. - Folded { id: String, path: Path }, - - /// Represents a node that is unfolded (i.e., its children are currently visible). - /// - `id`: A unique identifier for the node. - /// - `path`: The path from the root to this node, represented as a sequence of indices. - Unfolded { id: String, path: Path }, -} - -/// A type alias for a path in the tree, represented as a sequence of indices. -pub type Path = Vec; - -/// Represents a node within a tree structure. -/// -/// A node can either be a `NonLeaf`, containing children and a visibility flag, -/// or a `Leaf`, representing an end node without children. -#[derive(Clone, Debug, PartialEq, Eq)] -pub enum Node { - /// Represents a non-leaf node, which can contain child nodes. - /// - `id`: A unique identifier for the node. - /// - `children`: A vector of child nodes. - /// - `children_visible`: A boolean indicating if the children of this node are visible. - NonLeaf { - id: String, - children: Vec, - children_visible: bool, - }, - - /// Represents a leaf node, which does not contain any child nodes. - /// - `id`: A unique identifier for the leaf node. - Leaf(String), -} - -impl TryFrom<&path::PathBuf> for Node { - /// Attempts to create a `Node` from a given directory path. - /// - /// This method constructs a `Node::NonLeaf` representing the directory specified by `dir_path`. - /// It recursively explores the directory, converting subdirectories into `Node::NonLeaf` instances - /// and files into `Node::Leaf` instances. Directories and files are kept in separate lists initially, - /// then combined with all directories first, followed by files. Both lists are sorted alphabetically - /// before merging. The resulting tree structure reflects the hierarchy of files and directories within - /// `dir_path`, with directories listed before files, both in alphabetical order. - /// - /// # Parameters - /// - `dir_path`: A reference to a `PathBuf` representing the directory to be converted into a `Node`. - /// - /// # Returns - /// A `Result` containing the root `Node` of the constructed tree if successful, or an `Error` if the - /// directory cannot be read or if any file name cannot be converted to a string. - /// - /// # Errors - /// This method returns an `Error` if: - /// - The path does not exist or is not a directory. - /// - There is an error reading the directory contents. - /// - A file name cannot be converted to a UTF-8 string. - type Error = anyhow::Error; - - fn try_from(dir_path: &path::PathBuf) -> anyhow::Result { - let mut directories = Vec::new(); - let mut files = Vec::new(); - - if dir_path.is_dir() { - for entry in fs::read_dir(dir_path)? { - let path = entry?.path(); - if path.is_dir() { - directories.push(Node::try_from(&path)?); - } else if path.is_file() { - files.push(Node::Leaf( - path.file_name() - .and_then(|name| name.to_str()) - .ok_or_else(|| { - std::io::Error::other("Failed to convert file name to string") - })? - .to_string(), - )); - } - } - } - - directories.sort_by(|a, b| a.id().cmp(b.id())); - files.sort_by(|a, b| a.id().cmp(b.id())); - - let mut children = directories; - children.extend(files); - - Ok(Node::NonLeaf { - id: dir_path - .file_name() - .and_then(|name| name.to_str()) - .ok_or_else(|| std::io::Error::other("Failed to convert directory name to string"))? - .to_string(), - children, - children_visible: false, - }) - } -} - -impl Node { - fn id(&self) -> &String { - match self { - Node::NonLeaf { id, .. } => id, - Node::Leaf(id) => id, - } - } - - /// Flattens the tree structure into a vector of `Kind`, including only visible nodes. - /// - /// This method performs a depth-first search (DFS) to traverse the tree and collect - /// nodes into a vector. Each node is represented as either `Kind::Folded` or `Kind::Unfolded` - /// based on its visibility and whether it has children. - /// - /// Returns: - /// - Vec: A vector of `Kind` representing the visible nodes in the tree. - pub fn flatten_visibles(&self) -> Vec { - fn dfs(node: &Node, path: Path, ret: &mut Vec) { - match node { - Node::NonLeaf { - id, - children, - children_visible, - } => { - if *children_visible { - ret.push(Kind::Unfolded { - id: id.clone(), - path: path.clone(), - }); - for (index, child) in children.iter().enumerate() { - let mut new_path = path.clone(); - new_path.push(index); - dfs(child, new_path, ret); - } - } else { - ret.push(Kind::Folded { - id: id.clone(), - path: path.clone(), - }); - } - } - Node::Leaf(item) => { - ret.push(Kind::Folded { - id: item.clone(), - path: path.clone(), - }); - } - } - } - - let mut ret = Vec::new(); - dfs(self, Vec::new(), &mut ret); - ret - } - - /// Toggles the visibility of the children of the node specified by the given path. - /// - /// Parameters: - /// - path: &Path - A reference to a vector of usize, representing the path to the target node. - /// - /// This method modifies the tree in-place. If the target node is found and is a `NonLeaf`, - /// its `children_visible` field is toggled. - pub fn toggle(&mut self, path: &Path) { - if let Some(Node::NonLeaf { - children_visible, .. - }) = self.get_mut(path) - { - *children_visible = !*children_visible; - } - } - - /// Retrieves the IDs of all nodes along the path to a specified node. - /// - /// Parameters: - /// - path: &Path - A reference to a vector of usize, representing the path to the target node. - /// - /// Returns: - /// - Vec: A vector of String IDs representing the nodes along the path to the target node. - pub fn get_waypoints(&self, path: &Path) -> Vec { - let mut ids = Vec::new(); - let mut node = self; - for &index in path { - match node { - Node::NonLeaf { id, children, .. } => { - ids.push(id.clone()); - if let Some(child) = children.get(index) { - node = child; - } else { - break; - } - } - Node::Leaf(id) => { - ids.push(id.clone()); - break; - } - } - } - ids - } - - /// Retrieves a reference to the node specified by the given path. - /// - /// Parameters: - /// - path: &Path - A reference to a vector of usize, representing the path to the target node. - /// - /// Returns: - /// - Option<&Node>: An option containing a reference to the target node if found, or None otherwise. - pub fn get(&self, path: &Path) -> Option<&Node> { - let mut node = self; - for seg in path { - match node { - Node::NonLeaf { - id: _, - children, - children_visible: _, - } => { - if let Some(next_node) = children.get(*seg) { - node = next_node; - } else { - return None; - } - } - Node::Leaf(_) => { - return None; - } - } - } - Some(node) - } - - /// Retrieves a mutable reference to the node specified by the given path. - /// - /// Parameters: - /// - path: &Path - A reference to a vector of usize, representing the path to the target node. - /// - /// Returns: - /// - Option<&mut Node>: An option containing a mutable reference to the target node if found, or None otherwise. - pub fn get_mut(&mut self, path: &Path) -> Option<&mut Node> { - let mut node = self; - for seg in path { - match node { - Node::NonLeaf { - id: _, - children, - children_visible: _, - } => { - if let Some(next_node) = children.get_mut(*seg) { - node = next_node; - } else { - return None; - } - } - Node::Leaf(_) => { - return None; - } - } - } - Some(node) - } -} - -#[cfg(test)] -mod test { - use super::*; - - fn create_test_node() -> Node { - Node::NonLeaf { - id: "root".into(), - children: vec![ - Node::NonLeaf { - id: "a".into(), - children: vec![Node::Leaf("aa".into()), Node::Leaf("ab".into())], - children_visible: true, - }, - Node::Leaf("b".into()), - Node::Leaf("c".into()), - ], - children_visible: true, - } - } - - fn as_nonleaf(node: &Node) -> Option<(&String, &Vec, bool)> { - match node { - Node::NonLeaf { - id, - children, - children_visible, - } => Some((id, children, *children_visible)), - _ => None, - } - } - - mod toggle { - use super::*; - - #[test] - fn test() { - let mut node = create_test_node(); - node.toggle(&vec![]); - assert!(!as_nonleaf(node.get(&vec![]).unwrap()).unwrap().2); - } - } - - mod flatten_visibles { - use super::*; - - #[test] - fn test() { - let node = create_test_node(); - assert_eq!( - vec![ - Kind::Unfolded { - id: "root".into(), - path: vec![], - }, - Kind::Unfolded { - id: "a".into(), - path: vec![0], - }, - Kind::Folded { - id: "aa".into(), - path: vec![0, 0], - }, - Kind::Folded { - id: "ab".into(), - path: vec![0, 1], - }, - Kind::Folded { - id: "b".into(), - path: vec![1], - }, - Kind::Folded { - id: "c".into(), - path: vec![2], - }, - ], - node.flatten_visibles(), - ); - } - - #[test] - fn test_after_toggle() { - let mut node = create_test_node(); - node.toggle(&vec![]); - assert_eq!( - vec![Kind::Folded { - id: "root".into(), - path: vec![], - },], - node.flatten_visibles(), - ); - } - } -} diff --git a/promkit-widgets/src/tree/tree.rs b/promkit-widgets/src/tree/tree.rs deleted file mode 100644 index 125ff7f9..00000000 --- a/promkit-widgets/src/tree/tree.rs +++ /dev/null @@ -1,81 +0,0 @@ -use crate::cursor::Cursor; - -use super::node::{Kind, Node}; - -/// A `Tree` structure that manages a collection of nodes in a hierarchical manner. -/// It utilizes a cursor to navigate and manipulate the nodes within the tree. -#[derive(Clone)] -pub struct Tree { - root: Node, - cursor: Cursor>, -} - -impl Tree { - /// Creates a new `Tree` with a given root node. - /// - /// # Arguments - /// - /// * `root` - The root node of the tree. - pub fn new(root: Node) -> Self { - Self { - root: root.clone(), - cursor: Cursor::new(root.flatten_visibles(), 0, false), - } - } - - /// Returns a vector of all nodes in the tree, represented with their depth information. - pub fn kinds(&self) -> Vec { - self.cursor.contents().clone() - } - - /// Returns the current position of the cursor within the tree. - pub fn position(&self) -> usize { - self.cursor.position() - } - - /// Retrieves the data of the current node pointed by the cursor, along with its path from the root. - pub fn get(&self) -> Vec { - let kind = self.cursor.contents()[self.position()].clone(); - match kind { - Kind::Folded { id, path } | Kind::Unfolded { id, path } => { - let mut ret = self.root.get_waypoints(&path); - ret.push(id.to_string()); - ret - } - } - } - - /// Toggles the state of the current node and updates the cursor position accordingly. - pub fn toggle(&mut self) { - let path = match self.cursor.contents()[self.position()].clone() { - Kind::Folded { path, .. } => path, - Kind::Unfolded { path, .. } => path, - }; - self.root.toggle(&path); - self.cursor = Cursor::new(self.root.flatten_visibles(), self.position(), false); - } - - /// Moves the cursor backward in the tree, if possible. - /// - /// Returns `true` if the cursor was successfully moved backward, `false` otherwise. - pub fn backward(&mut self) -> bool { - self.cursor.backward() - } - - /// Moves the cursor forward in the tree, if possible. - /// - /// Returns `true` if the cursor was successfully moved forward, `false` otherwise. - pub fn forward(&mut self) -> bool { - self.cursor.forward() - } - - /// Moves the cursor to the head of the tree. - pub fn move_to_head(&mut self) { - self.cursor.move_to_head() - } - - /// Moves the cursor to the tail of the tree. - pub fn move_to_tail(&mut self) { - self.cursor.move_to_tail() - } -} diff --git a/promkit-widgets/src/tree/treez.rs b/promkit-widgets/src/tree/treez.rs new file mode 100644 index 00000000..0242b0a9 --- /dev/null +++ b/promkit-widgets/src/tree/treez.rs @@ -0,0 +1,367 @@ +use std::{fs, path}; + +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct Row { + pub depth: usize, + pub id: String, + pub path: Vec, + pub has_children: bool, + pub collapsed: bool, +} + +pub trait RowOperation { + type Row; + + fn up(&self, current: usize) -> usize; + fn head(&self) -> usize; + fn down(&self, current: usize) -> usize; + fn tail(&self) -> usize; + fn toggle(&mut self, current: usize) -> usize; + fn set_rows_visibility(&mut self, collapsed: bool); + fn extract(&self, current: usize, n: usize) -> Vec; +} + +fn collect_rows_with( + input: &T, + depth: usize, + current_path: &mut Vec, + rows: &mut Vec, + id_of: &FId, + children_of: &FChildren, +) -> Result<(), E> +where + FId: Fn(&T) -> Result, + FChildren: Fn(&T) -> Result, E>, +{ + let id = id_of(input)?; + let children = children_of(input)?; + let has_children = !children.is_empty(); + + current_path.push(id.clone()); + rows.push(Row { + depth, + id, + path: current_path.clone(), + has_children, + collapsed: has_children, + }); + + if has_children { + for child in &children { + collect_rows_with(child, depth + 1, current_path, rows, id_of, children_of)?; + } + } + + current_path.pop(); + Ok(()) +} + +pub fn create_rows( + root: &T, + id_of: FId, + children_of: FChildren, +) -> Result, E> +where + FId: Fn(&T) -> Result, + FChildren: Fn(&T) -> Result, E>, +{ + let mut rows = Vec::new(); + let mut current_path = Vec::new(); + collect_rows_with(root, 0, &mut current_path, &mut rows, &id_of, &children_of)?; + Ok(rows) +} + +fn path_id(input: &path::Path) -> anyhow::Result { + input + .file_name() + .and_then(|name| name.to_str()) + .map(ToOwned::to_owned) + .or_else(|| { + let rendered = input.display().to_string(); + (!rendered.is_empty()).then_some(rendered) + }) + .ok_or_else(|| anyhow::anyhow!("Failed to convert path to string")) +} + +pub fn create_rows_from_path(input: &path::Path) -> anyhow::Result> { + create_rows( + &input.to_path_buf(), + |path: &path::PathBuf| path_id(path), + |path| { + if !path.is_dir() { + return Ok(Vec::new()); + } + + let mut directories = Vec::new(); + let mut files = Vec::new(); + + for entry in fs::read_dir(path)? { + let path = entry?.path(); + if path.is_dir() { + directories.push(path); + } else if path.is_file() { + files.push(path); + } + } + + directories.sort_by(|a, b| a.file_name().cmp(&b.file_name())); + files.sort_by(|a, b| a.file_name().cmp(&b.file_name())); + directories.extend(files); + + Ok(directories) + }, + ) +} + +fn is_visible(rows: &[Row], index: usize) -> bool { + if index >= rows.len() { + return false; + } + + let mut ancestor_depth = rows[index].depth; + for row in rows[..index].iter().rev() { + if row.depth < ancestor_depth { + if row.has_children && row.collapsed { + return false; + } + ancestor_depth = row.depth; + if ancestor_depth == 0 { + break; + } + } + } + true +} + +impl RowOperation for Vec { + type Row = Row; + + fn up(&self, current: usize) -> usize { + if self.is_empty() || current == 0 { + return 0; + } + + let mut prev = current - 1; + loop { + if is_visible(self, prev) { + return prev; + } + if prev == 0 { + return current; + } + prev -= 1; + } + } + + fn head(&self) -> usize { + self.iter() + .enumerate() + .find_map(|(index, _)| is_visible(self, index).then_some(index)) + .unwrap_or(0) + } + + fn down(&self, current: usize) -> usize { + if self.is_empty() || current >= self.len().saturating_sub(1) { + return current; + } + + let mut next = current + 1; + while next < self.len() { + if is_visible(self, next) { + return next; + } + next += 1; + } + current + } + + fn tail(&self) -> usize { + self.iter() + .enumerate() + .rev() + .find_map(|(index, _)| is_visible(self, index).then_some(index)) + .unwrap_or(0) + } + + fn toggle(&mut self, current: usize) -> usize { + let Some(row) = self.get_mut(current) else { + return current; + }; + if row.has_children { + row.collapsed = !row.collapsed; + } + current + } + + fn set_rows_visibility(&mut self, collapsed: bool) { + for row in self.iter_mut() { + if row.has_children { + row.collapsed = collapsed; + } + } + } + + fn extract(&self, current: usize, n: usize) -> Vec { + let mut result = Vec::new(); + let mut index = current; + + while index < self.len() && result.len() < n { + if is_visible(self, index) { + result.push(self[index].clone()); + } + index += 1; + } + + result + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[derive(Clone)] + struct TestNode { + id: &'static str, + children: Vec, + } + + fn create_test_rows() -> Vec { + vec![ + Row { + depth: 0, + id: "root".into(), + path: vec!["root".into()], + has_children: true, + collapsed: false, + }, + Row { + depth: 1, + id: "a".into(), + path: vec!["root".into(), "a".into()], + has_children: true, + collapsed: false, + }, + Row { + depth: 2, + id: "aa".into(), + path: vec!["root".into(), "a".into(), "aa".into()], + has_children: false, + collapsed: false, + }, + Row { + depth: 2, + id: "ab".into(), + path: vec!["root".into(), "a".into(), "ab".into()], + has_children: false, + collapsed: false, + }, + Row { + depth: 1, + id: "b".into(), + path: vec!["root".into(), "b".into()], + has_children: false, + collapsed: false, + }, + ] + } + + #[test] + fn extract_skips_hidden_descendants() { + let mut rows = create_test_rows(); + rows[0].collapsed = true; + + assert_eq!( + rows.extract(0, 5), + vec![Row { + depth: 0, + id: "root".into(), + path: vec!["root".into()], + has_children: true, + collapsed: true, + }] + ); + } + + #[test] + fn down_skips_hidden_descendants() { + let mut rows = create_test_rows(); + rows[1].collapsed = true; + + assert_eq!(rows.down(1), 4); + } + + #[test] + fn create_rows_is_generic() { + let root = TestNode { + id: "root", + children: vec![ + TestNode { + id: "a", + children: vec![ + TestNode { + id: "aa", + children: vec![], + }, + TestNode { + id: "ab", + children: vec![], + }, + ], + }, + TestNode { + id: "b", + children: vec![], + }, + ], + }; + + let rows = create_rows( + &root, + |node| Ok::<_, std::convert::Infallible>(node.id.to_string()), + |node| Ok::<_, std::convert::Infallible>(node.children.clone()), + ) + .unwrap(); + + assert_eq!( + rows, + vec![ + Row { + depth: 0, + id: "root".into(), + path: vec!["root".into()], + has_children: true, + collapsed: true, + }, + Row { + depth: 1, + id: "a".into(), + path: vec!["root".into(), "a".into()], + has_children: true, + collapsed: true, + }, + Row { + depth: 2, + id: "aa".into(), + path: vec!["root".into(), "a".into(), "aa".into()], + has_children: false, + collapsed: false, + }, + Row { + depth: 2, + id: "ab".into(), + path: vec!["root".into(), "a".into(), "ab".into()], + has_children: false, + collapsed: false, + }, + Row { + depth: 1, + id: "b".into(), + path: vec!["root".into(), "b".into()], + has_children: false, + collapsed: false, + }, + ] + ); + } +} diff --git a/promkit/src/preset/tree.rs b/promkit/src/preset/tree.rs index 3550e366..c8e008c8 100644 --- a/promkit/src/preset/tree.rs +++ b/promkit/src/preset/tree.rs @@ -13,7 +13,7 @@ use crate::{ preset::Evaluator, widgets::{ text::{self, Text}, - tree::{self, config::Config, node::Node}, + tree::{self, config::Config, Document}, }, Signal, }; @@ -67,13 +67,13 @@ impl crate::Prompt for Tree { type Return = Vec; fn finalize(&mut self) -> anyhow::Result { - Ok(self.tree.tree.get()) + Ok(self.tree.document.get()) } } impl Tree { - /// Creates a new `Tree` instance with the specified root node. - pub fn new(root: Node) -> Self { + /// Creates a new `Tree` instance with the specified tree document. + pub fn new(document: Document) -> Self { Self { renderer: None, evaluator: |event, ctx| Box::pin(evaluate::default(event, ctx)), @@ -88,7 +88,7 @@ impl Tree { ..Default::default() }, tree: tree::State { - tree: tree::Tree::new(root), + document, config: Config { folded_symbol: String::from("▶︎ "), unfolded_symbol: String::from("▼ "), diff --git a/promkit/src/preset/tree/evaluate.rs b/promkit/src/preset/tree/evaluate.rs index ce26326f..070629d9 100644 --- a/promkit/src/preset/tree/evaluate.rs +++ b/promkit/src/preset/tree/evaluate.rs @@ -44,7 +44,7 @@ pub async fn default(event: &Event, ctx: &mut Tree) -> anyhow::Result { kind: KeyEventKind::Press, state: KeyEventState::NONE, }) => { - ctx.tree.tree.backward(); + ctx.tree.document.up(); } Event::Mouse(MouseEvent { kind: MouseEventKind::ScrollUp, @@ -52,7 +52,7 @@ pub async fn default(event: &Event, ctx: &mut Tree) -> anyhow::Result { row: _, modifiers: KeyModifiers::NONE, }) => { - ctx.tree.tree.backward(); + ctx.tree.document.up(); } Event::Key(KeyEvent { @@ -61,7 +61,7 @@ pub async fn default(event: &Event, ctx: &mut Tree) -> anyhow::Result { kind: KeyEventKind::Press, state: KeyEventState::NONE, }) => { - ctx.tree.tree.forward(); + ctx.tree.document.down(); } Event::Mouse(MouseEvent { kind: MouseEventKind::ScrollDown, @@ -69,7 +69,7 @@ pub async fn default(event: &Event, ctx: &mut Tree) -> anyhow::Result { row: _, modifiers: KeyModifiers::NONE, }) => { - ctx.tree.tree.forward(); + ctx.tree.document.down(); } // Fold/Unfold @@ -79,7 +79,7 @@ pub async fn default(event: &Event, ctx: &mut Tree) -> anyhow::Result { kind: KeyEventKind::Press, state: KeyEventState::NONE, }) => { - ctx.tree.tree.toggle(); + ctx.tree.document.toggle(); } _ => (), From e5d10eab4be565d758e881145ff7674ec610c20d Mon Sep 17 00:00:00 2001 From: ynqa Date: Sun, 12 Apr 2026 15:52:14 +0900 Subject: [PATCH 2/7] chore: move tree widget into structured --- examples/tree/src/tree.rs | 2 +- promkit-widgets/src/lib.rs | 6 +----- promkit-widgets/src/structured/mod.rs | 4 ++++ promkit-widgets/src/{ => structured}/tree.rs | 7 ------- promkit-widgets/src/{ => structured}/tree/config.rs | 0 promkit-widgets/src/{ => structured}/tree/document.rs | 0 promkit-widgets/src/{ => structured}/tree/treez.rs | 0 promkit/src/preset/tree.rs | 2 +- 8 files changed, 7 insertions(+), 14 deletions(-) rename promkit-widgets/src/{ => structured}/tree.rs (81%) rename promkit-widgets/src/{ => structured}/tree/config.rs (100%) rename promkit-widgets/src/{ => structured}/tree/document.rs (100%) rename promkit-widgets/src/{ => structured}/tree/treez.rs (100%) diff --git a/examples/tree/src/tree.rs b/examples/tree/src/tree.rs index 5cc25f53..694be710 100644 --- a/examples/tree/src/tree.rs +++ b/examples/tree/src/tree.rs @@ -1,6 +1,6 @@ use std::path::Path; -use promkit::{preset::tree::Tree, widgets::tree::Document, Prompt}; +use promkit::{preset::tree::Tree, widgets::structured::tree::Document, Prompt}; #[tokio::main] async fn main() -> anyhow::Result<()> { diff --git a/promkit-widgets/src/lib.rs b/promkit-widgets/src/lib.rs index 84e6ef61..6b67a3f7 100644 --- a/promkit-widgets/src/lib.rs +++ b/promkit-widgets/src/lib.rs @@ -8,7 +8,7 @@ pub mod cursor; #[cfg_attr(docsrs, doc(cfg(feature = "checkbox")))] pub mod checkbox; -#[cfg(any(feature = "json", feature = "yaml"))] +#[cfg(any(feature = "json", feature = "yaml", feature = "tree"))] pub mod structured; #[cfg(feature = "json")] @@ -41,10 +41,6 @@ pub mod status; #[cfg_attr(docsrs, doc(cfg(feature = "texteditor")))] pub mod text_editor; -#[cfg(feature = "tree")] -#[cfg_attr(docsrs, doc(cfg(feature = "tree")))] -pub mod tree; - #[cfg(feature = "spinner")] #[cfg_attr(docsrs, doc(cfg(feature = "spinner")))] pub mod spinner; diff --git a/promkit-widgets/src/structured/mod.rs b/promkit-widgets/src/structured/mod.rs index ab708d4b..1b25691f 100644 --- a/promkit-widgets/src/structured/mod.rs +++ b/promkit-widgets/src/structured/mod.rs @@ -6,6 +6,10 @@ pub mod json; #[cfg_attr(docsrs, doc(cfg(feature = "yaml")))] pub mod yaml; +#[cfg(feature = "tree")] +#[cfg_attr(docsrs, doc(cfg(feature = "tree")))] +pub mod tree; + /// Container type of structured widget. #[derive(Clone, Debug, PartialEq)] pub enum ContainerType { diff --git a/promkit-widgets/src/tree.rs b/promkit-widgets/src/structured/tree.rs similarity index 81% rename from promkit-widgets/src/tree.rs rename to promkit-widgets/src/structured/tree.rs index 8fad2f62..0ae0713c 100644 --- a/promkit-widgets/src/tree.rs +++ b/promkit-widgets/src/structured/tree.rs @@ -8,16 +8,9 @@ pub mod treez; pub use treez::Row; /// Represents the state of a tree structure within the application. -/// -/// This state includes not only the tree itself but also various properties -/// that affect how the tree is displayed and interacted with. These properties -/// include symbols for folded and unfolded items, styles for active and inactive -/// items, the number of lines available for rendering, and the indentation level -/// for child items in the tree. #[derive(Clone)] pub struct State { pub document: Document, - /// Configuration for rendering and behavior. pub config: Config, } diff --git a/promkit-widgets/src/tree/config.rs b/promkit-widgets/src/structured/tree/config.rs similarity index 100% rename from promkit-widgets/src/tree/config.rs rename to promkit-widgets/src/structured/tree/config.rs diff --git a/promkit-widgets/src/tree/document.rs b/promkit-widgets/src/structured/tree/document.rs similarity index 100% rename from promkit-widgets/src/tree/document.rs rename to promkit-widgets/src/structured/tree/document.rs diff --git a/promkit-widgets/src/tree/treez.rs b/promkit-widgets/src/structured/tree/treez.rs similarity index 100% rename from promkit-widgets/src/tree/treez.rs rename to promkit-widgets/src/structured/tree/treez.rs diff --git a/promkit/src/preset/tree.rs b/promkit/src/preset/tree.rs index c8e008c8..1cb743f1 100644 --- a/promkit/src/preset/tree.rs +++ b/promkit/src/preset/tree.rs @@ -12,8 +12,8 @@ use crate::{ }, preset::Evaluator, widgets::{ + structured::tree::{self, Document, config::Config}, text::{self, Text}, - tree::{self, config::Config, Document}, }, Signal, }; From 3590a52f4626b34937c99988a0586a7b9281f16a Mon Sep 17 00:00:00 2001 From: ynqa Date: Sun, 12 Apr 2026 15:58:33 +0900 Subject: [PATCH 3/7] chore: use RowOperation instead of definition --- promkit-widgets/src/structured/tree/document.rs | 4 +++- promkit-widgets/src/structured/tree/treez.rs | 14 ++------------ 2 files changed, 5 insertions(+), 13 deletions(-) diff --git a/promkit-widgets/src/structured/tree/document.rs b/promkit-widgets/src/structured/tree/document.rs index 76e2e615..0645c976 100644 --- a/promkit-widgets/src/structured/tree/document.rs +++ b/promkit-widgets/src/structured/tree/document.rs @@ -1,4 +1,6 @@ -use super::{Row, treez::RowOperation}; +use crate::structured::RowOperation; + +use super::Row; /// Represents a navigable tree document, allowing for efficient row navigation and folding. #[derive(Clone)] diff --git a/promkit-widgets/src/structured/tree/treez.rs b/promkit-widgets/src/structured/tree/treez.rs index 0242b0a9..f7a1f884 100644 --- a/promkit-widgets/src/structured/tree/treez.rs +++ b/promkit-widgets/src/structured/tree/treez.rs @@ -1,5 +1,7 @@ use std::{fs, path}; +use crate::structured::RowOperation; + #[derive(Clone, Debug, PartialEq, Eq)] pub struct Row { pub depth: usize, @@ -9,18 +11,6 @@ pub struct Row { pub collapsed: bool, } -pub trait RowOperation { - type Row; - - fn up(&self, current: usize) -> usize; - fn head(&self) -> usize; - fn down(&self, current: usize) -> usize; - fn tail(&self) -> usize; - fn toggle(&mut self, current: usize) -> usize; - fn set_rows_visibility(&mut self, collapsed: bool); - fn extract(&self, current: usize, n: usize) -> Vec; -} - fn collect_rows_with( input: &T, depth: usize, From 61acd02f788699cc5acc917326ecb44281461f38 Mon Sep 17 00:00:00 2001 From: ynqa Date: Sun, 12 Apr 2026 17:09:24 +0900 Subject: [PATCH 4/7] chore: define adapter for tree construction --- promkit-widgets/src/structured/tree.rs | 2 + .../src/structured/tree/document.rs | 2 +- promkit-widgets/src/structured/tree/path.rs | 49 ++++++++++ promkit-widgets/src/structured/tree/treez.rs | 98 +++++++------------ 4 files changed, 86 insertions(+), 65 deletions(-) create mode 100644 promkit-widgets/src/structured/tree/path.rs diff --git a/promkit-widgets/src/structured/tree.rs b/promkit-widgets/src/structured/tree.rs index 0ae0713c..f725a3f1 100644 --- a/promkit-widgets/src/structured/tree.rs +++ b/promkit-widgets/src/structured/tree.rs @@ -4,6 +4,8 @@ mod document; pub use document::Document; pub mod config; pub use config::Config; +pub mod path; +pub use path::create_rows_from_path; pub mod treez; pub use treez::Row; diff --git a/promkit-widgets/src/structured/tree/document.rs b/promkit-widgets/src/structured/tree/document.rs index 0645c976..592d910b 100644 --- a/promkit-widgets/src/structured/tree/document.rs +++ b/promkit-widgets/src/structured/tree/document.rs @@ -15,7 +15,7 @@ impl Document { } pub fn from_path(path: &std::path::Path) -> anyhow::Result { - Ok(Self::new(super::treez::create_rows_from_path(path)?)) + Ok(Self::new(super::create_rows_from_path(path)?)) } } diff --git a/promkit-widgets/src/structured/tree/path.rs b/promkit-widgets/src/structured/tree/path.rs new file mode 100644 index 00000000..d801e0ce --- /dev/null +++ b/promkit-widgets/src/structured/tree/path.rs @@ -0,0 +1,49 @@ +use std::{fs, path}; + +use super::{Row, treez::{Adapter, create_rows}}; + +pub struct PathAdapter; + +impl Adapter for PathAdapter { + type Node = path::PathBuf; + type Error = anyhow::Error; + + fn id_of(&self, node: &Self::Node) -> Result { + node.file_name() + .and_then(|name| name.to_str()) + .map(ToOwned::to_owned) + .or_else(|| { + let rendered = node.display().to_string(); + (!rendered.is_empty()).then_some(rendered) + }) + .ok_or_else(|| anyhow::anyhow!("Failed to convert path to string")) + } + + fn children_of(&self, node: &Self::Node) -> Result, Self::Error> { + if !node.is_dir() { + return Ok(Vec::new()); + } + + let mut directories = Vec::new(); + let mut files = Vec::new(); + + for entry in fs::read_dir(node)? { + let path = entry?.path(); + if path.is_dir() { + directories.push(path); + } else if path.is_file() { + files.push(path); + } + } + + directories.sort_by(|a, b| a.file_name().cmp(&b.file_name())); + files.sort_by(|a, b| a.file_name().cmp(&b.file_name())); + directories.extend(files); + + Ok(directories) + } +} + +pub fn create_rows_from_path(input: &path::Path) -> anyhow::Result> { + create_rows(&input.to_path_buf(), &PathAdapter) +} diff --git a/promkit-widgets/src/structured/tree/treez.rs b/promkit-widgets/src/structured/tree/treez.rs index f7a1f884..d15a2899 100644 --- a/promkit-widgets/src/structured/tree/treez.rs +++ b/promkit-widgets/src/structured/tree/treez.rs @@ -1,5 +1,3 @@ -use std::{fs, path}; - use crate::structured::RowOperation; #[derive(Clone, Debug, PartialEq, Eq)] @@ -11,20 +9,26 @@ pub struct Row { pub collapsed: bool, } -fn collect_rows_with( +pub trait Adapter { + type Node; + type Error; + + fn id_of(&self, node: &Self::Node) -> Result; + fn children_of(&self, node: &Self::Node) -> Result, Self::Error>; +} + +fn collect_rows_with( input: &T, depth: usize, current_path: &mut Vec, rows: &mut Vec, - id_of: &FId, - children_of: &FChildren, + adapter: &A, ) -> Result<(), E> where - FId: Fn(&T) -> Result, - FChildren: Fn(&T) -> Result, E>, + A: Adapter, { - let id = id_of(input)?; - let children = children_of(input)?; + let id = adapter.id_of(input)?; + let children = adapter.children_of(input)?; let has_children = !children.is_empty(); current_path.push(id.clone()); @@ -38,7 +42,7 @@ where if has_children { for child in &children { - collect_rows_with(child, depth + 1, current_path, rows, id_of, children_of)?; + collect_rows_with(child, depth + 1, current_path, rows, adapter)?; } } @@ -46,63 +50,19 @@ where Ok(()) } -pub fn create_rows( +pub fn create_rows( root: &T, - id_of: FId, - children_of: FChildren, + adapter: &A, ) -> Result, E> where - FId: Fn(&T) -> Result, - FChildren: Fn(&T) -> Result, E>, + A: Adapter, { let mut rows = Vec::new(); let mut current_path = Vec::new(); - collect_rows_with(root, 0, &mut current_path, &mut rows, &id_of, &children_of)?; + collect_rows_with(root, 0, &mut current_path, &mut rows, adapter)?; Ok(rows) } -fn path_id(input: &path::Path) -> anyhow::Result { - input - .file_name() - .and_then(|name| name.to_str()) - .map(ToOwned::to_owned) - .or_else(|| { - let rendered = input.display().to_string(); - (!rendered.is_empty()).then_some(rendered) - }) - .ok_or_else(|| anyhow::anyhow!("Failed to convert path to string")) -} - -pub fn create_rows_from_path(input: &path::Path) -> anyhow::Result> { - create_rows( - &input.to_path_buf(), - |path: &path::PathBuf| path_id(path), - |path| { - if !path.is_dir() { - return Ok(Vec::new()); - } - - let mut directories = Vec::new(); - let mut files = Vec::new(); - - for entry in fs::read_dir(path)? { - let path = entry?.path(); - if path.is_dir() { - directories.push(path); - } else if path.is_file() { - files.push(path); - } - } - - directories.sort_by(|a, b| a.file_name().cmp(&b.file_name())); - files.sort_by(|a, b| a.file_name().cmp(&b.file_name())); - directories.extend(files); - - Ok(directories) - }, - ) -} - fn is_visible(rows: &[Row], index: usize) -> bool { if index >= rows.len() { return false; @@ -216,6 +176,21 @@ mod tests { children: Vec, } + struct TestAdapter; + + impl Adapter for TestAdapter { + type Node = TestNode; + type Error = std::convert::Infallible; + + fn id_of(&self, node: &Self::Node) -> Result { + Ok(node.id.to_string()) + } + + fn children_of(&self, node: &Self::Node) -> Result, Self::Error> { + Ok(node.children.clone()) + } + } + fn create_test_rows() -> Vec { vec![ Row { @@ -306,12 +281,7 @@ mod tests { ], }; - let rows = create_rows( - &root, - |node| Ok::<_, std::convert::Infallible>(node.id.to_string()), - |node| Ok::<_, std::convert::Infallible>(node.children.clone()), - ) - .unwrap(); + let rows = create_rows(&root, &TestAdapter).unwrap(); assert_eq!( rows, From 582557c4fbf0ce08e8821f2c1d1bd6d970ad7d4b Mon Sep 17 00:00:00 2001 From: ynqa Date: Sun, 12 Apr 2026 17:18:03 +0900 Subject: [PATCH 5/7] docs: tree --- promkit-widgets/src/structured/tree/treez.rs | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/promkit-widgets/src/structured/tree/treez.rs b/promkit-widgets/src/structured/tree/treez.rs index d15a2899..19ba9617 100644 --- a/promkit-widgets/src/structured/tree/treez.rs +++ b/promkit-widgets/src/structured/tree/treez.rs @@ -1,19 +1,30 @@ use crate::structured::RowOperation; +/// A single visible-or-collapsible row in a tree view. #[derive(Clone, Debug, PartialEq, Eq)] pub struct Row { + /// Depth from the root node. Used for indentation when rendering. pub depth: usize, + /// Display label of the current node. pub id: String, + /// Breadcrumb-like labels from the root to this node. pub path: Vec, + /// Whether this row has child nodes. pub has_children: bool, + /// Whether the children of this row are currently collapsed. pub collapsed: bool, } +/// Adapts an arbitrary tree-shaped data source into rows. pub trait Adapter { + /// Input node type used by the adapted tree source. type Node; + /// Error returned while reading node metadata or children. type Error; + /// Returns the display label for the given node. fn id_of(&self, node: &Self::Node) -> Result; + /// Returns the direct children of the given node. fn children_of(&self, node: &Self::Node) -> Result, Self::Error>; } @@ -50,6 +61,10 @@ where Ok(()) } +/// Creates tree rows from an arbitrary tree source via an [`Adapter`]. +/// +/// Parent rows are emitted before their descendants, and rows with children +/// start in the collapsed state by default. pub fn create_rows( root: &T, adapter: &A, From eb899c3d686fe3b291f858c4ff39fc32fbb1348b Mon Sep 17 00:00:00 2001 From: ynqa Date: Mon, 13 Apr 2026 20:33:22 +0900 Subject: [PATCH 6/7] cargo fmt/clippy --- promkit-widgets/src/structured/tree/path.rs | 5 ++++- promkit-widgets/src/structured/tree/treez.rs | 5 +---- promkit/src/preset/tree.rs | 2 +- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/promkit-widgets/src/structured/tree/path.rs b/promkit-widgets/src/structured/tree/path.rs index d801e0ce..f2af2259 100644 --- a/promkit-widgets/src/structured/tree/path.rs +++ b/promkit-widgets/src/structured/tree/path.rs @@ -1,6 +1,9 @@ use std::{fs, path}; -use super::{Row, treez::{Adapter, create_rows}}; +use super::{ + Row, + treez::{Adapter, create_rows}, +}; pub struct PathAdapter; diff --git a/promkit-widgets/src/structured/tree/treez.rs b/promkit-widgets/src/structured/tree/treez.rs index 19ba9617..fc540b7f 100644 --- a/promkit-widgets/src/structured/tree/treez.rs +++ b/promkit-widgets/src/structured/tree/treez.rs @@ -65,10 +65,7 @@ where /// /// Parent rows are emitted before their descendants, and rows with children /// start in the collapsed state by default. -pub fn create_rows( - root: &T, - adapter: &A, -) -> Result, E> +pub fn create_rows(root: &T, adapter: &A) -> Result, E> where A: Adapter, { diff --git a/promkit/src/preset/tree.rs b/promkit/src/preset/tree.rs index 1cb743f1..312836d1 100644 --- a/promkit/src/preset/tree.rs +++ b/promkit/src/preset/tree.rs @@ -12,7 +12,7 @@ use crate::{ }, preset::Evaluator, widgets::{ - structured::tree::{self, Document, config::Config}, + structured::tree::{self, config::Config, Document}, text::{self, Text}, }, Signal, From 7143156de239a977dff72a633b5512071fd8fe05 Mon Sep 17 00:00:00 2001 From: ynqa Date: Mon, 13 Apr 2026 21:31:12 +0900 Subject: [PATCH 7/7] chore: create_rows into adapter --- promkit-widgets/src/structured/tree.rs | 1 - .../src/structured/tree/document.rs | 4 +-- promkit-widgets/src/structured/tree/path.rs | 9 +----- promkit-widgets/src/structured/tree/treez.rs | 29 +++++++++---------- 4 files changed, 16 insertions(+), 27 deletions(-) diff --git a/promkit-widgets/src/structured/tree.rs b/promkit-widgets/src/structured/tree.rs index f725a3f1..86271f7d 100644 --- a/promkit-widgets/src/structured/tree.rs +++ b/promkit-widgets/src/structured/tree.rs @@ -5,7 +5,6 @@ pub use document::Document; pub mod config; pub use config::Config; pub mod path; -pub use path::create_rows_from_path; pub mod treez; pub use treez::Row; diff --git a/promkit-widgets/src/structured/tree/document.rs b/promkit-widgets/src/structured/tree/document.rs index 592d910b..29aa0625 100644 --- a/promkit-widgets/src/structured/tree/document.rs +++ b/promkit-widgets/src/structured/tree/document.rs @@ -1,6 +1,6 @@ use crate::structured::RowOperation; -use super::Row; +use super::{Row, path::PathAdapter, treez::Adapter}; /// Represents a navigable tree document, allowing for efficient row navigation and folding. #[derive(Clone)] @@ -15,7 +15,7 @@ impl Document { } pub fn from_path(path: &std::path::Path) -> anyhow::Result { - Ok(Self::new(super::create_rows_from_path(path)?)) + Ok(Self::new(PathAdapter.create_rows(&path.to_path_buf())?)) } } diff --git a/promkit-widgets/src/structured/tree/path.rs b/promkit-widgets/src/structured/tree/path.rs index f2af2259..66178145 100644 --- a/promkit-widgets/src/structured/tree/path.rs +++ b/promkit-widgets/src/structured/tree/path.rs @@ -1,9 +1,6 @@ use std::{fs, path}; -use super::{ - Row, - treez::{Adapter, create_rows}, -}; +use super::treez::Adapter; pub struct PathAdapter; @@ -46,7 +43,3 @@ impl Adapter for PathAdapter { Ok(directories) } } - -pub fn create_rows_from_path(input: &path::Path) -> anyhow::Result> { - create_rows(&input.to_path_buf(), &PathAdapter) -} diff --git a/promkit-widgets/src/structured/tree/treez.rs b/promkit-widgets/src/structured/tree/treez.rs index fc540b7f..fcfe6e53 100644 --- a/promkit-widgets/src/structured/tree/treez.rs +++ b/promkit-widgets/src/structured/tree/treez.rs @@ -26,6 +26,17 @@ pub trait Adapter { fn id_of(&self, node: &Self::Node) -> Result; /// Returns the direct children of the given node. fn children_of(&self, node: &Self::Node) -> Result, Self::Error>; + + /// Creates tree rows from the given root node. + /// + /// Parent rows are emitted before their descendants, and rows with children + /// start in the collapsed state by default. + fn create_rows(&self, root: &Self::Node) -> Result, Self::Error> { + let mut rows = Vec::new(); + let mut current_path = Vec::new(); + collect_rows_with(root, 0, &mut current_path, &mut rows, self)?; + Ok(rows) + } } fn collect_rows_with( @@ -36,7 +47,7 @@ fn collect_rows_with( adapter: &A, ) -> Result<(), E> where - A: Adapter, + A: Adapter + ?Sized, { let id = adapter.id_of(input)?; let children = adapter.children_of(input)?; @@ -61,20 +72,6 @@ where Ok(()) } -/// Creates tree rows from an arbitrary tree source via an [`Adapter`]. -/// -/// Parent rows are emitted before their descendants, and rows with children -/// start in the collapsed state by default. -pub fn create_rows(root: &T, adapter: &A) -> Result, E> -where - A: Adapter, -{ - let mut rows = Vec::new(); - let mut current_path = Vec::new(); - collect_rows_with(root, 0, &mut current_path, &mut rows, adapter)?; - Ok(rows) -} - fn is_visible(rows: &[Row], index: usize) -> bool { if index >= rows.len() { return false; @@ -293,7 +290,7 @@ mod tests { ], }; - let rows = create_rows(&root, &TestAdapter).unwrap(); + let rows = TestAdapter.create_rows(&root).unwrap(); assert_eq!( rows,