scrolling
This commit is contained in:
+21
-2
@@ -1,6 +1,25 @@
|
|||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use libghostty_vt::{Terminal, key};
|
use libghostty_vt::{Terminal, key};
|
||||||
|
|
||||||
|
/// Avoid libghostty-vt 0.2.1's encode_to_vec capacity calculation, which can
|
||||||
|
/// reserve too little when the destination already has some spare capacity.
|
||||||
|
pub fn append_encoded(
|
||||||
|
out: &mut Vec<u8>,
|
||||||
|
mut encode: impl FnMut(&mut [u8]) -> libghostty_vt::error::Result<usize>,
|
||||||
|
) -> Result<()> {
|
||||||
|
let mut buffer = [0; 128];
|
||||||
|
match encode(&mut buffer) {
|
||||||
|
Ok(written) => out.extend_from_slice(&buffer[..written]),
|
||||||
|
Err(libghostty_vt::Error::OutOfSpace { required }) => {
|
||||||
|
let mut buffer = vec![0; required];
|
||||||
|
let written = encode(&mut buffer)?;
|
||||||
|
out.extend_from_slice(&buffer[..written]);
|
||||||
|
}
|
||||||
|
Err(error) => return Err(error.into()),
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
pub fn mods(m: egui::Modifiers) -> key::Mods {
|
pub fn mods(m: egui::Modifiers) -> key::Mods {
|
||||||
let mut out = key::Mods::empty();
|
let mut out = key::Mods::empty();
|
||||||
if m.ctrl {
|
if m.ctrl {
|
||||||
@@ -71,7 +90,7 @@ pub fn encode(terminal: &Terminal<'_, '_>, events: &[egui::Event]) -> Result<Vec
|
|||||||
})
|
})
|
||||||
.set_unshifted_codepoint(ucp)
|
.set_unshifted_codepoint(ucp)
|
||||||
.set_utf8(text);
|
.set_utf8(text);
|
||||||
encoder.encode_to_vec(&event, &mut out)?;
|
append_encoded(&mut out, |buffer| encoder.encode(&event, buffer))?;
|
||||||
}
|
}
|
||||||
egui::Event::Text(text) if !used_text.contains(&index) => {
|
egui::Event::Text(text) if !used_text.contains(&index) => {
|
||||||
event
|
event
|
||||||
@@ -81,7 +100,7 @@ pub fn encode(terminal: &Terminal<'_, '_>, events: &[egui::Event]) -> Result<Vec
|
|||||||
.set_consumed_mods(key::Mods::empty())
|
.set_consumed_mods(key::Mods::empty())
|
||||||
.set_unshifted_codepoint('\0')
|
.set_unshifted_codepoint('\0')
|
||||||
.set_utf8(Some(text.as_str()));
|
.set_utf8(Some(text.as_str()));
|
||||||
encoder.encode_to_vec(&event, &mut out)?;
|
append_encoded(&mut out, |buffer| encoder.encode(&event, buffer))?;
|
||||||
}
|
}
|
||||||
egui::Event::Ime(egui::ImeEvent::Commit(text)) => {
|
egui::Event::Ime(egui::ImeEvent::Commit(text)) => {
|
||||||
out.extend_from_slice(text.as_bytes())
|
out.extend_from_slice(text.as_bytes())
|
||||||
|
|||||||
+82
-13
@@ -31,6 +31,8 @@ pub struct Pane {
|
|||||||
callback_error: Rc<RefCell<Option<String>>>,
|
callback_error: Rc<RefCell<Option<String>>>,
|
||||||
mouse_encoder: mouse::Encoder<'static>,
|
mouse_encoder: mouse::Encoder<'static>,
|
||||||
mouse_event: mouse::Event<'static>,
|
mouse_event: mouse::Event<'static>,
|
||||||
|
scrollback_scroll: ScrollAccumulator,
|
||||||
|
mouse_scroll: ScrollAccumulator,
|
||||||
selection_start: Option<PointCoordinate>,
|
selection_start: Option<PointCoordinate>,
|
||||||
focused: bool,
|
focused: bool,
|
||||||
saved_session: Option<(Box<Pane>, ScrollbackFile)>,
|
saved_session: Option<(Box<Pane>, ScrollbackFile)>,
|
||||||
@@ -38,6 +40,26 @@ pub struct Pane {
|
|||||||
|
|
||||||
struct ScrollbackFile(PathBuf);
|
struct ScrollbackFile(PathBuf);
|
||||||
|
|
||||||
|
#[derive(Default)]
|
||||||
|
struct ScrollAccumulator(f32);
|
||||||
|
|
||||||
|
impl ScrollAccumulator {
|
||||||
|
fn rows(&mut self, delta: f32) -> isize {
|
||||||
|
self.0 += delta;
|
||||||
|
let rows = self.0.trunc();
|
||||||
|
self.0 -= rows;
|
||||||
|
rows as isize
|
||||||
|
}
|
||||||
|
|
||||||
|
fn wheel(&mut self, unit: egui::MouseWheelUnit, delta: f32, cell: f32, page: u16) -> isize {
|
||||||
|
self.rows(match unit {
|
||||||
|
egui::MouseWheelUnit::Point => delta / cell,
|
||||||
|
egui::MouseWheelUnit::Line => delta * 3.0,
|
||||||
|
egui::MouseWheelUnit::Page => delta * f32::from(page),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl ScrollbackFile {
|
impl ScrollbackFile {
|
||||||
fn new(bytes: &[u8]) -> Result<Self> {
|
fn new(bytes: &[u8]) -> Result<Self> {
|
||||||
use std::{
|
use std::{
|
||||||
@@ -71,6 +93,41 @@ impl ScrollbackFile {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod scroll_tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn precise_scrolling_preserves_distance_across_frames() {
|
||||||
|
let mut scroll = ScrollAccumulator::default();
|
||||||
|
// Sub-pixel events from a slow gesture must eventually move a row.
|
||||||
|
let rows: isize = (0..128).map(|_| scroll.rows(0.25 / 16.0)).sum();
|
||||||
|
assert_eq!(rows, 2);
|
||||||
|
assert_eq!(scroll.0, 0.0);
|
||||||
|
|
||||||
|
let mut batched = ScrollAccumulator::default();
|
||||||
|
assert_eq!(batched.rows(32.0 / 16.0), rows);
|
||||||
|
assert_eq!(scroll.rows(1.75), 1);
|
||||||
|
assert_eq!(scroll.rows(0.25), 1);
|
||||||
|
assert_eq!(scroll.rows(-0.75), 0);
|
||||||
|
assert_eq!(scroll.rows(-0.25), -1);
|
||||||
|
assert_eq!(scroll.rows(0.75), 0);
|
||||||
|
assert_eq!(scroll.rows(-0.75), 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn mouse_tracking_respects_precise_distance_and_wheel_units() {
|
||||||
|
use egui::MouseWheelUnit::{Line, Page, Point};
|
||||||
|
let mut scroll = ScrollAccumulator::default();
|
||||||
|
assert_eq!(scroll.wheel(Point, 0.0, 16.0, 24), 0);
|
||||||
|
assert_eq!(scroll.wheel(Point, 4.0, 16.0, 24), 0);
|
||||||
|
assert_eq!(scroll.wheel(Point, 12.0, 16.0, 24), 1);
|
||||||
|
assert_eq!(scroll.wheel(Point, -32.0, 16.0, 24), -2);
|
||||||
|
assert_eq!(scroll.wheel(Line, 1.0, 16.0, 24), 3);
|
||||||
|
assert_eq!(scroll.wheel(Page, -1.0, 16.0, 24), -24);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod scrollback_tests {
|
mod scrollback_tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
@@ -307,6 +364,8 @@ impl Pane {
|
|||||||
callback_error,
|
callback_error,
|
||||||
mouse_encoder,
|
mouse_encoder,
|
||||||
mouse_event,
|
mouse_event,
|
||||||
|
scrollback_scroll: ScrollAccumulator::default(),
|
||||||
|
mouse_scroll: ScrollAccumulator::default(),
|
||||||
selection_start: None,
|
selection_start: None,
|
||||||
focused: false,
|
focused: false,
|
||||||
saved_session: None,
|
saved_session: None,
|
||||||
@@ -414,6 +473,7 @@ impl Pane {
|
|||||||
.try_send(bytes)
|
.try_send(bytes)
|
||||||
.map_err(|e| anyhow::anyhow!("PTY input queue: {e}"))?;
|
.map_err(|e| anyhow::anyhow!("PTY input queue: {e}"))?;
|
||||||
self.terminal.scroll_viewport(ScrollViewport::Bottom);
|
self.terminal.scroll_viewport(ScrollViewport::Bottom);
|
||||||
|
self.scrollback_scroll = ScrollAccumulator::default();
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -531,6 +591,12 @@ impl Pane {
|
|||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
let modifiers = ui.input(|i| i.modifiers);
|
let modifiers = ui.input(|i| i.modifiers);
|
||||||
let tracking = self.terminal.is_mouse_tracking()? && !modifiers.shift;
|
let tracking = self.terminal.is_mouse_tracking()? && !modifiers.shift;
|
||||||
|
if !response.hovered() || tracking {
|
||||||
|
self.scrollback_scroll = ScrollAccumulator::default();
|
||||||
|
}
|
||||||
|
if !response.hovered() || !tracking {
|
||||||
|
self.mouse_scroll = ScrollAccumulator::default();
|
||||||
|
}
|
||||||
let pos = response
|
let pos = response
|
||||||
.interact_pointer_pos()
|
.interact_pointer_pos()
|
||||||
.or_else(|| ui.input(|i| i.pointer.hover_pos()));
|
.or_else(|| ui.input(|i| i.pointer.hover_pos()));
|
||||||
@@ -579,8 +645,9 @@ impl Pane {
|
|||||||
mouse::Action::Release
|
mouse::Action::Release
|
||||||
})
|
})
|
||||||
.set_button(Some(button));
|
.set_button(Some(button));
|
||||||
self.mouse_encoder
|
input::append_encoded(&mut data, |buffer| {
|
||||||
.encode_to_vec(&self.mouse_event, &mut data)?;
|
self.mouse_encoder.encode(&self.mouse_event, buffer)
|
||||||
|
})?;
|
||||||
}
|
}
|
||||||
egui::Event::PointerMoved(_) => {
|
egui::Event::PointerMoved(_) => {
|
||||||
let button = ui.input(|i| {
|
let button = ui.input(|i| {
|
||||||
@@ -595,20 +662,23 @@ impl Pane {
|
|||||||
self.mouse_event
|
self.mouse_event
|
||||||
.set_action(mouse::Action::Motion)
|
.set_action(mouse::Action::Motion)
|
||||||
.set_button(button);
|
.set_button(button);
|
||||||
self.mouse_encoder
|
input::append_encoded(&mut data, |buffer| {
|
||||||
.encode_to_vec(&self.mouse_event, &mut data)?;
|
self.mouse_encoder.encode(&self.mouse_event, buffer)
|
||||||
|
})?;
|
||||||
}
|
}
|
||||||
egui::Event::MouseWheel { delta, .. } => {
|
egui::Event::MouseWheel { unit, delta, .. } => {
|
||||||
|
let rows = self.mouse_scroll.wheel(unit, delta.y, cell.y, self.size.1);
|
||||||
self.mouse_event
|
self.mouse_event
|
||||||
.set_action(mouse::Action::Press)
|
.set_action(mouse::Action::Press)
|
||||||
.set_button(Some(if delta.y > 0.0 {
|
.set_button(Some(if rows > 0 {
|
||||||
mouse::Button::Four
|
mouse::Button::Four
|
||||||
} else {
|
} else {
|
||||||
mouse::Button::Five
|
mouse::Button::Five
|
||||||
}));
|
}));
|
||||||
for _ in 0..3 {
|
for _ in 0..rows.unsigned_abs() {
|
||||||
self.mouse_encoder
|
input::append_encoded(&mut data, |buffer| {
|
||||||
.encode_to_vec(&self.mouse_event, &mut data)?;
|
self.mouse_encoder.encode(&self.mouse_event, buffer)
|
||||||
|
})?;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
_ => {}
|
_ => {}
|
||||||
@@ -641,10 +711,9 @@ impl Pane {
|
|||||||
}
|
}
|
||||||
if response.hovered() {
|
if response.hovered() {
|
||||||
let scroll = ui.input(|i| i.raw_scroll_delta.y);
|
let scroll = ui.input(|i| i.raw_scroll_delta.y);
|
||||||
if scroll.abs() >= 1.0 {
|
let rows = self.scrollback_scroll.rows(scroll / cell.y);
|
||||||
self.terminal.scroll_viewport(ScrollViewport::Delta(
|
if rows != 0 {
|
||||||
-(scroll / cell.y).round() as isize,
|
self.terminal.scroll_viewport(ScrollViewport::Delta(-rows));
|
||||||
));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -80,6 +80,54 @@ fn terminal_preserves_unicode_colors_and_application_input_modes() {
|
|||||||
assert!(plain(&term).contains("hello red"));
|
assert!(plain(&term).contains("hello red"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn input_encoding_grows_buffers_without_losing_events() {
|
||||||
|
let term = pane::terminal(&Config::default(), &theme::DEFAULT).unwrap();
|
||||||
|
let mut events = vec![Event::Text("a".into()), Event::Text("123456789".into())];
|
||||||
|
let long_text = "界".repeat(200);
|
||||||
|
events.push(Event::Text(long_text.clone()));
|
||||||
|
for _ in 0..100 {
|
||||||
|
events.push(key(Key::ArrowUp, Modifiers::NONE));
|
||||||
|
}
|
||||||
|
let expected = format!("a123456789{long_text}{}", "\x1b[A".repeat(100));
|
||||||
|
assert_eq!(input::encode(&term, &events).unwrap(), expected.as_bytes());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn mouse_encoding_appends_when_destination_has_insufficient_spare_capacity() {
|
||||||
|
use libghostty_vt::mouse;
|
||||||
|
|
||||||
|
let mut term = pane::terminal(&Config::default(), &theme::DEFAULT).unwrap();
|
||||||
|
term.vt_write(b"\x1b[?1000h\x1b[?1006h");
|
||||||
|
let mut encoder = mouse::Encoder::new().unwrap();
|
||||||
|
encoder
|
||||||
|
.set_options_from_terminal(&term)
|
||||||
|
.set_size(mouse::EncoderSize {
|
||||||
|
screen_width: 800,
|
||||||
|
screen_height: 600,
|
||||||
|
cell_width: 8,
|
||||||
|
cell_height: 16,
|
||||||
|
padding_top: 0,
|
||||||
|
padding_bottom: 0,
|
||||||
|
padding_left: 0,
|
||||||
|
padding_right: 0,
|
||||||
|
});
|
||||||
|
let mut event = mouse::Event::new().unwrap();
|
||||||
|
event
|
||||||
|
.set_action(mouse::Action::Press)
|
||||||
|
.set_button(Some(mouse::Button::Left))
|
||||||
|
.set_position(mouse::Position { x: 0.0, y: 0.0 });
|
||||||
|
let mut out = Vec::with_capacity(16);
|
||||||
|
out.resize(9, b'x');
|
||||||
|
for _ in 0..100 {
|
||||||
|
input::append_encoded(&mut out, |buffer| encoder.encode(&event, buffer)).unwrap();
|
||||||
|
}
|
||||||
|
assert_eq!(
|
||||||
|
out,
|
||||||
|
format!("xxxxxxxxx{}", "\x1b[<0;1;1M".repeat(100)).as_bytes()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn queries_return_to_the_pty_and_alternate_screen_restores() {
|
fn queries_return_to_the_pty_and_alternate_screen_restores() {
|
||||||
let mut term = pane::terminal(&Config::default(), &theme::DEFAULT).unwrap();
|
let mut term = pane::terminal(&Config::default(), &theme::DEFAULT).unwrap();
|
||||||
|
|||||||
Reference in New Issue
Block a user