1use std::cmp::{max, min};
2use std::iter::repeat;
3
4use alacritty_terminal::grid::Dimensions;
5/// Most of the code, and specifically the constants, in this are copied from Alacritty,
6/// with modifications for our circumstances
7use alacritty_terminal::index::{Column as GridCol, Line as GridLine, Point, Side};
8use alacritty_terminal::term::TermMode;
9use gpui::scene::MouseScrollWheel;
10use gpui::{geometry::vector::Vector2F, MouseButtonEvent, MouseMovedEvent, ScrollWheelEvent};
11
12use crate::TerminalSize;
13
14struct Modifiers {
15 ctrl: bool,
16 shift: bool,
17 alt: bool,
18}
19
20impl Modifiers {
21 fn from_moved(e: &MouseMovedEvent) -> Self {
22 Modifiers {
23 ctrl: e.ctrl,
24 shift: e.shift,
25 alt: e.alt,
26 }
27 }
28
29 fn from_button(e: &MouseButtonEvent) -> Self {
30 Modifiers {
31 ctrl: e.ctrl,
32 shift: e.shift,
33 alt: e.alt,
34 }
35 }
36
37 fn from_scroll(scroll: &ScrollWheelEvent) -> Self {
38 Modifiers {
39 ctrl: scroll.ctrl,
40 shift: scroll.shift,
41 alt: scroll.alt,
42 }
43 }
44}
45
46enum MouseFormat {
47 SGR,
48 Normal(bool),
49}
50
51impl MouseFormat {
52 fn from_mode(mode: TermMode) -> Self {
53 if mode.contains(TermMode::SGR_MOUSE) {
54 MouseFormat::SGR
55 } else if mode.contains(TermMode::UTF8_MOUSE) {
56 MouseFormat::Normal(true)
57 } else {
58 MouseFormat::Normal(false)
59 }
60 }
61}
62
63#[derive(Debug)]
64enum MouseButton {
65 LeftButton = 0,
66 MiddleButton = 1,
67 RightButton = 2,
68 LeftMove = 32,
69 MiddleMove = 33,
70 RightMove = 34,
71 NoneMove = 35,
72 ScrollUp = 64,
73 ScrollDown = 65,
74 Other = 99,
75}
76
77impl MouseButton {
78 fn from_move(e: &MouseMovedEvent) -> Self {
79 match e.pressed_button {
80 Some(b) => match b {
81 gpui::MouseButton::Left => MouseButton::LeftMove,
82 gpui::MouseButton::Middle => MouseButton::MiddleMove,
83 gpui::MouseButton::Right => MouseButton::RightMove,
84 gpui::MouseButton::Navigate(_) => MouseButton::Other,
85 },
86 None => MouseButton::NoneMove,
87 }
88 }
89
90 fn from_button(e: &MouseButtonEvent) -> Self {
91 match e.button {
92 gpui::MouseButton::Left => MouseButton::LeftButton,
93 gpui::MouseButton::Right => MouseButton::MiddleButton,
94 gpui::MouseButton::Middle => MouseButton::RightButton,
95 gpui::MouseButton::Navigate(_) => MouseButton::Other,
96 }
97 }
98
99 fn from_scroll(e: &ScrollWheelEvent) -> Self {
100 if e.delta.raw().y() > 0. {
101 MouseButton::ScrollUp
102 } else {
103 MouseButton::ScrollDown
104 }
105 }
106
107 fn is_other(&self) -> bool {
108 match self {
109 MouseButton::Other => true,
110 _ => false,
111 }
112 }
113}
114
115pub fn scroll_report(
116 point: Point,
117 scroll_lines: i32,
118 e: &MouseScrollWheel,
119 mode: TermMode,
120) -> Option<impl Iterator<Item = Vec<u8>>> {
121 if mode.intersects(TermMode::MOUSE_MODE) {
122 mouse_report(
123 point,
124 MouseButton::from_scroll(e),
125 true,
126 Modifiers::from_scroll(e),
127 MouseFormat::from_mode(mode),
128 )
129 .map(|report| repeat(report).take(max(scroll_lines, 1) as usize))
130 } else {
131 None
132 }
133}
134
135pub fn alt_scroll(scroll_lines: i32) -> Vec<u8> {
136 let cmd = if scroll_lines > 0 { b'A' } else { b'B' };
137
138 let mut content = Vec::with_capacity(scroll_lines.abs() as usize * 3);
139 for _ in 0..scroll_lines.abs() {
140 content.push(0x1b);
141 content.push(b'O');
142 content.push(cmd);
143 }
144 content
145}
146
147pub fn mouse_button_report(
148 point: Point,
149 e: &MouseButtonEvent,
150 pressed: bool,
151 mode: TermMode,
152) -> Option<Vec<u8>> {
153 let button = MouseButton::from_button(e);
154 if !button.is_other() && mode.intersects(TermMode::MOUSE_MODE) {
155 mouse_report(
156 point,
157 button,
158 pressed,
159 Modifiers::from_button(e),
160 MouseFormat::from_mode(mode),
161 )
162 } else {
163 None
164 }
165}
166
167pub fn mouse_moved_report(point: Point, e: &MouseMovedEvent, mode: TermMode) -> Option<Vec<u8>> {
168 let button = MouseButton::from_move(e);
169
170 if !button.is_other() && mode.intersects(TermMode::MOUSE_MOTION | TermMode::MOUSE_DRAG) {
171 //Only drags are reported in drag mode, so block NoneMove.
172 if mode.contains(TermMode::MOUSE_DRAG) && matches!(button, MouseButton::NoneMove) {
173 None
174 } else {
175 mouse_report(
176 point,
177 button,
178 true,
179 Modifiers::from_moved(e),
180 MouseFormat::from_mode(mode),
181 )
182 }
183 } else {
184 None
185 }
186}
187
188pub fn mouse_side(pos: Vector2F, cur_size: TerminalSize) -> alacritty_terminal::index::Direction {
189 if cur_size.cell_width as usize == 0 {
190 return Side::Right;
191 }
192 let x = pos.0.x() as usize;
193 let cell_x = x.saturating_sub(cur_size.cell_width as usize) % cur_size.cell_width as usize;
194 let half_cell_width = (cur_size.cell_width / 2.0) as usize;
195 let additional_padding = (cur_size.width() - cur_size.cell_width * 2.) % cur_size.cell_width;
196 let end_of_grid = cur_size.width() - cur_size.cell_width - additional_padding;
197 //Width: Pixels or columns?
198 if cell_x > half_cell_width
199 // Edge case when mouse leaves the window.
200 || x as f32 >= end_of_grid
201 {
202 Side::Right
203 } else {
204 Side::Left
205 }
206}
207
208pub fn grid_point(pos: Vector2F, cur_size: TerminalSize, display_offset: usize) -> Point {
209 let col = pos.x() / cur_size.cell_width;
210 let col = min(GridCol(col as usize), cur_size.last_column());
211 let line = pos.y() / cur_size.line_height;
212 let line = min(line as i32, cur_size.bottommost_line().0);
213 Point::new(GridLine(line - display_offset as i32), col)
214}
215
216///Generate the bytes to send to the terminal, from the cell location, a mouse event, and the terminal mode
217fn mouse_report(
218 point: Point,
219 button: MouseButton,
220 pressed: bool,
221 modifiers: Modifiers,
222 format: MouseFormat,
223) -> Option<Vec<u8>> {
224 if point.line < 0 {
225 return None;
226 }
227
228 let mut mods = 0;
229 if modifiers.shift {
230 mods += 4;
231 }
232 if modifiers.alt {
233 mods += 8;
234 }
235 if modifiers.ctrl {
236 mods += 16;
237 }
238
239 match format {
240 MouseFormat::SGR => {
241 Some(sgr_mouse_report(point, button as u8 + mods, pressed).into_bytes())
242 }
243 MouseFormat::Normal(utf8) => {
244 if pressed {
245 normal_mouse_report(point, button as u8 + mods, utf8)
246 } else {
247 normal_mouse_report(point, 3 + mods, utf8)
248 }
249 }
250 }
251}
252
253fn normal_mouse_report(point: Point, button: u8, utf8: bool) -> Option<Vec<u8>> {
254 let Point { line, column } = point;
255 let max_point = if utf8 { 2015 } else { 223 };
256
257 if line >= max_point || column >= max_point {
258 return None;
259 }
260
261 let mut msg = vec![b'\x1b', b'[', b'M', 32 + button];
262
263 let mouse_pos_encode = |pos: usize| -> Vec<u8> {
264 let pos = 32 + 1 + pos;
265 let first = 0xC0 + pos / 64;
266 let second = 0x80 + (pos & 63);
267 vec![first as u8, second as u8]
268 };
269
270 if utf8 && column >= 95 {
271 msg.append(&mut mouse_pos_encode(column.0));
272 } else {
273 msg.push(32 + 1 + column.0 as u8);
274 }
275
276 if utf8 && line >= 95 {
277 msg.append(&mut mouse_pos_encode(line.0 as usize));
278 } else {
279 msg.push(32 + 1 + line.0 as u8);
280 }
281
282 Some(msg)
283}
284
285fn sgr_mouse_report(point: Point, button: u8, pressed: bool) -> String {
286 let c = if pressed { 'M' } else { 'm' };
287
288 let msg = format!(
289 "\x1b[<{};{};{}{}",
290 button,
291 point.column + 1,
292 point.line + 1,
293 c
294 );
295
296 msg
297}
298
299#[cfg(test)]
300mod test {
301 use crate::mappings::mouse::grid_point;
302
303 #[test]
304 fn test_mouse_to_selection() {
305 let term_width = 100.;
306 let term_height = 200.;
307 let cell_width = 10.;
308 let line_height = 20.;
309 let mouse_pos_x = 100.; //Window relative
310 let mouse_pos_y = 100.; //Window relative
311 let origin_x = 10.;
312 let origin_y = 20.;
313
314 let cur_size = crate::TerminalSize::new(
315 line_height,
316 cell_width,
317 gpui::geometry::vector::vec2f(term_width, term_height),
318 );
319
320 let mouse_pos = gpui::geometry::vector::vec2f(mouse_pos_x, mouse_pos_y);
321 let origin = gpui::geometry::vector::vec2f(origin_x, origin_y); //Position of terminal window, 1 'cell' in
322 let mouse_pos = mouse_pos - origin;
323 let point = grid_point(mouse_pos, cur_size, 0);
324 assert_eq!(
325 point,
326 alacritty_terminal::index::Point::new(
327 alacritty_terminal::index::Line(((mouse_pos_y - origin_y) / line_height) as i32),
328 alacritty_terminal::index::Column(((mouse_pos_x - origin_x) / cell_width) as usize),
329 )
330 );
331 }
332}