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::{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}