|
1 | | -use std::{iter, path::PathBuf}; |
| 1 | +use std::{collections::VecDeque, iter, path::PathBuf}; |
2 | 2 |
|
3 | 3 | use tokio::sync::mpsc; |
4 | 4 | use tui_realm_treeview::{Node, NodeValue, TREE_CMD_CLOSE, TREE_CMD_OPEN, TREE_INITIAL_NODE, Tree, TreeView}; |
5 | 5 | use tuirealm::{ |
6 | | - AttrValue, Attribute, Component, Event, MockComponent, State, StateValue, |
| 6 | + AttrValue, Attribute, Component, Event, Frame, MockComponent, State, StateValue, |
7 | 7 | command::{Cmd, CmdResult, Direction, Position}, |
8 | | - event::{Key, KeyEvent, KeyModifiers}, |
| 8 | + event::{Key, KeyEvent, KeyModifiers, MouseButton, MouseEvent, MouseEventKind}, |
9 | 9 | props::{Alignment, BorderType, Borders, Color, Style}, |
| 10 | + ratatui::layout::{Position as RectPosition, Rect}, |
10 | 11 | }; |
11 | 12 |
|
12 | 13 | use crate::{ |
13 | 14 | app::{ |
14 | | - messages::{DownloadPopupMsg, Msg}, |
| 15 | + messages::{DownloadPopupMsg, MenuMsg, Msg}, |
15 | 16 | user_events::{FileEvent, UserEvent}, |
16 | 17 | }, |
17 | 18 | cscs::{api_client::types::PathType, ports::BackgroundTask}, |
@@ -45,10 +46,10 @@ impl NodeValue for FileNode { |
45 | 46 | } |
46 | 47 | } |
47 | 48 |
|
48 | | -#[derive(MockComponent)] |
49 | 49 | pub struct FileTree { |
50 | 50 | component: TreeView<FileNode>, |
51 | 51 | file_tree_tx: mpsc::Sender<BackgroundTask>, |
| 52 | + current_rect: Rect, |
52 | 53 | } |
53 | 54 | impl FileTree { |
54 | 55 | pub fn new(file_tree_tx: mpsc::Sender<BackgroundTask>) -> Self { |
@@ -81,29 +82,127 @@ impl FileTree { |
81 | 82 | .with_tree(tree) |
82 | 83 | .initial_node(root_node.id()), |
83 | 84 | file_tree_tx, |
| 85 | + current_rect: Rect::ZERO, |
| 86 | + } |
| 87 | + } |
| 88 | + fn node_list(&self) -> Vec<&String> { |
| 89 | + let root = self.component.tree().root(); |
| 90 | + let mut ids = vec![]; |
| 91 | + let mut stack = VecDeque::new(); |
| 92 | + stack.push_back(root.id()); |
| 93 | + |
| 94 | + while let Some(current_id) = stack.pop_front() { |
| 95 | + ids.push(current_id); |
| 96 | + let node = root.query(current_id).unwrap(); |
| 97 | + if !node.is_leaf() && self.component.tree_state().is_open(node) { |
| 98 | + for child in node.children().iter().rev() { |
| 99 | + stack.push_front(child.id()); |
| 100 | + } |
| 101 | + } |
| 102 | + } |
| 103 | + |
| 104 | + ids |
| 105 | + } |
| 106 | + |
| 107 | + fn open_current_node(&mut self) -> CmdResult { |
| 108 | + let current_id = self.state().unwrap_one().unwrap_string(); |
| 109 | + let node = self.component.tree().root().query(¤t_id).unwrap(); |
| 110 | + match node.value().path_type { |
| 111 | + PathType::Directory => { |
| 112 | + if node.children().is_empty() { |
| 113 | + // try loading children if there are none |
| 114 | + let tree_tx = self.file_tree_tx.clone(); |
| 115 | + tokio::spawn(async move { |
| 116 | + tree_tx |
| 117 | + .send(BackgroundTask::ListPaths(PathBuf::from(current_id))) |
| 118 | + .await |
| 119 | + .unwrap(); |
| 120 | + }); |
| 121 | + CmdResult::None |
| 122 | + } else { |
| 123 | + self.perform(Cmd::Custom(TREE_CMD_OPEN)) |
| 124 | + } |
| 125 | + } |
| 126 | + PathType::File => CmdResult::None, |
| 127 | + PathType::Link => CmdResult::None, |
| 128 | + } |
| 129 | + } |
| 130 | + |
| 131 | + fn close_current_node(&mut self) -> CmdResult { |
| 132 | + let current_id = self.state().unwrap_one().unwrap_string(); |
| 133 | + let node = self.component.tree().root().query(¤t_id).unwrap(); |
| 134 | + if self.component.tree_state().is_closed(node) { |
| 135 | + // current node is already closed, so we select and close the parent |
| 136 | + if let Some(parent) = self.component.tree().root().parent(node.id()) { |
| 137 | + self.attr( |
| 138 | + Attribute::Custom(TREE_INITIAL_NODE), |
| 139 | + AttrValue::String(parent.id().clone()), |
| 140 | + ); |
| 141 | + } |
| 142 | + } |
| 143 | + self.perform(Cmd::Custom(TREE_CMD_CLOSE)) |
| 144 | + } |
| 145 | + |
| 146 | + fn mouse_select_row(&mut self, row: u16) -> CmdResult { |
| 147 | + let mut list_index = (row - self.current_rect.y) as usize; |
| 148 | + list_index = list_index.saturating_sub(1); |
| 149 | + let render_area_h = self.current_rect.height as usize - 2; |
| 150 | + // adjust for border |
| 151 | + if list_index >= render_area_h { |
| 152 | + list_index = render_area_h - 1; |
| 153 | + } |
| 154 | + |
| 155 | + // the tree view auto-scrolls when selecting a node, we need to compensate for that in our |
| 156 | + // selection. See `calc_rows_to_skip` in `TreeWidget` for where this comes from. |
| 157 | + let nodes = self.node_list(); |
| 158 | + let offset_max = nodes.len().saturating_sub(render_area_h); |
| 159 | + let num_lines_to_show_at_top = render_area_h / 2; |
| 160 | + let root = self.component.tree().root().clone(); |
| 161 | + let prev = self.component.tree_state().selected().unwrap(); |
| 162 | + let prev_index = nodes.iter().position(|n| n == &&prev.to_string()).unwrap() + 1; |
| 163 | + let current_offset = prev_index.saturating_sub(num_lines_to_show_at_top).min(offset_max); |
| 164 | + list_index += current_offset; |
| 165 | + // current offset is how far the view is currently scrolled |
| 166 | + |
| 167 | + let selected = root.query(nodes[list_index]).unwrap(); |
| 168 | + if prev != selected.id() { |
| 169 | + self.attr( |
| 170 | + Attribute::Custom(TREE_INITIAL_NODE), |
| 171 | + AttrValue::String(selected.id().to_string()), |
| 172 | + ); |
| 173 | + } |
| 174 | + if self.component.tree_state().is_open(selected) { |
| 175 | + self.perform(Cmd::Custom(TREE_CMD_CLOSE)) |
| 176 | + } else { |
| 177 | + self.open_current_node() |
84 | 178 | } |
85 | 179 | } |
86 | 180 | } |
| 181 | +impl MockComponent for FileTree { |
| 182 | + fn view(&mut self, frame: &mut Frame, area: Rect) { |
| 183 | + self.current_rect = area; |
| 184 | + self.component.view(frame, area); |
| 185 | + } |
| 186 | + fn query(&self, attr: Attribute) -> Option<AttrValue> { |
| 187 | + self.component.query(attr) |
| 188 | + } |
| 189 | + fn attr(&mut self, query: Attribute, attr: AttrValue) { |
| 190 | + self.component.attr(query, attr) |
| 191 | + } |
| 192 | + fn state(&self) -> State { |
| 193 | + self.component.state() |
| 194 | + } |
| 195 | + fn perform(&mut self, cmd: Cmd) -> CmdResult { |
| 196 | + self.component.perform(cmd) |
| 197 | + } |
| 198 | +} |
87 | 199 | impl Component<Msg, UserEvent> for FileTree { |
88 | 200 | fn on(&mut self, ev: Event<UserEvent>) -> Option<Msg> { |
89 | 201 | match ev { |
90 | 202 | Event::Keyboard(KeyEvent { |
91 | 203 | code: Key::Left, |
92 | 204 | modifiers: KeyModifiers::NONE, |
93 | | - }) => { |
94 | | - let current_id = self.state().unwrap_one().unwrap_string(); |
95 | | - let node = self.component.tree().root().query(¤t_id).unwrap(); |
96 | | - if self.component.tree_state().is_closed(node) { |
97 | | - // current node is already closed, so we select and close the parent |
98 | | - if let Some(parent) = self.component.tree().root().parent(node.id()) { |
99 | | - self.attr( |
100 | | - Attribute::Custom(TREE_INITIAL_NODE), |
101 | | - AttrValue::String(parent.id().clone()), |
102 | | - ); |
103 | | - } |
104 | | - } |
105 | | - self.perform(Cmd::Custom(TREE_CMD_CLOSE)) |
106 | | - } |
| 205 | + }) => self.close_current_node(), |
107 | 206 | Event::Keyboard(KeyEvent { |
108 | 207 | code: Key::Right, |
109 | 208 | modifiers: KeyModifiers::NONE, |
@@ -154,6 +253,25 @@ impl Component<Msg, UserEvent> for FileTree { |
154 | 253 | code: Key::End, |
155 | 254 | modifiers: KeyModifiers::NONE, |
156 | 255 | }) => self.perform(Cmd::GoTo(Position::End)), |
| 256 | + |
| 257 | + Event::Mouse(MouseEvent { |
| 258 | + kind, column: col, row, .. |
| 259 | + }) => { |
| 260 | + if !self.current_rect.contains(RectPosition { x: col, y: row }) { |
| 261 | + CmdResult::None |
| 262 | + } else { |
| 263 | + match kind { |
| 264 | + MouseEventKind::Down(MouseButton::Left) => self.mouse_select_row(row), |
| 265 | + MouseEventKind::Down(MouseButton::Right) => { |
| 266 | + self.mouse_select_row(row); |
| 267 | + return Some(Msg::Menu(MenuMsg::Opened)); |
| 268 | + } |
| 269 | + MouseEventKind::ScrollDown => self.perform(Cmd::Scroll(Direction::Down)), |
| 270 | + MouseEventKind::ScrollUp => self.perform(Cmd::Scroll(Direction::Up)), |
| 271 | + _ => CmdResult::None, |
| 272 | + } |
| 273 | + } |
| 274 | + } |
157 | 275 | Event::User(UserEvent::File(FileEvent::List(id, subpaths))) => { |
158 | 276 | let tree = self.component.tree_mut(); |
159 | 277 | let parent = tree.root_mut().query_mut(&id).unwrap(); |
|
0 commit comments