-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathmouse.rs
More file actions
186 lines (169 loc) · 6.44 KB
/
Copy pathmouse.rs
File metadata and controls
186 lines (169 loc) · 6.44 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
//! Live mouse demo. Run with `cargo run --example mouse`.
//!
//! Drag across the text to select it — the selection highlights, and releasing
//! the button copies it to the system clipboard via OSC 52. Click the on-screen
//! buttons (`[ clear ]`, `[ quit ]`). Press `q`/`Esc` to quit.
//!
//! This exercises the whole `tuika::mouse` stack end to end: `translate_event`
//! feeding `SelectionState` / `ClickTracker` / `HitMap`, `highlight` +
//! `selected_text` over the rendered buffer, and `clipboard::write_clipboard`
//! (OSC 52). Chrome is drawn with plain ratatui widgets so the interaction rects
//! are exact — the star here is the mouse API, not layout.
//!
//! Shift-drag is intentionally *not* handled: most terminals use it to bypass
//! application mouse capture for their own native selection.
use std::io;
use std::time::Duration;
use crossterm::event::{self, Event as CtEvent, KeyCode, KeyEventKind};
use crossterm::terminal::{disable_raw_mode, enable_raw_mode};
use ratatui::backend::CrosstermBackend;
use ratatui::layout::Rect;
use ratatui::style::Style;
use ratatui::text::{Line, Span};
use ratatui::widgets::{Block, Paragraph};
use ratatui::{Terminal, TerminalOptions, Viewport};
use tuika::host::AltScreen;
use tuika::mouse::{ClickTracker, HitMap, SelectionState, paint_selection, selected_text};
use tuika::prelude::*;
use tuika::term::clipboard;
const SAMPLE: &[&str] = &[
"Drag across this text to select it.",
"Release the button to copy the selection",
"to your clipboard via OSC 52.",
"Wide glyphs work too: 你好 world.",
];
#[derive(Clone, Copy, PartialEq, Eq)]
enum Btn {
Clear,
Quit,
}
fn main() -> io::Result<()> {
enable_raw_mode()?;
let mut alt = AltScreen::enter()?; // also enables mouse capture
let mut term = Terminal::with_options(
CrosstermBackend::new(io::stdout()),
TerminalOptions {
viewport: Viewport::Fullscreen,
},
)?;
let theme = Theme::default();
let mut sel = SelectionState::new();
let mut clicks = ClickTracker::new();
let mut status = String::from("drag to select · click a button · q to quit");
let sel_style = Style::default()
.bg(theme.selection_bg)
.fg(theme.selection_fg);
// Set when a drag is released; the copy happens in the next draw, where the
// freshly rendered frame buffer is available to read the selected text from.
let mut pending_copy = false;
loop {
// Rects are computed inside the draw and reused for hit-testing below.
let mut text_area = Rect::default();
let mut clear_btn = Rect::default();
let mut quit_btn = Rect::default();
term.draw(|f| {
let area = f.area();
text_area = Rect::new(
2,
2,
area.width.saturating_sub(4).max(1),
SAMPLE.len() as u16,
);
let btn_y = text_area.bottom().saturating_add(1);
clear_btn = Rect::new(2, btn_y, 9, 1);
quit_btn = Rect::new(12, btn_y, 8, 1);
f.render_widget(
Paragraph::new(Line::from(Span::styled(
" tuika mouse demo ",
theme.accent_style(),
))),
Rect::new(1, 0, area.width.saturating_sub(2), 1),
);
f.render_widget(
Block::bordered(),
Rect::new(1, 1, area.width.saturating_sub(2), SAMPLE.len() as u16 + 2),
);
f.render_widget(
Paragraph::new(SAMPLE.iter().map(|s| Line::from(*s)).collect::<Vec<_>>()),
text_area,
);
f.render_widget(
Paragraph::new(Span::styled("[ clear ]", theme.muted_style())),
clear_btn,
);
f.render_widget(
Paragraph::new(Span::styled("[ quit ]", theme.muted_style())),
quit_btn,
);
if sel.resolve(f.buffer_mut(), text_area) {
pending_copy = true;
}
if let Some(range) = sel.range() {
// Read the selected text off the freshly rendered frame *before*
// highlighting (highlight only changes style, so order is cosmetic).
if pending_copy {
let text = selected_text(f.buffer_mut(), text_area, range);
if let Ok(true) = clipboard::write(&mut io::stdout(), &text) {
status = format!("copied {} chars", text.chars().count());
}
pending_copy = false;
}
paint_selection(f.buffer_mut(), text_area, range, sel_style);
} else {
pending_copy = false;
}
f.render_widget(
Paragraph::new(Span::styled(status.clone(), theme.muted_style())),
Rect::new(
1,
area.height.saturating_sub(1),
area.width.saturating_sub(2),
1,
),
);
})?;
let mut hits: HitMap<Btn> = HitMap::new();
hits.push(clear_btn, Btn::Clear);
hits.push(quit_btn, Btn::Quit);
if !event::poll(Duration::from_millis(100))? {
continue;
}
let ct = event::read()?;
if let CtEvent::Key(k) = &ct
&& k.kind != KeyEventKind::Release
&& matches!(k.code, KeyCode::Char('q') | KeyCode::Esc)
{
break;
}
let Some(Event::Mouse(m)) = translate_event(ct) else {
continue;
};
// Keep selection state consistent for every plain gesture.
let selection_changed = m.plain() && sel.handle(&m);
// A click that lands on a button wins over selection.
if let Some(click) = clicks.handle(&m)
&& let Some(btn) = hits.hit(click.column, click.row)
{
match btn {
Btn::Quit => break,
Btn::Clear => {
sel.clear();
status = "cleared".into();
}
}
continue;
}
// Releasing a drag arms the copy for the next frame.
if selection_changed
&& matches!(m.kind, MouseKind::Up(MouseButton::Left))
&& sel.range().is_some()
{
pending_copy = true;
}
}
let _ = term.clear();
drop(term);
alt.leave();
disable_raw_mode()?;
Ok(())
}