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