T58: the terminal shows a scrollbar over its history, which a user asked for in issue #2 after 0.1.5, since a program that takes the mouse takes the wheel: a thumb along the right edge, in Ely's sizes and colors, stands where the view stands, and a drag moves the view through the history; none shows without history
This commit is contained in:
9 files changed
+194
-7
No files matched your search
@@ -16,6 +16,7 @@ mod pointer;
|
||||
mod prompts;
|
||||
mod pty;
|
||||
mod reports;
|
||||
mod scrollbar;
|
||||
pub mod shell;
|
||||
mod status;
|
||||
mod tap;
|
||||
|
||||
@@ -0,0 +1,170 @@
|
||||
//! The terminal's scrollbar: a thumb over the history, in Ely's sizes and colors, that a drag moves through it, whatever the wheel is bound to.
|
||||
|
||||
use std::{cell::Cell, rc::Rc};
|
||||
|
||||
use alacritty_terminal::{
|
||||
grid::{Dimensions, Scroll},
|
||||
term::Term,
|
||||
};
|
||||
use ely_gpui_component::theme::ActiveTheme;
|
||||
use gpui::{
|
||||
AnyElement, AppContext as _, Context, DragMoveEvent, EmptyView, EntityId, InteractiveElement,
|
||||
IntoElement, ParentElement, Pixels, StatefulInteractiveElement, Styled, Window, div, relative,
|
||||
};
|
||||
|
||||
use super::view::TerminalView;
|
||||
|
||||
/// A drag of the thumb: its view, how far down the history the view stood as it began, and the drag's first point.
|
||||
pub(super) struct ThumbDrag {
|
||||
owner: EntityId,
|
||||
from: f32,
|
||||
anchor: Rc<Cell<Option<Pixels>>>,
|
||||
}
|
||||
|
||||
/// Where the view stands in the history: the lines above the screen, the screen's lines, and how far up the view is.
|
||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||
pub(super) struct Span {
|
||||
history: usize,
|
||||
screen: usize,
|
||||
offset: usize,
|
||||
}
|
||||
|
||||
impl Span {
|
||||
pub(super) fn of<T>(term: &Term<T>) -> Self {
|
||||
Self {
|
||||
history: term.grid().history_size(),
|
||||
screen: term.screen_lines(),
|
||||
offset: term.grid().display_offset(),
|
||||
}
|
||||
}
|
||||
|
||||
/// The thumb's share of the track.
|
||||
fn share(self) -> f32 {
|
||||
self.screen as f32 / (self.history + self.screen) as f32
|
||||
}
|
||||
|
||||
/// How far down the history the view stands: 0 at its top, 1 at the bottom.
|
||||
fn ratio(self) -> f32 {
|
||||
1.0 - self.offset as f32 / self.history as f32
|
||||
}
|
||||
|
||||
/// How far up the view stands with the thumb `ratio` down the track.
|
||||
fn offset_at(self, ratio: f32) -> usize {
|
||||
((1.0 - ratio.clamp(0.0, 1.0)) * self.history as f32).round() as usize
|
||||
}
|
||||
}
|
||||
|
||||
/// The bar along the view's right edge, over the padding there; none without history, as on the alternate screen.
|
||||
pub(super) fn bar(span: Span, window: &Window, cx: &Context<TerminalView>) -> Option<AnyElement> {
|
||||
if span.history == 0 {
|
||||
return None;
|
||||
}
|
||||
let theme = cx.theme();
|
||||
let rem = window.rem_size();
|
||||
let thickness = theme.scrollbar_thickness().to_pixels(rem);
|
||||
let min_thumb = theme.scrollbar_min_thumb().to_pixels(rem);
|
||||
let (idle, busy) = (theme.colors.fg.opacity(0.22), theme.colors.fg.opacity(0.42));
|
||||
let ratio = span.ratio();
|
||||
// The room above and below the thumb shares what it leaves as the view stands.
|
||||
let (mut above, mut below) = (div(), div());
|
||||
above.style().flex_grow = Some(ratio);
|
||||
below.style().flex_grow = Some(1.0 - ratio);
|
||||
let drag = ThumbDrag {
|
||||
owner: cx.entity_id(),
|
||||
from: ratio,
|
||||
anchor: Rc::default(),
|
||||
};
|
||||
let thumb = div()
|
||||
.id("thumb")
|
||||
.w(thickness)
|
||||
.h(relative(span.share()))
|
||||
.min_h(min_thumb)
|
||||
.flex_shrink_0()
|
||||
.rounded_full()
|
||||
.bg(idle)
|
||||
.hover(|style| style.bg(busy))
|
||||
.on_drag(drag, |_, _, _, cx| cx.new(|_| EmptyView));
|
||||
let bar = div()
|
||||
.id("scrollbar")
|
||||
.absolute()
|
||||
.top_0()
|
||||
.bottom_0()
|
||||
.right_0()
|
||||
.w(thickness * 2.0)
|
||||
.pl(thickness * 0.5)
|
||||
.flex()
|
||||
.flex_col()
|
||||
.occlude()
|
||||
.on_drag_move(cx.listener(TerminalView::drag_thumb))
|
||||
.child(above)
|
||||
.child(thumb)
|
||||
.child(below);
|
||||
Some(bar.into_any_element())
|
||||
}
|
||||
|
||||
impl TerminalView {
|
||||
/// Scrolls the history after the thumb, along its track's bounds.
|
||||
fn drag_thumb(
|
||||
&mut self,
|
||||
event: &DragMoveEvent<ThumbDrag>,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
let drag = event.drag(cx);
|
||||
if drag.owner != cx.entity_id() {
|
||||
return;
|
||||
}
|
||||
let min_thumb = cx
|
||||
.theme()
|
||||
.scrollbar_min_thumb()
|
||||
.to_pixels(window.rem_size());
|
||||
let mut term = self.pty.term.lock();
|
||||
let span = Span::of(&term);
|
||||
let height = event.bounds.size.height;
|
||||
let travel = height - (height * span.share()).max(min_thumb).min(height);
|
||||
if span.history == 0 || travel <= Pixels::ZERO {
|
||||
return;
|
||||
}
|
||||
let along = event.event.position.y - event.bounds.origin.y;
|
||||
let anchor = drag.anchor.get().unwrap_or(along);
|
||||
drag.anchor.set(Some(anchor));
|
||||
let offset = span.offset_at(drag.from + (along - anchor) / travel);
|
||||
let delta = offset as i32 - span.offset as i32;
|
||||
if delta != 0 {
|
||||
term.scroll_display(Scroll::Delta(delta));
|
||||
drop(term);
|
||||
log::trace!(
|
||||
"terminal {}: the scrollbar shows {offset} lines up",
|
||||
self.pty.pid
|
||||
);
|
||||
cx.notify();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn the_thumb_stands_where_the_view_stands_in_the_history() {
|
||||
let span = |offset| Span {
|
||||
history: 300,
|
||||
screen: 100,
|
||||
offset,
|
||||
};
|
||||
assert_eq!(span(0).share(), 0.25, "a screen of four");
|
||||
assert_eq!(span(0).ratio(), 1.0, "at the bottom");
|
||||
assert_eq!(span(300).ratio(), 0.0, "at the top");
|
||||
assert_eq!(span(150).ratio(), 0.5);
|
||||
assert_eq!(
|
||||
span(0).offset_at(0.0),
|
||||
300,
|
||||
"the thumb at the top shows the oldest lines"
|
||||
);
|
||||
assert_eq!(span(0).offset_at(1.0), 0);
|
||||
assert_eq!(span(0).offset_at(0.5), 150);
|
||||
assert_eq!(span(0).offset_at(-0.2), 300, "past the track's ends");
|
||||
assert_eq!(span(0).offset_at(1.3), 0);
|
||||
}
|
||||
}
|
||||
+11
-4
@@ -28,6 +28,7 @@ use super::{
|
||||
pointer::Pointer,
|
||||
pty::{Events, Pty, PtyEvent, config, fit},
|
||||
reports::Notices,
|
||||
scrollbar::{self, Span},
|
||||
status::{Records, State},
|
||||
zoom,
|
||||
};
|
||||
@@ -413,6 +414,7 @@ impl Render for TerminalView {
|
||||
});
|
||||
let shown = frame(&term, &ink, &lit);
|
||||
let blinks = term.cursor_style().blinking;
|
||||
let span = Span::of(&term);
|
||||
drop(term);
|
||||
let focused = self.focus.is_focused(window) && window.is_window_active();
|
||||
self.blinking = focused && blinks;
|
||||
@@ -480,9 +482,14 @@ impl Render for TerminalView {
|
||||
root.text_color(ink.fg).child(format!("{error:#}"))
|
||||
}
|
||||
};
|
||||
div().relative().size_full().child(root).children(
|
||||
self.find_box(cx)
|
||||
.map(|found| div().absolute().top_2().right_4().child(found)),
|
||||
)
|
||||
div()
|
||||
.relative()
|
||||
.size_full()
|
||||
.child(root)
|
||||
.children(scrollbar::bar(span, window, cx))
|
||||
.children(
|
||||
self.find_box(cx)
|
||||
.map(|found| div().absolute().top_2().right_4().child(found)),
|
||||
)
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user