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