mouse.rs

  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}