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::{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 fn from_scroll(scroll: &ScrollWheelEvent) -> Self {
37 Modifiers {
38 ctrl: scroll.ctrl,
39 shift: scroll.shift,
40 alt: scroll.alt,
41 }
42 }
43}
44
45enum MouseFormat {
46 SGR,
47 Normal(bool),
48}
49
50impl MouseFormat {
51 fn from_mode(mode: TermMode) -> Self {
52 if mode.contains(TermMode::SGR_MOUSE) {
53 MouseFormat::SGR
54 } else if mode.contains(TermMode::UTF8_MOUSE) {
55 MouseFormat::Normal(true)
56 } else {
57 MouseFormat::Normal(false)
58 }
59 }
60}
61
62#[derive(Debug)]
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) {
121 mouse_report(
122 point,
123 MouseButton::from_scroll(e),
124 true,
125 Modifiers::from_scroll(e),
126 MouseFormat::from_mode(mode),
127 )
128 .map(|report| repeat(report).take(max(scroll_lines, 1) 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.abs() as usize * 3);
138 for _ in 0..scroll_lines.abs() {
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
169 if !button.is_other() && mode.intersects(TermMode::MOUSE_MOTION | TermMode::MOUSE_DRAG) {
170 //Only drags are reported in drag mode, so block NoneMove.
171 if mode.contains(TermMode::MOUSE_DRAG) && matches!(button, MouseButton::NoneMove) {
172 None
173 } else {
174 mouse_report(
175 point,
176 button,
177 true,
178 Modifiers::from_moved(e),
179 MouseFormat::from_mode(mode),
180 )
181 }
182 } else {
183 None
184 }
185}
186
187pub fn mouse_side(pos: Vector2F, cur_size: TerminalSize) -> alacritty_terminal::index::Direction {
188 let x = pos.0.x() as usize;
189 let cell_x = x.saturating_sub(cur_size.cell_width as usize) % cur_size.cell_width as usize;
190 let half_cell_width = (cur_size.cell_width / 2.0) as usize;
191 let additional_padding = (cur_size.width() - cur_size.cell_width * 2.) % cur_size.cell_width;
192 let end_of_grid = cur_size.width() - cur_size.cell_width - additional_padding;
193 //Width: Pixels or columns?
194 if cell_x > half_cell_width
195 // Edge case when mouse leaves the window.
196 || x as f32 >= end_of_grid
197 {
198 Side::Right
199 } else {
200 Side::Left
201 }
202}
203
204pub fn mouse_point(pos: Vector2F, cur_size: TerminalSize, display_offset: usize) -> Point {
205 let col = pos.x() / cur_size.cell_width;
206 let col = min(GridCol(col as usize), cur_size.last_column());
207 let line = pos.y() / cur_size.line_height;
208 let line = min(line as i32, cur_size.bottommost_line().0);
209 Point::new(GridLine(line - display_offset as i32), col)
210}
211
212///Generate the bytes to send to the terminal, from the cell location, a mouse event, and the terminal mode
213fn mouse_report(
214 point: Point,
215 button: MouseButton,
216 pressed: bool,
217 modifiers: Modifiers,
218 format: MouseFormat,
219) -> Option<Vec<u8>> {
220 if point.line < 0 {
221 return None;
222 }
223
224 let mut mods = 0;
225 if modifiers.shift {
226 mods += 4;
227 }
228 if modifiers.alt {
229 mods += 8;
230 }
231 if modifiers.ctrl {
232 mods += 16;
233 }
234
235 match format {
236 MouseFormat::SGR => {
237 Some(sgr_mouse_report(point, button as u8 + mods, pressed).into_bytes())
238 }
239 MouseFormat::Normal(utf8) => {
240 if pressed {
241 normal_mouse_report(point, button as u8 + mods, utf8)
242 } else {
243 normal_mouse_report(point, 3 + mods, utf8)
244 }
245 }
246 }
247}
248
249fn normal_mouse_report(point: Point, button: u8, utf8: bool) -> Option<Vec<u8>> {
250 let Point { line, column } = point;
251 let max_point = if utf8 { 2015 } else { 223 };
252
253 if line >= max_point || column >= max_point {
254 return None;
255 }
256
257 let mut msg = vec![b'\x1b', b'[', b'M', 32 + button];
258
259 let mouse_pos_encode = |pos: usize| -> Vec<u8> {
260 let pos = 32 + 1 + pos;
261 let first = 0xC0 + pos / 64;
262 let second = 0x80 + (pos & 63);
263 vec![first as u8, second as u8]
264 };
265
266 if utf8 && column >= 95 {
267 msg.append(&mut mouse_pos_encode(column.0));
268 } else {
269 msg.push(32 + 1 + column.0 as u8);
270 }
271
272 if utf8 && line >= 95 {
273 msg.append(&mut mouse_pos_encode(line.0 as usize));
274 } else {
275 msg.push(32 + 1 + line.0 as u8);
276 }
277
278 Some(msg)
279}
280
281fn sgr_mouse_report(point: Point, button: u8, pressed: bool) -> String {
282 let c = if pressed { 'M' } else { 'm' };
283
284 let msg = format!(
285 "\x1b[<{};{};{}{}",
286 button,
287 point.column + 1,
288 point.line + 1,
289 c
290 );
291
292 msg
293}
294
295#[cfg(test)]
296mod test {
297 use crate::mappings::mouse::mouse_point;
298
299 #[test]
300 fn test_mouse_to_selection() {
301 let term_width = 100.;
302 let term_height = 200.;
303 let cell_width = 10.;
304 let line_height = 20.;
305 let mouse_pos_x = 100.; //Window relative
306 let mouse_pos_y = 100.; //Window relative
307 let origin_x = 10.;
308 let origin_y = 20.;
309
310 let cur_size = crate::TerminalSize::new(
311 line_height,
312 cell_width,
313 gpui::geometry::vector::vec2f(term_width, term_height),
314 );
315
316 let mouse_pos = gpui::geometry::vector::vec2f(mouse_pos_x, mouse_pos_y);
317 let origin = gpui::geometry::vector::vec2f(origin_x, origin_y); //Position of terminal window, 1 'cell' in
318 let mouse_pos = mouse_pos - origin;
319 let point = mouse_point(mouse_pos, cur_size, 0);
320 assert_eq!(
321 point,
322 alacritty_terminal::index::Point::new(
323 alacritty_terminal::index::Line(((mouse_pos_y - origin_y) / line_height) as i32),
324 alacritty_terminal::index::Column(((mouse_pos_x - origin_x) / cell_width) as usize),
325 )
326 );
327 }
328}