mouse.rs

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