| 1505 |
1505 |
|
self.paste_primary();
|
| 1506 |
1506 |
|
}
|
| 1507 |
1507 |
|
PointerEventKind::Axis { vertical, .. } => {
|
| 1508 |
|
- |
// The wheel moves the viewport through scrollback. On the
|
| 1509 |
|
- |
// alt screen there is none, and translating the wheel into
|
| 1510 |
|
- |
// arrow keys for full-screen programs is a separate
|
| 1511 |
|
- |
// question (GO shop task da46cb19) — until it is settled,
|
| 1512 |
|
- |
// a wheel turn in vim does nothing rather than something
|
| 1513 |
|
- |
// surprising.
|
| 1514 |
|
- |
//
|
| 1515 |
1508 |
|
// `discrete` is notches where the compositor reports them
|
| 1516 |
1509 |
|
// and zero on a touchpad, where `absolute` carries a
|
| 1517 |
1510 |
|
// continuous distance; take the notch count when there is
|
| 1525 |
1518 |
|
} else {
|
| 1526 |
1519 |
|
0
|
| 1527 |
1520 |
|
};
|
| 1528 |
|
- |
let lines = notches.unsigned_abs() as u16 * WHEEL_LINES;
|
| 1529 |
|
- |
if notches < 0 {
|
| 1530 |
|
- |
self.scroll_view(|g| g.scroll_view_up(lines));
|
| 1531 |
|
- |
} else if notches > 0 {
|
| 1532 |
|
- |
self.scroll_view(|g| g.scroll_view_down(lines));
|
| 1533 |
|
- |
}
|
|
1521 |
+ |
self.apply_wheel(notches);
|
| 1534 |
1522 |
|
}
|
| 1535 |
1523 |
|
PointerEventKind::Leave { .. } => {
|
| 1536 |
1524 |
|
// Keep the drag alive: the pointer leaving the window
|
| 1791 |
1779 |
|
}
|
| 1792 |
1780 |
|
}
|
| 1793 |
1781 |
|
|
|
1782 |
+ |
/// Act on a wheel turn of `notches`, negative for up.
|
|
1783 |
+ |
fn apply_wheel(&mut self, notches: i32) {
|
|
1784 |
+ |
let modes = shop_xkb::Modes {
|
|
1785 |
+ |
cursor_keys_application: self.grid.cursor_keys_application(),
|
|
1786 |
+ |
keypad_application: self.grid.keypad_application(),
|
|
1787 |
+ |
};
|
|
1788 |
+ |
match wheel_action(
|
|
1789 |
+ |
notches,
|
|
1790 |
+ |
self.grid.on_alt(),
|
|
1791 |
+ |
self.grid.alternate_scroll(),
|
|
1792 |
+ |
modes,
|
|
1793 |
+ |
) {
|
|
1794 |
+ |
WheelAction::Nothing => {}
|
|
1795 |
+ |
WheelAction::Up(lines) => self.scroll_view(|g| g.scroll_view_up(lines)),
|
|
1796 |
+ |
WheelAction::Down(lines) => self.scroll_view(|g| g.scroll_view_down(lines)),
|
|
1797 |
+ |
// No snap back to the bottom here, unlike typing: the viewport is
|
|
1798 |
+ |
// already pinned at zero on the alt screen, so there is nothing
|
|
1799 |
+ |
// for a reset to do.
|
|
1800 |
+ |
WheelAction::Keys(bytes) => {
|
|
1801 |
+ |
if let Err(e) = self.pty.write(&bytes) {
|
|
1802 |
+ |
warn!("pty write: {e}");
|
|
1803 |
+ |
}
|
|
1804 |
+ |
}
|
|
1805 |
+ |
}
|
|
1806 |
+ |
}
|
|
1807 |
+ |
|
| 1794 |
1808 |
|
/// Reconcile the selection with what just happened to the grid.
|
| 1795 |
1809 |
|
///
|
| 1796 |
1810 |
|
/// A selection is a claim about which cells hold which text, and a scroll
|
| 1851 |
1865 |
|
}
|
| 1852 |
1866 |
|
}
|
| 1853 |
1867 |
|
|
|
1868 |
+ |
/// What a wheel turn amounts to, once the screen and the modes are known.
|
|
1869 |
+ |
///
|
|
1870 |
+ |
/// Split out from the pointer handler so the decision is testable without a
|
|
1871 |
+ |
/// compositor: the arithmetic and the alt-screen rule are the parts worth
|
|
1872 |
+ |
/// getting right, and neither of them needs a window.
|
|
1873 |
+ |
#[derive(Debug, PartialEq, Eq)]
|
|
1874 |
+ |
enum WheelAction {
|
|
1875 |
+ |
/// Move the viewport back into history by this many rows.
|
|
1876 |
+ |
Up(u16),
|
|
1877 |
+ |
/// Move the viewport toward the live screen by this many rows.
|
|
1878 |
+ |
Down(u16),
|
|
1879 |
+ |
/// Write these bytes to the PTY.
|
|
1880 |
+ |
Keys(Vec<u8>),
|
|
1881 |
+ |
Nothing,
|
|
1882 |
+ |
}
|
|
1883 |
+ |
|
|
1884 |
+ |
/// Decide what a wheel turn of `notches` does. Negative is up, toward older
|
|
1885 |
+ |
/// output.
|
|
1886 |
+ |
///
|
|
1887 |
+ |
/// Off the alt screen the wheel moves the viewport through scrollback. On it
|
|
1888 |
+ |
/// there is no history to move through — the offset is pinned at zero — so the
|
|
1889 |
+ |
/// turn becomes cursor keys instead, which is what makes `less`, `man` and
|
|
1890 |
+ |
/// `git log` scroll. DECSET 1007 is the program's way of declining that.
|
|
1891 |
+ |
fn wheel_action(
|
|
1892 |
+ |
notches: i32,
|
|
1893 |
+ |
on_alt: bool,
|
|
1894 |
+ |
alternate_scroll: bool,
|
|
1895 |
+ |
modes: shop_xkb::Modes,
|
|
1896 |
+ |
) -> WheelAction {
|
|
1897 |
+ |
if notches == 0 {
|
|
1898 |
+ |
return WheelAction::Nothing;
|
|
1899 |
+ |
}
|
|
1900 |
+ |
let lines = notches
|
|
1901 |
+ |
.unsigned_abs()
|
|
1902 |
+ |
.saturating_mul(u32::from(WHEEL_LINES))
|
|
1903 |
+ |
.min(u32::from(u16::MAX)) as u16;
|
|
1904 |
+ |
let up = notches < 0;
|
|
1905 |
+ |
|
|
1906 |
+ |
if !on_alt {
|
|
1907 |
+ |
return if up {
|
|
1908 |
+ |
WheelAction::Up(lines)
|
|
1909 |
+ |
} else {
|
|
1910 |
+ |
WheelAction::Down(lines)
|
|
1911 |
+ |
};
|
|
1912 |
+ |
}
|
|
1913 |
+ |
if !alternate_scroll {
|
|
1914 |
+ |
return WheelAction::Nothing;
|
|
1915 |
+ |
}
|
|
1916 |
+ |
// One arrow per row the viewport would have moved, so a turn covers the
|
|
1917 |
+ |
// same distance whichever screen it lands on. `encode` is the only place
|
|
1918 |
+ |
// that knows whether DECCKM makes that `CSI B` or `SS3 B`.
|
|
1919 |
+ |
let keysym = if up { Keysym::Up } else { Keysym::Down };
|
|
1920 |
+ |
let one = shop_xkb::encode(keysym, "", shop_xkb::Mods::default(), modes);
|
|
1921 |
+ |
let mut bytes = Vec::with_capacity(one.len() * lines as usize);
|
|
1922 |
+ |
for _ in 0..lines {
|
|
1923 |
+ |
bytes.extend_from_slice(&one);
|
|
1924 |
+ |
}
|
|
1925 |
+ |
WheelAction::Keys(bytes)
|
|
1926 |
+ |
}
|
|
1927 |
+ |
|
| 1854 |
1928 |
|
/// SCTK's modifier set as shop-xkb's.
|
| 1855 |
1929 |
|
///
|
| 1856 |
1930 |
|
/// Two structurally identical types, kept apart on purpose: shop-xkb does not
|
| 1964 |
2038 |
|
items.iter().map(|s| (*s).to_string()).collect()
|
| 1965 |
2039 |
|
}
|
| 1966 |
2040 |
|
|
|
2041 |
+ |
// ---- the wheel ------------------------------------------------------
|
|
2042 |
+ |
|
|
2043 |
+ |
const NORMAL: shop_xkb::Modes = shop_xkb::Modes {
|
|
2044 |
+ |
cursor_keys_application: false,
|
|
2045 |
+ |
keypad_application: false,
|
|
2046 |
+ |
};
|
|
2047 |
+ |
|
|
2048 |
+ |
fn keys(action: &WheelAction) -> &str {
|
|
2049 |
+ |
match action {
|
|
2050 |
+ |
WheelAction::Keys(b) => std::str::from_utf8(b).expect("arrow bytes are ascii"),
|
|
2051 |
+ |
other => panic!("expected keys, got {other:?}"),
|
|
2052 |
+ |
}
|
|
2053 |
+ |
}
|
|
2054 |
+ |
|
|
2055 |
+ |
#[test]
|
|
2056 |
+ |
fn on_the_main_screen_the_wheel_moves_the_viewport() {
|
|
2057 |
+ |
assert_eq!(
|
|
2058 |
+ |
wheel_action(-1, false, true, NORMAL),
|
|
2059 |
+ |
WheelAction::Up(WHEEL_LINES)
|
|
2060 |
+ |
);
|
|
2061 |
+ |
assert_eq!(
|
|
2062 |
+ |
wheel_action(2, false, true, NORMAL),
|
|
2063 |
+ |
WheelAction::Down(WHEEL_LINES * 2)
|
|
2064 |
+ |
);
|
|
2065 |
+ |
}
|
|
2066 |
+ |
|
|
2067 |
+ |
#[test]
|
|
2068 |
+ |
fn the_main_screen_ignores_1007() {
|
|
2069 |
+ |
// The mode only answers what to do where there is no history; it is
|
|
2070 |
+ |
// not an off switch for scrolling.
|
|
2071 |
+ |
assert_eq!(
|
|
2072 |
+ |
wheel_action(-1, false, false, NORMAL),
|
|
2073 |
+ |
WheelAction::Up(WHEEL_LINES)
|
|
2074 |
+ |
);
|
|
2075 |
+ |
}
|
|
2076 |
+ |
|
|
2077 |
+ |
#[test]
|
|
2078 |
+ |
fn on_the_alt_screen_the_wheel_becomes_arrows() {
|
|
2079 |
+ |
// One arrow per row the viewport would have moved, so `less` covers
|
|
2080 |
+ |
// the same distance as the scrollback would have.
|
|
2081 |
+ |
assert_eq!(
|
|
2082 |
+ |
keys(&wheel_action(-1, true, true, NORMAL)),
|
|
2083 |
+ |
"\x1b[A".repeat(3)
|
|
2084 |
+ |
);
|
|
2085 |
+ |
assert_eq!(
|
|
2086 |
+ |
keys(&wheel_action(1, true, true, NORMAL)),
|
|
2087 |
+ |
"\x1b[B".repeat(3)
|
|
2088 |
+ |
);
|
|
2089 |
+ |
assert_eq!(
|
|
2090 |
+ |
keys(&wheel_action(3, true, true, NORMAL)),
|
|
2091 |
+ |
"\x1b[B".repeat(9)
|
|
2092 |
+ |
);
|
|
2093 |
+ |
}
|
|
2094 |
+ |
|
|
2095 |
+ |
#[test]
|
|
2096 |
+ |
fn decckm_switches_the_arrows_to_their_application_form() {
|
|
2097 |
+ |
let modes = shop_xkb::Modes {
|
|
2098 |
+ |
cursor_keys_application: true,
|
|
2099 |
+ |
..NORMAL
|
|
2100 |
+ |
};
|
|
2101 |
+ |
assert_eq!(
|
|
2102 |
+ |
keys(&wheel_action(1, true, true, modes)),
|
|
2103 |
+ |
"\x1bOB".repeat(3)
|
|
2104 |
+ |
);
|
|
2105 |
+ |
}
|
|
2106 |
+ |
|
|
2107 |
+ |
#[test]
|
|
2108 |
+ |
fn clearing_1007_leaves_the_alt_screen_wheel_dead() {
|
|
2109 |
+ |
assert_eq!(wheel_action(1, true, false, NORMAL), WheelAction::Nothing);
|
|
2110 |
+ |
assert_eq!(wheel_action(-1, true, false, NORMAL), WheelAction::Nothing);
|
|
2111 |
+ |
}
|
|
2112 |
+ |
|
|
2113 |
+ |
#[test]
|
|
2114 |
+ |
fn no_notches_is_no_action() {
|
|
2115 |
+ |
assert_eq!(wheel_action(0, false, true, NORMAL), WheelAction::Nothing);
|
|
2116 |
+ |
assert_eq!(wheel_action(0, true, true, NORMAL), WheelAction::Nothing);
|
|
2117 |
+ |
}
|
|
2118 |
+ |
|
| 1967 |
2119 |
|
#[test]
|
| 1968 |
2120 |
|
fn with_no_flags_the_login_shell_runs() {
|
| 1969 |
2121 |
|
let (cmd, args) = spawn_target(None, None, Some("/usr/bin/nu".into()));
|