Skip to content

Commit 4e5da31

Browse files
committed
add mouse support
1 parent e30a0b5 commit 4e5da31

5 files changed

Lines changed: 316 additions & 41 deletions

File tree

coman/src/components/context_menu.rs

Lines changed: 53 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,21 @@
11
use tui_realm_stdlib::List;
22
use tuirealm::{
3-
AttrValue, Attribute, Component, Event, MockComponent, State, StateValue,
3+
AttrValue, Attribute, Component, Event, Frame, MockComponent, State, StateValue,
44
command::{Cmd, CmdResult, Direction, Position},
5-
event::{Key, KeyEvent},
5+
event::{Key, KeyEvent, MouseButton, MouseEvent, MouseEventKind},
66
props::{Alignment, BorderType, Borders, Color, Table, TableBuilder, TextSpan},
7+
ratatui::layout::{Position as RectPosition, Rect},
78
};
89

910
use crate::app::{
1011
messages::{MenuMsg, Msg, View},
1112
user_events::{FileEvent, JobEvent, UserEvent},
1213
};
1314

14-
#[derive(MockComponent)]
1515
pub struct ContextMenu {
1616
component: List,
1717
current_view: View,
18+
current_rect: Rect,
1819
}
1920

2021
impl ContextMenu {
@@ -27,7 +28,6 @@ impl ContextMenu {
2728
.add_col(TextSpan::from("Cancel Job").fg(Color::Cyan))
2829
.add_row()
2930
.add_col(TextSpan::from("Quit").fg(Color::Cyan))
30-
.add_row()
3131
.build()
3232
}
3333
fn workload_actions(index: usize) -> Option<Msg> {
@@ -50,7 +50,6 @@ impl ContextMenu {
5050
.add_col(TextSpan::from("Delete").fg(Color::Cyan))
5151
.add_row()
5252
.add_col(TextSpan::from("Quit").fg(Color::Cyan))
53-
.add_row()
5453
.build()
5554
}
5655
fn fileview_actions(index: usize) -> Option<Msg> {
@@ -82,10 +81,30 @@ impl ContextMenu {
8281
})
8382
.selected_line(0),
8483
current_view: view,
84+
current_rect: Rect::ZERO,
8585
}
8686
}
8787
}
8888

89+
impl MockComponent for ContextMenu {
90+
fn view(&mut self, frame: &mut Frame, area: Rect) {
91+
self.current_rect = area;
92+
self.component.view(frame, area);
93+
}
94+
fn query(&self, attr: Attribute) -> Option<AttrValue> {
95+
self.component.query(attr)
96+
}
97+
fn attr(&mut self, query: Attribute, attr: AttrValue) {
98+
self.component.attr(query, attr)
99+
}
100+
fn state(&self) -> State {
101+
self.component.state()
102+
}
103+
fn perform(&mut self, cmd: Cmd) -> CmdResult {
104+
self.component.perform(cmd)
105+
}
106+
}
107+
89108
impl Component<Msg, UserEvent> for ContextMenu {
90109
fn on(&mut self, ev: tuirealm::Event<UserEvent>) -> Option<Msg> {
91110
let _ = match ev {
@@ -116,6 +135,35 @@ impl Component<Msg, UserEvent> for ContextMenu {
116135
};
117136
return msg;
118137
}
138+
Event::Mouse(MouseEvent {
139+
kind, column: col, row, ..
140+
}) => {
141+
if !self.current_rect.contains(RectPosition { x: col, y: row }) {
142+
CmdResult::None
143+
} else {
144+
let mut list_index = (row - self.current_rect.y) as usize;
145+
list_index = list_index.saturating_sub(1);
146+
if list_index >= self.component.states.list_len {
147+
list_index = self.component.states.list_len;
148+
}
149+
150+
match kind {
151+
MouseEventKind::Moved => {
152+
self.component.states.list_index = list_index;
153+
CmdResult::Changed(self.component.state())
154+
}
155+
MouseEventKind::Down(MouseButton::Left) => {
156+
return match self.current_view {
157+
View::Workloads => ContextMenu::workload_actions(list_index),
158+
View::Files => ContextMenu::fileview_actions(list_index),
159+
};
160+
}
161+
MouseEventKind::ScrollUp => self.perform(Cmd::Move(Direction::Up)),
162+
MouseEventKind::ScrollDown => self.perform(Cmd::Move(Direction::Down)),
163+
_ => CmdResult::None,
164+
}
165+
}
166+
}
119167
Event::User(UserEvent::SwitchedToView(view)) => {
120168
match view {
121169
View::Workloads => self.attr(Attribute::Content, AttrValue::Table(ContextMenu::workload_options())),

coman/src/components/file_tree.rs

Lines changed: 137 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,18 @@
1-
use std::{iter, path::PathBuf};
1+
use std::{collections::VecDeque, iter, path::PathBuf};
22

33
use tokio::sync::mpsc;
44
use tui_realm_treeview::{Node, NodeValue, TREE_CMD_CLOSE, TREE_CMD_OPEN, TREE_INITIAL_NODE, Tree, TreeView};
55
use tuirealm::{
6-
AttrValue, Attribute, Component, Event, MockComponent, State, StateValue,
6+
AttrValue, Attribute, Component, Event, Frame, MockComponent, State, StateValue,
77
command::{Cmd, CmdResult, Direction, Position},
8-
event::{Key, KeyEvent, KeyModifiers},
8+
event::{Key, KeyEvent, KeyModifiers, MouseButton, MouseEvent, MouseEventKind},
99
props::{Alignment, BorderType, Borders, Color, Style},
10+
ratatui::layout::{Position as RectPosition, Rect},
1011
};
1112

1213
use crate::{
1314
app::{
14-
messages::{DownloadPopupMsg, Msg},
15+
messages::{DownloadPopupMsg, MenuMsg, Msg},
1516
user_events::{FileEvent, UserEvent},
1617
},
1718
cscs::{api_client::types::PathType, ports::BackgroundTask},
@@ -45,10 +46,10 @@ impl NodeValue for FileNode {
4546
}
4647
}
4748

48-
#[derive(MockComponent)]
4949
pub struct FileTree {
5050
component: TreeView<FileNode>,
5151
file_tree_tx: mpsc::Sender<BackgroundTask>,
52+
current_rect: Rect,
5253
}
5354
impl FileTree {
5455
pub fn new(file_tree_tx: mpsc::Sender<BackgroundTask>) -> Self {
@@ -81,29 +82,127 @@ impl FileTree {
8182
.with_tree(tree)
8283
.initial_node(root_node.id()),
8384
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(&current_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(&current_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()
84178
}
85179
}
86180
}
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+
}
87199
impl Component<Msg, UserEvent> for FileTree {
88200
fn on(&mut self, ev: Event<UserEvent>) -> Option<Msg> {
89201
match ev {
90202
Event::Keyboard(KeyEvent {
91203
code: Key::Left,
92204
modifiers: KeyModifiers::NONE,
93-
}) => {
94-
let current_id = self.state().unwrap_one().unwrap_string();
95-
let node = self.component.tree().root().query(&current_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(),
107206
Event::Keyboard(KeyEvent {
108207
code: Key::Right,
109208
modifiers: KeyModifiers::NONE,
@@ -154,6 +253,25 @@ impl Component<Msg, UserEvent> for FileTree {
154253
code: Key::End,
155254
modifiers: KeyModifiers::NONE,
156255
}) => 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+
}
157275
Event::User(UserEvent::File(FileEvent::List(id, subpaths))) => {
158276
let tree = self.component.tree_mut();
159277
let parent = tree.root_mut().query_mut(&id).unwrap();

0 commit comments

Comments
 (0)