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