feat(gpui): terminal mouse semantics parity - app reporting, right-click, copy-on-select
Mouse-mode apps (vim/htop) now own the pointer: press/release/drag/motion/wheel are encoded per protocol (SGR, normal, UTF-8 extension) and reported to the app, with Shift as the local-selection escape hatch - same as the legacy shell. The encoder is a pure module ported verbatim from input/mouse.rs with unit tests locking the byte sequences. Right-click follows the legacy Windows convention (copy+clear when a selection exists, paste otherwise), copy_on_select fires on release, Shift+click extends an existing selection, and Shift+PageUp/PageDown/Home/End page the scrollback on the primary screen.
This commit is contained in:
1 parent
845ecaeb80
commit
b79c05776b
3 files changed
+438
-30
No files matched your search
@@ -6,5 +6,6 @@
|
||||
pub mod colors;
|
||||
pub mod element;
|
||||
pub mod keymap;
|
||||
pub mod mouse_protocol;
|
||||
pub mod session;
|
||||
pub mod view;
|
||||
@@ -0,0 +1,190 @@
|
||||
//! 鼠标上报协议编码:X10/normal、SGR、UTF-8 扩展坐标。
|
||||
//!
|
||||
//! 从旧壳 `nebula_app/src/input/mouse.rs` 的 `mouse_report` /
|
||||
//! `normal_mouse_report` / `sgr_mouse_report` 逐字对照移植成纯函数——
|
||||
//! 字节序列以旧壳行为为权威,这里只做"无 UI 状态"化,方便单测锁定。
|
||||
//!
|
||||
//! 坐标约定:`point` 是 buffer 坐标(`viewport_to_point` 之后),负行
|
||||
//! 意味着鼠标落在回滚区,任何协议都不上报。
|
||||
|
||||
use nebula_terminal::index::Point as TermPoint;
|
||||
use nebula_terminal::term::TermMode;
|
||||
|
||||
/// 按钮码(xterm):左 0、中 1、右 2;滚轮上/下 64/65。
|
||||
/// 拖动在按钮码上 +32;无按键的纯移动固定 35。
|
||||
pub const BUTTON_LEFT: u8 = 0;
|
||||
pub const BUTTON_MIDDLE: u8 = 1;
|
||||
pub const BUTTON_RIGHT: u8 = 2;
|
||||
pub const WHEEL_UP: u8 = 64;
|
||||
pub const WHEEL_DOWN: u8 = 65;
|
||||
pub const DRAG_OFFSET: u8 = 32;
|
||||
pub const MOTION_ONLY: u8 = 35;
|
||||
|
||||
#[derive(Clone, Copy, Default)]
|
||||
pub struct ReportMods {
|
||||
pub shift: bool,
|
||||
pub alt: bool,
|
||||
pub control: bool,
|
||||
}
|
||||
|
||||
impl ReportMods {
|
||||
/// xterm 修饰位:shift +4、alt +8、ctrl +16。
|
||||
fn value(self) -> u8 {
|
||||
let mut mods = 0;
|
||||
if self.shift {
|
||||
mods += 4;
|
||||
}
|
||||
if self.alt {
|
||||
mods += 8;
|
||||
}
|
||||
if self.control {
|
||||
mods += 16;
|
||||
}
|
||||
mods
|
||||
}
|
||||
}
|
||||
|
||||
/// 编码一次鼠标上报。`None` 表示这次事件不应上报(回滚区、normal 模式
|
||||
/// 坐标超界)。`pressed=false` 只对按键有意义;滚轮永远按 pressed 上报。
|
||||
pub fn report(
|
||||
mode: &TermMode,
|
||||
point: TermPoint,
|
||||
button: u8,
|
||||
pressed: bool,
|
||||
mods: ReportMods,
|
||||
) -> Option<Vec<u8>> {
|
||||
// 回滚区不属于应用坐标空间。
|
||||
if point.line.0 < 0 {
|
||||
return None;
|
||||
}
|
||||
|
||||
let mods = mods.value();
|
||||
if mode.contains(TermMode::SGR_MOUSE) {
|
||||
let c = if pressed { 'M' } else { 'm' };
|
||||
Some(
|
||||
format!("\x1b[<{};{};{}{}", button + mods, point.column.0 + 1, point.line.0 + 1, c)
|
||||
.into_bytes(),
|
||||
)
|
||||
} else if pressed {
|
||||
normal_report(mode, point, button + mods)
|
||||
} else {
|
||||
// normal 协议无法区分哪个键抬起:释放一律报 3。
|
||||
normal_report(mode, point, 3 + mods)
|
||||
}
|
||||
}
|
||||
|
||||
fn normal_report(mode: &TermMode, point: TermPoint, button: u8) -> Option<Vec<u8>> {
|
||||
let utf8 = mode.contains(TermMode::UTF8_MOUSE);
|
||||
let max_point: i32 = if utf8 { 2015 } else { 223 };
|
||||
|
||||
if point.line.0 >= max_point || point.column.0 >= max_point as usize {
|
||||
return None;
|
||||
}
|
||||
|
||||
let mut msg = vec![b'\x1b', b'[', b'M', 32 + button];
|
||||
|
||||
let mouse_pos_encode = |pos: usize| -> [u8; 2] {
|
||||
let pos = 32 + 1 + pos;
|
||||
[(0xC0 + pos / 64) as u8, (0x80 + (pos & 63)) as u8]
|
||||
};
|
||||
|
||||
if utf8 && point.column.0 >= 95 {
|
||||
msg.extend_from_slice(&mouse_pos_encode(point.column.0));
|
||||
} else {
|
||||
msg.push(32 + 1 + point.column.0 as u8);
|
||||
}
|
||||
|
||||
if utf8 && point.line.0 >= 95 {
|
||||
msg.extend_from_slice(&mouse_pos_encode(point.line.0 as usize));
|
||||
} else {
|
||||
msg.push(32 + 1 + point.line.0 as u8);
|
||||
}
|
||||
|
||||
Some(msg)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use nebula_terminal::index::{Column, Line};
|
||||
|
||||
fn at(line: i32, col: usize) -> TermPoint {
|
||||
TermPoint::new(Line(line), Column(col))
|
||||
}
|
||||
|
||||
const NO_MODS: ReportMods = ReportMods { shift: false, alt: false, control: false };
|
||||
|
||||
#[test]
|
||||
fn sgr_press_and_release() {
|
||||
let mode = TermMode::SGR_MOUSE;
|
||||
assert_eq!(
|
||||
report(&mode, at(0, 0), BUTTON_LEFT, true, NO_MODS),
|
||||
Some(b"\x1b[<0;1;1M".to_vec())
|
||||
);
|
||||
assert_eq!(
|
||||
report(&mode, at(9, 4), BUTTON_LEFT, false, NO_MODS),
|
||||
Some(b"\x1b[<0;5;10m".to_vec())
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sgr_modifiers_add_to_button() {
|
||||
let mode = TermMode::SGR_MOUSE;
|
||||
let mods = ReportMods { shift: false, alt: false, control: true };
|
||||
assert_eq!(
|
||||
report(&mode, at(0, 0), BUTTON_RIGHT, true, mods),
|
||||
Some(b"\x1b[<18;1;1M".to_vec())
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn normal_press_release_and_bounds() {
|
||||
let mode = TermMode::empty();
|
||||
// 按下左键 (0,0):CSI M, 32+0, 33, 33。
|
||||
assert_eq!(
|
||||
report(&mode, at(0, 0), BUTTON_LEFT, true, NO_MODS),
|
||||
Some(vec![0x1b, b'[', b'M', 32, 33, 33])
|
||||
);
|
||||
// 释放一律报按钮 3。
|
||||
assert_eq!(
|
||||
report(&mode, at(0, 0), BUTTON_LEFT, false, NO_MODS),
|
||||
Some(vec![0x1b, b'[', b'M', 35, 33, 33])
|
||||
);
|
||||
// normal 协议坐标上限 223。
|
||||
assert_eq!(report(&mode, at(0, 223), BUTTON_LEFT, true, NO_MODS), None);
|
||||
assert_eq!(report(&mode, at(223, 0), BUTTON_LEFT, true, NO_MODS), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn utf8_mouse_encodes_wide_coordinates() {
|
||||
let mode = TermMode::UTF8_MOUSE;
|
||||
// col=100 → pos=133 → 0xC2 0x85(两字节),行仍单字节。
|
||||
assert_eq!(
|
||||
report(&mode, at(0, 100), BUTTON_LEFT, true, NO_MODS),
|
||||
Some(vec![0x1b, b'[', b'M', 32, 0xC2, 0x85, 33])
|
||||
);
|
||||
// UTF-8 扩展上限 2015。
|
||||
assert_eq!(report(&mode, at(0, 2015), BUTTON_LEFT, true, NO_MODS), None);
|
||||
assert!(report(&mode, at(0, 2014), BUTTON_LEFT, true, NO_MODS).is_some());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn scrollback_is_never_reported() {
|
||||
for mode in [TermMode::SGR_MOUSE, TermMode::empty()] {
|
||||
assert_eq!(report(&mode, at(-1, 0), BUTTON_LEFT, true, NO_MODS), None);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn wheel_uses_press_encoding() {
|
||||
let mode = TermMode::SGR_MOUSE;
|
||||
assert_eq!(
|
||||
report(&mode, at(2, 2), WHEEL_UP, true, NO_MODS),
|
||||
Some(b"\x1b[<64;3;3M".to_vec())
|
||||
);
|
||||
assert_eq!(
|
||||
report(&mode, at(2, 2), WHEEL_DOWN, true, NO_MODS),
|
||||
Some(b"\x1b[<65;3;3M".to_vec())
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -21,6 +21,7 @@ use std::sync::Arc;
|
||||
use super::colors::Palette;
|
||||
use super::element::TerminalElement;
|
||||
use super::keymap;
|
||||
use super::mouse_protocol;
|
||||
use super::session::{self, GridSize, TerminalSession};
|
||||
use crate::config::Settings;
|
||||
|
||||
@@ -58,31 +59,39 @@ pub struct TerminalView {
|
||||
viewports: ViewportTracker,
|
||||
scroll_px: f32,
|
||||
selecting: bool,
|
||||
/// 选中即复制(旧壳 `copy_on_select`);关闭时复制交给右键路径。
|
||||
copy_on_select: bool,
|
||||
/// 鼠标模式下最后上报的单元格:move 事件按"进入新单元格"去重。
|
||||
last_report_point: Option<TermPoint>,
|
||||
}
|
||||
|
||||
impl TerminalView {
|
||||
pub fn new(cx: &mut Context<Self>) -> Self {
|
||||
// 字体、调色板与终端启动配置来自用户配置(nebula.toml +
|
||||
// nebula_settings.txt,bootstrap 时装载为全局 Settings)。
|
||||
let (families, font_size, palette, term_config) = match cx.try_global::<Settings>() {
|
||||
Some(settings) => (
|
||||
[
|
||||
settings.font_family.clone(),
|
||||
settings.font_bold_family.clone(),
|
||||
settings.font_italic_family.clone(),
|
||||
settings.font_bold_italic_family.clone(),
|
||||
],
|
||||
px(settings.font_size_px),
|
||||
Arc::new(settings.palette.clone()),
|
||||
settings.term_config(),
|
||||
),
|
||||
None => (
|
||||
std::array::from_fn(|_| String::from("Cascadia Mono")),
|
||||
px(15.0),
|
||||
Arc::new(Palette::default()),
|
||||
nebula_terminal::term::Config::default(),
|
||||
),
|
||||
};
|
||||
let (families, font_size, palette, term_config, copy_on_select) =
|
||||
match cx.try_global::<Settings>() {
|
||||
Some(settings) => (
|
||||
[
|
||||
settings.font_family.clone(),
|
||||
settings.font_bold_family.clone(),
|
||||
settings.font_italic_family.clone(),
|
||||
settings.font_bold_italic_family.clone(),
|
||||
],
|
||||
px(settings.font_size_px),
|
||||
Arc::new(settings.palette.clone()),
|
||||
settings.term_config(),
|
||||
settings.copy_on_select,
|
||||
),
|
||||
None => (
|
||||
std::array::from_fn(|_| String::from("Cascadia Mono")),
|
||||
px(15.0),
|
||||
Arc::new(Palette::default()),
|
||||
nebula_terminal::term::Config::default(),
|
||||
// 旧壳的出厂默认即开。
|
||||
true,
|
||||
),
|
||||
};
|
||||
// to_string:SharedString 的 &str 转换要求 'static,字体名是运行时值。
|
||||
let mono = |family: &str, weight: FontWeight, style: FontStyle| Font {
|
||||
weight,
|
||||
@@ -147,6 +156,8 @@ impl TerminalView {
|
||||
viewports: ViewportTracker::default(),
|
||||
scroll_px: 0.0,
|
||||
selecting: false,
|
||||
copy_on_select,
|
||||
last_report_point: None,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -339,6 +350,28 @@ impl TerminalView {
|
||||
}
|
||||
|
||||
let mode = self.term_mode();
|
||||
|
||||
// 回滚快捷键(对齐旧壳默认绑定,仅主屏):Shift+PageUp/PageDown
|
||||
// 翻页、Shift+Home/End 到顶/到底;备用屏下不拦截,交给编码器
|
||||
// 把带修饰的键序发给应用(less 自己处理 Shift+PageUp)。
|
||||
if mods.shift && !mods.control && !mods.alt && !mode.contains(TermMode::ALT_SCREEN) {
|
||||
let scroll = match ks.key.as_str() {
|
||||
"pageup" => Some(Scroll::PageUp),
|
||||
"pagedown" => Some(Scroll::PageDown),
|
||||
"home" => Some(Scroll::Top),
|
||||
"end" => Some(Scroll::Bottom),
|
||||
_ => None,
|
||||
};
|
||||
if let Some(scroll) = scroll {
|
||||
if let Some(session) = &self.session {
|
||||
session.term.lock().scroll_display(scroll);
|
||||
}
|
||||
cx.notify();
|
||||
cx.stop_propagation();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(bytes) = keymap::encode(ks, &mode) {
|
||||
self.write_input(bytes, cx);
|
||||
cx.stop_propagation();
|
||||
@@ -357,6 +390,30 @@ impl TerminalView {
|
||||
(viewport_to_point(display_offset, TermPoint::new(row, Column(col))), side)
|
||||
}
|
||||
|
||||
/// 应用是否接管了鼠标(vim/htop 等)。Shift 按住时强制旁路——这是
|
||||
/// 终端的通用逃生门:应用吃鼠标时用户仍能选择/复制。
|
||||
fn mouse_mode_active(&self, mods: &gpui::Modifiers) -> bool {
|
||||
!mods.shift && self.term_mode().intersects(TermMode::MOUSE_MODE)
|
||||
}
|
||||
|
||||
/// 把一次鼠标事件按当前协议(SGR/normal/UTF-8)编码上报给应用。
|
||||
fn send_mouse_report(
|
||||
&mut self,
|
||||
position: Point<Pixels>,
|
||||
button: u8,
|
||||
pressed: bool,
|
||||
mods: &gpui::Modifiers,
|
||||
) {
|
||||
let (point, _) = self.grid_point(position);
|
||||
self.last_report_point = Some(point);
|
||||
let mode = self.term_mode();
|
||||
let mods =
|
||||
mouse_protocol::ReportMods { shift: mods.shift, alt: mods.alt, control: mods.control };
|
||||
if let Some(bytes) = mouse_protocol::report(&mode, point, button, pressed, mods) {
|
||||
self.write_bytes(bytes);
|
||||
}
|
||||
}
|
||||
|
||||
fn on_mouse_down(
|
||||
&mut self,
|
||||
event: &MouseDownEvent,
|
||||
@@ -364,14 +421,36 @@ impl TerminalView {
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
window.focus(&self.focus_handle);
|
||||
let Some(session) = &self.session else { return };
|
||||
if self.session.is_none() {
|
||||
return;
|
||||
}
|
||||
if self.mouse_mode_active(&event.modifiers) {
|
||||
self.send_mouse_report(
|
||||
event.position,
|
||||
mouse_protocol::BUTTON_LEFT,
|
||||
true,
|
||||
&event.modifiers,
|
||||
);
|
||||
return;
|
||||
}
|
||||
let (point, side) = self.grid_point(event.position);
|
||||
let ty = match event.click_count {
|
||||
1 => SelectionType::Simple,
|
||||
2 => SelectionType::Semantic,
|
||||
_ => SelectionType::Lines,
|
||||
};
|
||||
session.term.lock().selection = Some(Selection::new(ty, point, side));
|
||||
if let Some(session) = &self.session {
|
||||
let mut term = session.term.lock();
|
||||
// Shift+单击扩展既有选区到点击处(原生文本框行为,对齐旧壳);
|
||||
// 其余情况都开新选区。
|
||||
let extend = event.modifiers.shift
|
||||
&& event.click_count == 1
|
||||
&& term.selection.as_ref().is_some_and(|s| !s.is_empty());
|
||||
match term.selection.as_mut() {
|
||||
Some(selection) if extend => selection.update(point, side),
|
||||
_ => term.selection = Some(Selection::new(ty, point, side)),
|
||||
}
|
||||
}
|
||||
self.selecting = true;
|
||||
cx.notify();
|
||||
}
|
||||
@@ -382,20 +461,144 @@ impl TerminalView {
|
||||
_window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
if !self.selecting || event.pressed_button != Some(MouseButton::Left) {
|
||||
if self.selecting {
|
||||
if event.pressed_button != Some(MouseButton::Left) {
|
||||
return;
|
||||
}
|
||||
let (point, side) = self.grid_point(event.position);
|
||||
if let Some(session) = &self.session {
|
||||
if let Some(selection) = session.term.lock().selection.as_mut() {
|
||||
selection.update(point, side);
|
||||
}
|
||||
}
|
||||
cx.notify();
|
||||
return;
|
||||
}
|
||||
let (point, side) = self.grid_point(event.position);
|
||||
if let Some(session) = &self.session {
|
||||
if let Some(selection) = session.term.lock().selection.as_mut() {
|
||||
selection.update(point, side);
|
||||
}
|
||||
if !self.mouse_mode_active(&event.modifiers) {
|
||||
return;
|
||||
}
|
||||
// 鼠标模式的移动上报:拖动 = 按钮码+32(需 DRAG 或 MOTION 任一),
|
||||
// 无按键纯移动 = 35(仅 MOTION);同一单元格内的移动不重报。
|
||||
let mode = self.term_mode();
|
||||
if !mode.intersects(TermMode::MOUSE_DRAG | TermMode::MOUSE_MOTION) {
|
||||
return;
|
||||
}
|
||||
let button = match event.pressed_button {
|
||||
Some(MouseButton::Left) => mouse_protocol::BUTTON_LEFT + mouse_protocol::DRAG_OFFSET,
|
||||
Some(MouseButton::Middle) => {
|
||||
mouse_protocol::BUTTON_MIDDLE + mouse_protocol::DRAG_OFFSET
|
||||
},
|
||||
Some(MouseButton::Right) => mouse_protocol::BUTTON_RIGHT + mouse_protocol::DRAG_OFFSET,
|
||||
_ => {
|
||||
if !mode.contains(TermMode::MOUSE_MOTION) {
|
||||
return;
|
||||
}
|
||||
mouse_protocol::MOTION_ONLY
|
||||
},
|
||||
};
|
||||
let (point, _) = self.grid_point(event.position);
|
||||
if self.last_report_point == Some(point) {
|
||||
return;
|
||||
}
|
||||
self.send_mouse_report(event.position, button, true, &event.modifiers);
|
||||
}
|
||||
|
||||
fn on_mouse_up(&mut self, event: &MouseUpEvent, _window: &mut Window, cx: &mut Context<Self>) {
|
||||
if !self.selecting && self.mouse_mode_active(&event.modifiers) {
|
||||
self.send_mouse_report(
|
||||
event.position,
|
||||
mouse_protocol::BUTTON_LEFT,
|
||||
false,
|
||||
&event.modifiers,
|
||||
);
|
||||
return;
|
||||
}
|
||||
self.selecting = false;
|
||||
// 选中即复制:在抬手时一次性入剪贴板(旧壳同样在 release 复制,
|
||||
// 避免拖动过程刷爆剪贴板)。空选区在 copy_selection 内自然短路。
|
||||
if self.copy_on_select {
|
||||
self.copy_selection(cx);
|
||||
}
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
fn on_mouse_up(&mut self, _event: &MouseUpEvent, _window: &mut Window, cx: &mut Context<Self>) {
|
||||
self.selecting = false;
|
||||
/// 右键(旧壳 Windows 惯例):有选区 → 复制并清除;无选区 → 粘贴。
|
||||
/// 应用接管鼠标时上报给应用。
|
||||
fn on_right_down(
|
||||
&mut self,
|
||||
event: &MouseDownEvent,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
window.focus(&self.focus_handle);
|
||||
if self.session.is_none() {
|
||||
return;
|
||||
}
|
||||
if self.mouse_mode_active(&event.modifiers) {
|
||||
self.send_mouse_report(
|
||||
event.position,
|
||||
mouse_protocol::BUTTON_RIGHT,
|
||||
true,
|
||||
&event.modifiers,
|
||||
);
|
||||
return;
|
||||
}
|
||||
let has_selection = self
|
||||
.session
|
||||
.as_ref()
|
||||
.is_some_and(|s| s.term.lock().selection.as_ref().is_some_and(|sel| !sel.is_empty()));
|
||||
if has_selection {
|
||||
self.copy_selection(cx);
|
||||
if let Some(session) = &self.session {
|
||||
session.term.lock().selection = None;
|
||||
}
|
||||
cx.notify();
|
||||
} else {
|
||||
self.paste(cx);
|
||||
}
|
||||
}
|
||||
|
||||
fn on_right_up(&mut self, event: &MouseUpEvent, _window: &mut Window, cx: &mut Context<Self>) {
|
||||
if self.mouse_mode_active(&event.modifiers) {
|
||||
self.send_mouse_report(
|
||||
event.position,
|
||||
mouse_protocol::BUTTON_RIGHT,
|
||||
false,
|
||||
&event.modifiers,
|
||||
);
|
||||
}
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
/// 中键只在鼠标模式下有意义(上报给应用);旧壳在 Windows 上没有
|
||||
/// 中键粘贴路径,这里保持一致。
|
||||
fn on_middle_down(
|
||||
&mut self,
|
||||
event: &MouseDownEvent,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
window.focus(&self.focus_handle);
|
||||
if self.mouse_mode_active(&event.modifiers) {
|
||||
self.send_mouse_report(
|
||||
event.position,
|
||||
mouse_protocol::BUTTON_MIDDLE,
|
||||
true,
|
||||
&event.modifiers,
|
||||
);
|
||||
}
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
fn on_middle_up(&mut self, event: &MouseUpEvent, _window: &mut Window, cx: &mut Context<Self>) {
|
||||
if self.mouse_mode_active(&event.modifiers) {
|
||||
self.send_mouse_report(
|
||||
event.position,
|
||||
mouse_protocol::BUTTON_MIDDLE,
|
||||
false,
|
||||
&event.modifiers,
|
||||
);
|
||||
}
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
@@ -412,8 +615,18 @@ impl TerminalView {
|
||||
return;
|
||||
}
|
||||
self.scroll_px -= lines as f32 * self.line_height.as_f32();
|
||||
let mode = self.term_mode();
|
||||
// 应用接管鼠标时滚轮也归应用(htop 列表滚动);Shift 旁路回本地回滚。
|
||||
if !event.modifiers.shift && mode.intersects(TermMode::MOUSE_MODE) {
|
||||
let code =
|
||||
if lines > 0 { mouse_protocol::WHEEL_UP } else { mouse_protocol::WHEEL_DOWN };
|
||||
for _ in 0..lines.unsigned_abs() {
|
||||
self.send_mouse_report(event.position, code, true, &event.modifiers);
|
||||
}
|
||||
cx.notify();
|
||||
return;
|
||||
}
|
||||
let Some(session) = &self.session else { return };
|
||||
let mode = *session.term.lock().mode();
|
||||
if mode.contains(TermMode::ALT_SCREEN) && mode.contains(TermMode::ALTERNATE_SCROLL) {
|
||||
// 备用屏(less/vim):滚轮翻译成方向键。
|
||||
let seq: &[u8] = if mode.contains(TermMode::APP_CURSOR) {
|
||||
@@ -546,8 +759,12 @@ impl Render for TerminalView {
|
||||
.track_focus(&self.focus_handle)
|
||||
.on_key_down(cx.listener(Self::on_key_down))
|
||||
.on_mouse_down(MouseButton::Left, cx.listener(Self::on_mouse_down))
|
||||
.on_mouse_down(MouseButton::Right, cx.listener(Self::on_right_down))
|
||||
.on_mouse_down(MouseButton::Middle, cx.listener(Self::on_middle_down))
|
||||
.on_mouse_move(cx.listener(Self::on_mouse_move))
|
||||
.on_mouse_up(MouseButton::Left, cx.listener(Self::on_mouse_up))
|
||||
.on_mouse_up(MouseButton::Right, cx.listener(Self::on_right_up))
|
||||
.on_mouse_up(MouseButton::Middle, cx.listener(Self::on_middle_up))
|
||||
.on_scroll_wheel(cx.listener(Self::on_scroll));
|
||||
|
||||
if let Some(error) = &self.error {
|
||||
|
||||
Reference in new issue
Block a user