I want to create a TUI that shows data in different blocks on the left and have a console/terminal-like block on the right. If you’re familiar with cmd2 or prompt_toolkit in Python, that is what I am going for. The user enters a command, command is executed, output is formatted then sent to an output buffer which ultimately ends up in the console block depicted below.
My problem is that I want the user to be able to highlight and copy smoothly from the console window but my current implementation is very laggy and seems inaccurate.
My selection logic:
match mouse_event.kind {
MouseEventKind::Down(MouseButton::Left) => {
if self.rect_contains(self.input_rect, mouse_row, mouse_col) {
self.focus_mode = FocusMode::Input;
self.dragging = false;
} else if self.rect_contains(self.console_rect, mouse_row, mouse_col) {
self.focus_mode = FocusMode::Console;
// Start selection
let relative_row = mouse_row - self.console_rect.y;
let relative_col = mouse_col - self.console_rect.x;
self.selection_start = Some((relative_row as usize, relative_col as usize));
self.selection_end = self.selection_start;
self.dragging = true;
} else {
self.selection_start = None;
self.selection_end = None;
self.dragging = false;
}
}
MouseEventKind::Drag(MouseButton::Left) => {
if self.dragging && self.focus_mode == FocusMode::Console {
let relative_row = mouse_row - self.console_rect.y;
let relative_col = mouse_col - self.console_rect.x;
self.selection_end = Some((relative_row as usize, relative_col as usize));
}
}
MouseEventKind::Up(MouseButton::Left) => {
self.dragging = false;
}
_ => {}
}
Print logic
let highlighted_content: Vec<Line> = self
.messages
.iter()
.enumerate()
.flat_map(|(line_index, message)| {
let mut spans = Vec::new();
let chars: Vec<char> = message.chars().collect();
let mut in_selection = false;
for (char_index, &ch) in chars.iter().enumerate() {
if let Some((start_line, start_col)) = self.selection_start {
if let Some((end_line, end_col)) = self.selection_end {
if (line_index == start_line && char_index >= start_col)
|| (line_index == end_line && char_index <= end_col)
|| (line_index > start_line && line_index < end_line)
{
in_selection = true;
} else {
in_selection = false;
}
}
}
let span = if in_selection {
Span::styled(
ch.to_string(),
Style::default().fg(Color::Yellow).bg(Color::Blue),
)
} else {
Span::raw(ch.to_string())
};
spans.push(span);
}
vec![Line::from(spans)]
})
.collect();
Paragraph::new(highlighted_content)
.block(
Block::bordered()
.title("Console")
.style(match self.focus_mode {
FocusMode::Input => Style::default().fg(Color::White),
FocusMode::Console => Style::default().fg(Color::Yellow),
}),
)
.style(Style::default().fg(Color::White))
.alignment(Alignment::Left)
.wrap(Wrap { trim: false })
.render(self.console_rect, buf);
Any help/advice would be much appreciated and if any more information is needed, I’d be happy to provide answers. If looking at the current implemation with the problem would help, check out the my repo.