diff --git a/src/config/color.rs b/src/config/color.rs index f478763fc..276ba4497 100644 --- a/src/config/color.rs +++ b/src/config/color.rs @@ -22,6 +22,9 @@ pub struct Palette { pub brights: Option<[RgbColor; 8]>, /// Configure the colors and styling of the tab bar pub tab_bar: Option, + /// The color of the "thumb" of the scrollbar; the segment that + /// represents the current viewable area + pub scrollbar_thumb: Option, } impl From for term::color::ColorPalette { @@ -41,6 +44,7 @@ impl From for term::color::ColorPalette { apply_color!(cursor_border); apply_color!(selection_fg); apply_color!(selection_bg); + apply_color!(scrollbar_thumb); if let Some(ansi) = cfg.ansi { for (idx, col) in ansi.iter().enumerate() { diff --git a/src/config/mod.rs b/src/config/mod.rs index 6e653e93b..960726c94 100644 --- a/src/config/mod.rs +++ b/src/config/mod.rs @@ -420,6 +420,9 @@ pub struct Config { #[serde(default = "default_true")] pub enable_tab_bar: bool, + #[serde(default)] + pub enable_scroll_bar: bool, + /// If false, do not try to use a Wayland protocol connection /// when starting the gui frontend, and instead use X11. /// This option is only considered on X11/Wayland systems and diff --git a/src/frontend/gui/quad.rs b/src/frontend/gui/quad.rs index aa95284ae..582d5ebdb 100644 --- a/src/frontend/gui/quad.rs +++ b/src/frontend/gui/quad.rs @@ -117,4 +117,18 @@ impl<'a> Quad<'a> { v.cursor_color = color; } } + + #[allow(unused)] + pub fn get_position(&self) -> (f32, f32, f32, f32) { + let top_left = self.vert[V_TOP_LEFT].position; + let bottom_right = self.vert[V_BOT_RIGHT].position; + (top_left.0, top_left.1, bottom_right.0, bottom_right.1) + } + + pub fn set_position(&mut self, left: f32, top: f32, right: f32, bottom: f32) { + self.vert[V_TOP_LEFT].position = (left, top); + self.vert[V_TOP_RIGHT].position = (right, top); + self.vert[V_BOT_LEFT].position = (left, bottom); + self.vert[V_BOT_RIGHT].position = (right, bottom); + } } diff --git a/src/frontend/gui/renderstate.rs b/src/frontend/gui/renderstate.rs index 022540747..925969591 100644 --- a/src/frontend/gui/renderstate.rs +++ b/src/frontend/gui/renderstate.rs @@ -137,8 +137,9 @@ impl OpenGLRenderState { let mut indices = Vec::new(); let config = configuration(); - let avail_width = (width as usize) - .saturating_sub((config.window_padding.left + config.window_padding.right) as usize); + let padding_right = super::termwindow::effective_right_padding(&config, metrics); + let avail_width = + (width as usize).saturating_sub((config.window_padding.left + padding_right) as usize); let avail_height = (height as usize) .saturating_sub((config.window_padding.top + config.window_padding.bottom) as usize); @@ -197,6 +198,46 @@ impl OpenGLRenderState { } } + { + // And a quad for the scrollbar thumb + let x_pos = (width / 2.0) - cell_width; + let y_pos = (height / -2.0) + padding_top; + let thumb_width = cell_width; + let thumb_height = height; + + // Remember starting index for this position + let idx = verts.len() as u32; + verts.push(Vertex { + // Top left + position: (x_pos, thumb_width), + ..Default::default() + }); + verts.push(Vertex { + // Top Right + position: (x_pos + thumb_width, y_pos), + ..Default::default() + }); + verts.push(Vertex { + // Bottom Left + position: (x_pos, y_pos + thumb_height), + ..Default::default() + }); + verts.push(Vertex { + // Bottom Right + position: (x_pos + thumb_width, y_pos + thumb_height), + ..Default::default() + }); + + // Emit two triangles to form the glyph quad + indices.push(idx + V_TOP_LEFT as u32); + indices.push(idx + V_TOP_RIGHT as u32); + indices.push(idx + V_BOT_LEFT as u32); + + indices.push(idx + V_TOP_RIGHT as u32); + indices.push(idx + V_BOT_LEFT as u32); + indices.push(idx + V_BOT_RIGHT as u32); + } + Ok(( VertexBuffer::dynamic(context, &verts)?, IndexBuffer::new( diff --git a/src/frontend/gui/termwindow.rs b/src/frontend/gui/termwindow.rs index b6c97260b..1c2f55f98 100644 --- a/src/frontend/gui/termwindow.rs +++ b/src/frontend/gui/termwindow.rs @@ -24,7 +24,7 @@ use std::rc::Rc; use std::sync::Arc; use std::time::{Duration, Instant}; use term::color::ColorPalette; -use term::{CursorPosition, Line, Underline}; +use term::{CursorPosition, Line, Underline, VisibleRowIndex}; use termwiz::color::RgbColor; use termwiz::surface::CursorShape; @@ -63,11 +63,13 @@ pub struct TermWindow { render_state: RenderState, keys: KeyMap, show_tab_bar: bool, + show_scroll_bar: bool, tab_bar: TabBarState, last_mouse_coords: (usize, i64), drag_start_coords: Option, config_generation: usize, created_instant: Instant, + last_scroll_info: (VisibleRowIndex, usize), } struct Host<'a> { @@ -251,11 +253,13 @@ impl WindowCallbacks for TermWindow { } } - // When hovering over a hyperlink, show an appropriate - // mouse cursor to give the cue that it is clickable - context.set_cursor(Some(if self.show_tab_bar && y == 0 { + let in_tab_bar = self.show_tab_bar && y == 0; + let in_scroll_bar = self.show_scroll_bar && x >= self.terminal_size.cols as usize; + context.set_cursor(Some(if in_tab_bar || in_scroll_bar { MouseCursor::Arrow } else if tab.renderer().current_highlight().is_some() { + // When hovering over a hyperlink, show an appropriate + // mouse cursor to give the cue that it is clickable MouseCursor::Hand } else { MouseCursor::Text @@ -397,6 +401,18 @@ impl WindowCallbacks for TermWindow { } } +/// Computes the effective padding for the RHS. +/// This is needed because the default is 0, but if the user has +/// enabled the scroll bar then they will expect it to have a reasonable +/// size unless they've specified differently. +pub fn effective_right_padding(config: &ConfigHandle, render_metrics: &RenderMetrics) -> u16 { + if config.enable_scroll_bar && config.window_padding.right == 0 { + render_metrics.cell_size.width as u16 + } else { + config.window_padding.right as u16 + } +} + impl TermWindow { pub fn new_window( config: &ConfigHandle, @@ -416,13 +432,12 @@ impl TermWindow { }; let rows_with_tab_bar = if config.enable_tab_bar { 1 } else { 0 } + terminal_size.rows; - // Accomodating future scroll bar UI - let cols_with_scroll_bar = terminal_size.cols; let dimensions = Dimensions { - pixel_width: ((cols_with_scroll_bar * render_metrics.cell_size.width as u16) + pixel_width: ((terminal_size.cols * render_metrics.cell_size.width as u16) + config.window_padding.left - + config.window_padding.right) as usize, + + effective_right_padding(&config, &render_metrics)) + as usize, pixel_height: ((rows_with_tab_bar * render_metrics.cell_size.height as u16) + config.window_padding.top + config.window_padding.bottom) as usize, @@ -458,11 +473,13 @@ impl TermWindow { render_state, keys: KeyMap::new(), show_tab_bar: config.enable_tab_bar, + show_scroll_bar: config.enable_scroll_bar, tab_bar: TabBarState::default(), last_mouse_coords: (0, -1), drag_start_coords: None, config_generation: config.generation(), created_instant: Instant::now(), + last_scroll_info: (0, 0), }), )?; @@ -701,6 +718,7 @@ impl TermWindow { let config = configuration(); self.show_tab_bar = config.enable_tab_bar; + self.show_scroll_bar = config.enable_scroll_bar; self.keys = KeyMap::new(); self.config_generation = config.generation(); let dimensions = self.dimensions; @@ -712,6 +730,29 @@ impl TermWindow { } } + fn update_scrollbar(&mut self) { + if !self.show_scroll_bar { + return; + } + + let mux = Mux::get().unwrap(); + let tab = match mux.get_active_tab_for_window(self.mux_window_id) { + Some(tab) => tab, + None => return, + }; + + let info = tab.renderer().get_scrollbar_info(); + if info == self.last_scroll_info { + return; + } + + self.last_scroll_info = info; + + if let Some(window) = self.window.as_ref() { + window.invalidate(); + } + } + fn update_title(&mut self) { let mux = Mux::get().unwrap(); let window = match mux.get_window(self.mux_window_id) { @@ -792,6 +833,7 @@ impl TermWindow { drop(window); self.update_title(); + self.update_scrollbar(); } Ok(()) } @@ -971,7 +1013,7 @@ impl TermWindow { + (config.window_padding.top + config.window_padding.bottom); let pixel_width = (cols * self.render_metrics.cell_size.width as u16) - + (config.window_padding.left + config.window_padding.right); + + (config.window_padding.left + self.effective_right_padding(&config)); let dims = Dimensions { pixel_width: pixel_width as usize, @@ -983,7 +1025,7 @@ impl TermWindow { } else { // Resize of the window dimensions may result in changed terminal dimensions let avail_width = dimensions.pixel_width - - (config.window_padding.left + config.window_padding.right) as usize; + - (config.window_padding.left + self.effective_right_padding(&config)) as usize; let avail_height = dimensions.pixel_height - (config.window_padding.top + config.window_padding.bottom) as usize; @@ -1153,24 +1195,57 @@ impl TermWindow { ), bg, ); - // right padding + // right padding / scroll bar + let padding_right = self.effective_right_padding(&config); + ctx.clear_rect( Rect::new( Point::new( - (self.dimensions.pixel_width - config.window_padding.right as usize) as isize, + (self.dimensions.pixel_width - padding_right as usize) as isize, config.window_padding.top as isize, ), Size::new( - config.window_padding.right as isize, + padding_right as isize, (self.dimensions.pixel_height - config.window_padding.top as usize) as isize, ), ), bg, ); + if self.show_scroll_bar { + let (scroll_top, scroll_size) = term.get_scrollbar_info(); + let thumb_size = (self.terminal_size.rows as f32 / scroll_size as f32) + * self.terminal_size.pixel_height as f32; + let thumb_top = (1. + - (scroll_top + self.terminal_size.rows as i64) as f32 / scroll_size as f32) + * self.terminal_size.pixel_height as f32; + + let thumb_size = thumb_size.ceil() as isize; + let thumb_top = thumb_top.ceil() as isize; + + ctx.clear_rect( + Rect::new( + Point::new( + (self + .dimensions + .pixel_width + .saturating_sub(padding_right as usize)) + as isize, + thumb_top + config.window_padding.top as isize, + ), + Size::new(padding_right as isize, thumb_top + thumb_size), + ), + rgbcolor_to_window_color(palette.scrollbar_thumb), + ); + } + Ok(()) } + fn effective_right_padding(&self, config: &ConfigHandle) -> u16 { + effective_right_padding(config, &self.render_metrics) + } + fn paint_tab_opengl( &mut self, tab: &Rc, @@ -1205,6 +1280,35 @@ impl TermWindow { )?; } + if self.show_scroll_bar { + let (scroll_top, scroll_size) = term.get_scrollbar_info(); + let thumb_size = (self.terminal_size.rows as f32 / scroll_size as f32) + * self.terminal_size.pixel_height as f32; + let thumb_top = (1. + - (scroll_top + self.terminal_size.rows as i64) as f32 / scroll_size as f32) + * self.terminal_size.pixel_height as f32; + let gl_state = self.render_state.opengl(); + + // We reserved the final quad in the vertex buffer as the scrollbar + let mut vb = gl_state.glyph_vertex_buffer.borrow_mut(); + let num_vert = vb.len() - VERTICES_PER_CELL; + let mut vertices = &mut vb.slice_mut(..).unwrap().map(); + let mut quad = Quad::for_cell(num_vert / VERTICES_PER_CELL, &mut vertices); + + // Adjust the scrollbar thumb position + let top = (self.dimensions.pixel_height as f32 / -2.0) + thumb_top; + let bottom = top + thumb_size; + + let config = configuration(); + let padding = self.effective_right_padding(&config) as f32; + + let right = self.dimensions.pixel_width as f32 / 2.; + let left = right - padding; + + quad.set_bg_color(rgbcolor_to_window_color(palette.scrollbar_thumb)); + quad.set_position(left, top, right, bottom); + } + { let dirty_lines = term.get_dirty_lines(); diff --git a/src/mux/renderable.rs b/src/mux/renderable.rs index 6611dad7d..1d67979be 100644 --- a/src/mux/renderable.rs +++ b/src/mux/renderable.rs @@ -2,7 +2,7 @@ use downcast_rs::{impl_downcast, Downcast}; use std::borrow::Cow; use std::ops::Range; use std::sync::Arc; -use term::{CursorPosition, Line, Terminal, TerminalState}; +use term::{CursorPosition, Line, Terminal, TerminalState, VisibleRowIndex}; use termwiz::hyperlink::Hyperlink; /// Renderable allows passing something that isn't an actual term::Terminal @@ -33,6 +33,11 @@ pub trait Renderable: Downcast { /// Returns physical, non-scrollback (rows, cols) for the /// terminal screen fn physical_dimensions(&self) -> (usize, usize); + + /// Returns the potentially scrolled viewport offset, and the + /// size of the scrollback. This information is intended to be + /// used to render a scrollbar UI + fn get_scrollbar_info(&self) -> (VisibleRowIndex, usize); } impl_downcast!(Renderable); @@ -68,4 +73,10 @@ impl Renderable for Terminal { fn has_dirty_lines(&self) -> bool { TerminalState::has_dirty_lines(self) } + + fn get_scrollbar_info(&self) -> (VisibleRowIndex, usize) { + let offset = self.get_viewport_offset(); + let num_lines = self.screen().lines.len(); + (offset, num_lines) + } } diff --git a/src/server/tab.rs b/src/server/tab.rs index 215daab0e..0ec6b1172 100644 --- a/src/server/tab.rs +++ b/src/server/tab.rs @@ -20,7 +20,7 @@ use std::sync::{Arc, Mutex}; use std::time::{Duration, Instant}; use term::color::ColorPalette; use term::selection::SelectionRange; -use term::{Clipboard, CursorPosition, Line}; +use term::{Clipboard, CursorPosition, Line, VisibleRowIndex}; use term::{KeyCode, KeyModifiers, MouseButton, MouseEvent, MouseEventKind, TerminalHost}; use termwiz::hyperlink::Hyperlink; use termwiz::input::KeyEvent; @@ -468,6 +468,13 @@ impl Renderable for RenderableState { let (cols, rows) = self.inner.borrow().surface.dimensions(); (rows, cols) } + + fn get_scrollbar_info(&self) -> (VisibleRowIndex, usize) { + // Dummy scrollback information for now, until we + // plumb this into the protocol + let (_cols, rows) = self.physical_dimensions(); + (0, rows) + } } struct TabWriter { diff --git a/src/termwiztermtab.rs b/src/termwiztermtab.rs index 2163df66a..5adb37ee5 100644 --- a/src/termwiztermtab.rs +++ b/src/termwiztermtab.rs @@ -25,7 +25,9 @@ use std::sync::{Arc, Mutex}; use std::time::Duration; use term::color::ColorPalette; use term::selection::SelectionRange; -use term::{CursorPosition, KeyCode, KeyModifiers, Line, MouseEvent, TerminalHost}; +use term::{ + CursorPosition, KeyCode, KeyModifiers, Line, MouseEvent, TerminalHost, VisibleRowIndex, +}; use termwiz::hyperlink::Hyperlink; use termwiz::input::{InputEvent, KeyEvent}; use termwiz::lineedit::*; @@ -142,6 +144,11 @@ impl Renderable for RenderableState { let (cols, rows) = self.inner.borrow().surface.dimensions(); (rows, cols) } + + fn get_scrollbar_info(&self) -> (VisibleRowIndex, usize) { + let (_cols, rows) = self.physical_dimensions(); + (0, rows) + } } struct TermWizTerminalDomain { diff --git a/term/src/color.rs b/term/src/color.rs index 6c1904bf8..c9383d56a 100644 --- a/term/src/color.rs +++ b/term/src/color.rs @@ -17,6 +17,7 @@ pub struct ColorPalette { pub cursor_border: RgbColor, pub selection_fg: RgbColor, pub selection_bg: RgbColor, + pub scrollbar_thumb: RgbColor, } impl fmt::Debug for Palette256 { @@ -187,6 +188,8 @@ impl Default for ColorPalette { let selection_fg = colors[AnsiColor::Black as usize]; let selection_bg = RgbColor::new(0xff, 0xfa, 0xcd); + let scrollbar_thumb = RgbColor::new(0x44, 0x44, 0x44); + ColorPalette { colors: Palette256(colors), foreground, @@ -196,6 +199,7 @@ impl Default for ColorPalette { cursor_border, selection_fg, selection_bg, + scrollbar_thumb, } } }