Allow to scroll diffs horizontally (#1327)

This commit is contained in:
Christoph Rüßler 2023-01-08 12:47:37 +01:00 committed by extrawurst
parent 4c7c954337
commit a55cfc2754
18 changed files with 343 additions and 70 deletions

View file

@ -90,6 +90,7 @@ Bugfix followup release - check `0.22.0` notes for more infos!
* switch focus to index after staging last file ([#1169](https://github.com/extrawurst/gitui/pull/1169)) * switch focus to index after staging last file ([#1169](https://github.com/extrawurst/gitui/pull/1169))
* fix stashlist multi marking not updated after dropping ([#1207](https://github.com/extrawurst/gitui/pull/1207)) * fix stashlist multi marking not updated after dropping ([#1207](https://github.com/extrawurst/gitui/pull/1207))
* exact matches have a higher priority and are placed to the top of the list when fuzzily finding files ([#1183](https://github.com/extrawurst/gitui/pull/1183)) * exact matches have a higher priority and are placed to the top of the list when fuzzily finding files ([#1183](https://github.com/extrawurst/gitui/pull/1183))
* support horizontal scrolling in diff view ([#1017](https://github.com/extrawurst/gitui/issues/1017))
### Changed ### Changed
* minimum supported rust version bumped to 1.60 ([#1279](https://github.com/extrawurst/gitui/pull/1279)) * minimum supported rust version bumped to 1.60 ([#1279](https://github.com/extrawurst/gitui/pull/1279))

View file

@ -123,6 +123,7 @@ impl DrawableComponent for BlameFileComponent {
// //
// https://github.com/fdehau/tui-rs/issues/448 // https://github.com/fdehau/tui-rs/issues/448
table_state.selected().unwrap_or(0), table_state.selected().unwrap_or(0),
ui::Orientation::Vertical,
); );
self.table_state.set(table_state); self.table_state.set(table_state);

View file

@ -8,7 +8,7 @@ use crate::{
queue::{InternalEvent, Queue}, queue::{InternalEvent, Queue},
strings::{self, symbol}, strings::{self, symbol},
ui::style::{SharedTheme, Theme}, ui::style::{SharedTheme, Theme},
ui::{calc_scroll_top, draw_scrollbar}, ui::{calc_scroll_top, draw_scrollbar, Orientation},
}; };
use anyhow::Result; use anyhow::Result;
use asyncgit::sync::{BranchInfo, CommitId, Tags}; use asyncgit::sync::{BranchInfo, CommitId, Tags};
@ -501,6 +501,7 @@ impl DrawableComponent for CommitList {
&self.theme, &self.theme,
self.count_total, self.count_total,
self.selection, self.selection,
Orientation::Vertical,
); );
Ok(()) Ok(())

View file

@ -44,7 +44,7 @@ impl DrawableComponent for CompareCommitsComponent {
) -> Result<()> { ) -> Result<()> {
if self.is_visible() { if self.is_visible() {
let percentages = if self.diff.focused() { let percentages = if self.diff.focused() {
(30, 70) (0, 100)
} else { } else {
(50, 50) (50, 50)
}; };
@ -121,7 +121,12 @@ impl Component for CompareCommitsComponent {
if let Event::Key(e) = ev { if let Event::Key(e) = ev {
if key_match(e, self.key_config.keys.exit_popup) { if key_match(e, self.key_config.keys.exit_popup) {
self.hide_stacked(false); if self.diff.focused() {
self.details.focus(true);
self.diff.focus(false);
} else {
self.hide_stacked(false);
}
} else if key_match( } else if key_match(
e, e,
self.key_config.keys.focus_right, self.key_config.keys.focus_right,
@ -132,13 +137,6 @@ impl Component for CompareCommitsComponent {
} else if key_match( } else if key_match(
e, e,
self.key_config.keys.focus_left, self.key_config.keys.focus_left,
) && self.diff.focused()
{
self.details.focus(true);
self.diff.focus(false);
} else if key_match(
e,
self.key_config.keys.focus_left,
) { ) {
self.hide_stacked(false); self.hide_stacked(false);
} }

View file

@ -1,12 +1,14 @@
use super::{ use super::{
utils::scroll_horizontal::HorizontalScroll,
utils::scroll_vertical::VerticalScroll, CommandBlocking, utils::scroll_vertical::VerticalScroll, CommandBlocking,
Direction, DrawableComponent, ScrollType, Direction, DrawableComponent, HorizontalScrollType, ScrollType,
}; };
use crate::{ use crate::{
components::{CommandInfo, Component, EventState}, components::{CommandInfo, Component, EventState},
keys::{key_match, SharedKeyConfig}, keys::{key_match, SharedKeyConfig},
queue::{Action, InternalEvent, NeedsUpdate, Queue, ResetItem}, queue::{Action, InternalEvent, NeedsUpdate, Queue, ResetItem},
string_utils::tabs_to_spaces, string_utils::tabs_to_spaces,
string_utils::trim_offset,
strings, try_or_popup, strings, try_or_popup,
ui::style::SharedTheme, ui::style::SharedTheme,
}; };
@ -102,13 +104,15 @@ impl Selection {
pub struct DiffComponent { pub struct DiffComponent {
repo: RepoPathRef, repo: RepoPathRef,
diff: Option<FileDiff>, diff: Option<FileDiff>,
longest_line: usize,
pending: bool, pending: bool,
selection: Selection, selection: Selection,
selected_hunk: Option<usize>, selected_hunk: Option<usize>,
current_size: Cell<(u16, u16)>, current_size: Cell<(u16, u16)>,
focused: bool, focused: bool,
current: Current, current: Current,
scroll: VerticalScroll, vertical_scroll: VerticalScroll,
horizontal_scroll: HorizontalScroll,
queue: Queue, queue: Queue,
theme: SharedTheme, theme: SharedTheme,
key_config: SharedKeyConfig, key_config: SharedKeyConfig,
@ -131,9 +135,11 @@ impl DiffComponent {
pending: false, pending: false,
selected_hunk: None, selected_hunk: None,
diff: None, diff: None,
longest_line: 0,
current_size: Cell::new((0, 0)), current_size: Cell::new((0, 0)),
selection: Selection::Single(0), selection: Selection::Single(0),
scroll: VerticalScroll::new(), vertical_scroll: VerticalScroll::new(),
horizontal_scroll: HorizontalScroll::new(),
theme, theme,
key_config, key_config,
is_immutable, is_immutable,
@ -155,7 +161,9 @@ impl DiffComponent {
pub fn clear(&mut self, pending: bool) { pub fn clear(&mut self, pending: bool) {
self.current = Current::default(); self.current = Current::default();
self.diff = None; self.diff = None;
self.scroll.reset(); self.longest_line = 0;
self.vertical_scroll.reset();
self.horizontal_scroll.reset();
self.selection = Selection::Single(0); self.selection = Selection::Single(0);
self.selected_hunk = None; self.selected_hunk = None;
self.pending = pending; self.pending = pending;
@ -182,8 +190,27 @@ impl DiffComponent {
self.diff = Some(diff); self.diff = Some(diff);
self.longest_line = self
.diff
.iter()
.flat_map(|diff| diff.hunks.iter())
.flat_map(|hunk| hunk.lines.iter())
.map(|line| {
let converted_content = tabs_to_spaces(
line.content.as_ref().to_string(),
);
converted_content.len()
})
.max()
.map_or(0, |len| {
// Each hunk uses a 1-character wide vertical bar to its left to indicate
// selection.
len + 1
});
if reset_selection { if reset_selection {
self.scroll.reset(); self.vertical_scroll.reset();
self.selection = Selection::Single(0); self.selection = Selection::Single(0);
self.update_selection(0); self.update_selection(0);
} else { } else {
@ -241,6 +268,11 @@ impl DiffComponent {
self.diff.as_ref().map_or(0, |diff| diff.lines) self.diff.as_ref().map_or(0, |diff| diff.lines)
} }
fn max_scroll_right(&self) -> usize {
self.longest_line
.saturating_sub(self.current_size.get().0.into())
}
fn modify_selection(&mut self, direction: Direction) { fn modify_selection(&mut self, direction: Direction) {
if self.diff.is_some() { if self.diff.is_some() {
self.selection.modify(direction, self.lines_count()); self.selection.modify(direction, self.lines_count());
@ -340,7 +372,7 @@ impl DiffComponent {
Span::raw(Cow::from(")")), Span::raw(Cow::from(")")),
])]); ])]);
} else { } else {
let min = self.scroll.get_top(); let min = self.vertical_scroll.get_top();
let max = min + height as usize; let max = min + height as usize;
let mut line_cursor = 0_usize; let mut line_cursor = 0_usize;
@ -378,6 +410,8 @@ impl DiffComponent {
hunk_selected, hunk_selected,
i == hunk_len - 1, i == hunk_len - 1,
&self.theme, &self.theme,
self.horizontal_scroll
.get_right(),
)); ));
lines_added += 1; lines_added += 1;
} }
@ -400,6 +434,7 @@ impl DiffComponent {
selected_hunk: bool, selected_hunk: bool,
end_of_hunk: bool, end_of_hunk: bool,
theme: &SharedTheme, theme: &SharedTheme,
scrolled_right: usize,
) -> Spans<'a> { ) -> Spans<'a> {
let style = theme.diff_hunk_marker(selected_hunk); let style = theme.diff_hunk_marker(selected_hunk);
@ -418,18 +453,22 @@ impl DiffComponent {
} }
}; };
let content =
tabs_to_spaces(line.content.as_ref().to_string());
let content = trim_offset(&content, scrolled_right);
let filled = if selected { let filled = if selected {
// selected line // selected line
format!("{:w$}\n", line.content, w = width as usize) format!("{content:w$}\n", w = width as usize)
} else { } else {
// weird eof missing eol line // weird eof missing eol line
format!("{}\n", line.content) format!("{content}\n")
}; };
Spans::from(vec![ Spans::from(vec![
left_side_of_line, left_side_of_line,
Span::styled( Span::styled(
Cow::from(tabs_to_spaces(filled)), Cow::from(filled),
theme.diff_line(line.line_type, selected), theme.diff_line(line.line_type, selected),
), ),
]) ])
@ -606,14 +645,20 @@ impl DrawableComponent for DiffComponent {
r.height.saturating_sub(2), r.height.saturating_sub(2),
)); ));
let current_width = self.current_size.get().0;
let current_height = self.current_size.get().1; let current_height = self.current_size.get().1;
self.scroll.update( self.vertical_scroll.update(
self.selection.get_end(), self.selection.get_end(),
self.lines_count(), self.lines_count(),
usize::from(current_height), usize::from(current_height),
); );
self.horizontal_scroll.update_no_selection(
self.longest_line,
current_width.into(),
);
let title = format!( let title = format!(
"{}{}", "{}{}",
strings::title_diff(&self.key_config), strings::title_diff(&self.key_config),
@ -643,7 +688,11 @@ impl DrawableComponent for DiffComponent {
); );
if self.focused() { if self.focused() {
self.scroll.draw(f, r, &self.theme); self.vertical_scroll.draw(f, r, &self.theme);
if self.max_scroll_right() > 0 {
self.horizontal_scroll.draw(f, r, &self.theme);
}
} }
Ok(()) Ok(())
@ -754,6 +803,18 @@ impl Component for DiffComponent {
{ {
self.move_selection(ScrollType::PageDown); self.move_selection(ScrollType::PageDown);
Ok(EventState::Consumed) Ok(EventState::Consumed)
} else if key_match(
e,
self.key_config.keys.move_right,
) {
self.horizontal_scroll
.move_right(HorizontalScrollType::Right);
Ok(EventState::Consumed)
} else if key_match(e, self.key_config.keys.move_left)
{
self.horizontal_scroll
.move_right(HorizontalScrollType::Left);
Ok(EventState::Consumed)
} else if key_match( } else if key_match(
e, e,
self.key_config.keys.stage_unstage_item, self.key_config.keys.stage_unstage_item,

View file

@ -11,7 +11,7 @@ use crate::{
keys::SharedKeyConfig, keys::SharedKeyConfig,
queue::{InternalEvent, NeedsUpdate, Queue}, queue::{InternalEvent, NeedsUpdate, Queue},
strings, strings,
ui::{draw_scrollbar, style::SharedTheme}, ui::{draw_scrollbar, style::SharedTheme, Orientation},
}; };
use anyhow::Result; use anyhow::Result;
use asyncgit::{ use asyncgit::{
@ -412,6 +412,7 @@ impl FileRevlogComponent {
&self.theme, &self.theme,
self.count_total, self.count_total,
table_state.selected().unwrap_or(0), table_state.selected().unwrap_or(0),
Orientation::Vertical,
); );
self.table_state.set(table_state); self.table_state.set(table_state);
@ -445,7 +446,7 @@ impl DrawableComponent for FileRevlogComponent {
) -> Result<()> { ) -> Result<()> {
if self.visible { if self.visible {
let percentages = if self.diff.focused() { let percentages = if self.diff.focused() {
(30, 70) (0, 100)
} else { } else {
(50, 50) (50, 50)
}; };
@ -485,20 +486,17 @@ impl Component for FileRevlogComponent {
if let Event::Key(key) = event { if let Event::Key(key) = event {
if key_match(key, self.key_config.keys.exit_popup) { if key_match(key, self.key_config.keys.exit_popup) {
self.hide_stacked(false); if self.diff.focused() {
self.diff.focus(false);
} else {
self.hide_stacked(false);
}
} else if key_match( } else if key_match(
key, key,
self.key_config.keys.focus_right, self.key_config.keys.focus_right,
) && self.can_focus_diff() ) && self.can_focus_diff()
{ {
self.diff.focus(true); self.diff.focus(true);
} else if key_match(
key,
self.key_config.keys.focus_left,
) {
if self.diff.focused() {
self.diff.focus(false);
}
} else if key_match(key, self.key_config.keys.enter) { } else if key_match(key, self.key_config.keys.enter) {
if let Some(commit_id) = self.selected_commit() { if let Some(commit_id) = self.selected_commit() {
self.hide_stacked(true); self.hide_stacked(true);

View file

@ -71,7 +71,7 @@ impl DrawableComponent for InspectCommitComponent {
) -> Result<()> { ) -> Result<()> {
if self.is_visible() { if self.is_visible() {
let percentages = if self.diff.focused() { let percentages = if self.diff.focused() {
(30, 70) (0, 100)
} else { } else {
(50, 50) (50, 50)
}; };
@ -126,7 +126,7 @@ impl Component for InspectCommitComponent {
)); ));
out.push(CommandInfo::new( out.push(CommandInfo::new(
strings::commands::diff_focus_left(&self.key_config), strings::commands::close_popup(&self.key_config),
true, true,
self.diff.focused() || force_all, self.diff.focused() || force_all,
)); ));
@ -157,7 +157,12 @@ impl Component for InspectCommitComponent {
if let Event::Key(e) = ev { if let Event::Key(e) = ev {
if key_match(e, self.key_config.keys.exit_popup) { if key_match(e, self.key_config.keys.exit_popup) {
self.hide_stacked(false); if self.diff.focused() {
self.details.focus(true);
self.diff.focus(false);
} else {
self.hide_stacked(false);
}
} else if key_match( } else if key_match(
e, e,
self.key_config.keys.focus_right, self.key_config.keys.focus_right,
@ -168,13 +173,6 @@ impl Component for InspectCommitComponent {
} else if key_match( } else if key_match(
e, e,
self.key_config.keys.focus_left, self.key_config.keys.focus_left,
) && self.diff.focused()
{
self.details.focus(true);
self.diff.focus(false);
} else if key_match(
e,
self.key_config.keys.focus_left,
) { ) {
self.hide_stacked(false); self.hide_stacked(false);
} }

View file

@ -185,6 +185,12 @@ pub enum ScrollType {
PageDown, PageDown,
} }
#[derive(Copy, Clone)]
pub enum HorizontalScrollType {
Left,
Right,
}
#[derive(Copy, Clone)] #[derive(Copy, Clone)]
pub enum Direction { pub enum Direction {
Up, Up,

View file

@ -238,6 +238,7 @@ impl DrawableComponent for SyntaxTextComponent {
state.height().saturating_sub(2), state.height().saturating_sub(2),
)), )),
usize::from(state.scroll().y), usize::from(state.scroll().y),
ui::Orientation::Vertical,
); );
} }

View file

@ -128,6 +128,7 @@ impl DrawableComponent for TagListComponent {
&self.theme, &self.theme,
number_of_rows, number_of_rows,
table_state.selected().unwrap_or(0), table_state.selected().unwrap_or(0),
ui::Orientation::Vertical,
); );
self.table_state.set(table_state); self.table_state.set(table_state);

View file

@ -5,6 +5,7 @@ use unicode_width::UnicodeWidthStr;
pub mod emoji; pub mod emoji;
pub mod filetree; pub mod filetree;
pub mod logitems; pub mod logitems;
pub mod scroll_horizontal;
pub mod scroll_vertical; pub mod scroll_vertical;
pub mod statustree; pub mod statustree;

View file

@ -0,0 +1,140 @@
use std::cell::Cell;
use tui::{backend::Backend, layout::Rect, Frame};
use crate::{
components::HorizontalScrollType,
ui::{draw_scrollbar, style::SharedTheme, Orientation},
};
pub struct HorizontalScroll {
right: Cell<usize>,
max_right: Cell<usize>,
}
impl HorizontalScroll {
pub const fn new() -> Self {
Self {
right: Cell::new(0),
max_right: Cell::new(0),
}
}
pub fn get_right(&self) -> usize {
self.right.get()
}
pub fn reset(&self) {
self.right.set(0);
}
pub fn move_right(
&self,
move_type: HorizontalScrollType,
) -> bool {
let old = self.right.get();
let max = self.max_right.get();
let new_scroll_right = match move_type {
HorizontalScrollType::Left => old.saturating_sub(1),
HorizontalScrollType::Right => old.saturating_add(1),
};
let new_scroll_right = new_scroll_right.clamp(0, max);
if new_scroll_right == old {
return false;
}
self.right.set(new_scroll_right);
true
}
pub fn update(
&self,
selection: usize,
max_selection: usize,
visual_width: usize,
) -> usize {
let new_right = calc_scroll_right(
self.get_right(),
visual_width,
selection,
max_selection,
);
self.right.set(new_right);
if visual_width == 0 {
self.max_right.set(0);
} else {
let new_max_right =
max_selection.saturating_sub(visual_width);
self.max_right.set(new_max_right);
}
new_right
}
pub fn update_no_selection(
&self,
column_count: usize,
visual_width: usize,
) -> usize {
self.update(self.get_right(), column_count, visual_width)
}
pub fn draw<B: Backend>(
&self,
f: &mut Frame<B>,
r: Rect,
theme: &SharedTheme,
) {
draw_scrollbar(
f,
r,
theme,
self.max_right.get(),
self.right.get(),
Orientation::Horizontal,
);
}
}
const fn calc_scroll_right(
current_right: usize,
width_in_lines: usize,
selection: usize,
selection_max: usize,
) -> usize {
if width_in_lines == 0 {
return 0;
}
if selection_max <= width_in_lines {
return 0;
}
if current_right + width_in_lines <= selection {
selection.saturating_sub(width_in_lines) + 1
} else if current_right > selection {
selection
} else {
current_right
}
}
#[cfg(test)]
mod tests {
use super::*;
use pretty_assertions::assert_eq;
#[test]
fn test_scroll_no_scroll_to_right() {
assert_eq!(calc_scroll_right(1, 10, 4, 4), 0);
}
#[test]
fn test_scroll_zero_width() {
assert_eq!(calc_scroll_right(4, 0, 4, 3), 0);
}
}

View file

@ -4,7 +4,7 @@ use tui::{backend::Backend, layout::Rect, Frame};
use crate::{ use crate::{
components::ScrollType, components::ScrollType,
ui::{draw_scrollbar, style::SharedTheme}, ui::{draw_scrollbar, style::SharedTheme, Orientation},
}; };
pub struct VerticalScroll { pub struct VerticalScroll {
@ -95,6 +95,7 @@ impl VerticalScroll {
theme, theme,
self.max_top.get(), self.max_top.get(),
self.top.get(), self.top.get(),
Orientation::Vertical,
); );
} }
} }

View file

@ -1,3 +1,6 @@
use unicode_segmentation::UnicodeSegmentation;
use unicode_width::UnicodeWidthStr;
/// ///
pub fn trim_length_left(s: &str, width: usize) -> &str { pub fn trim_length_left(s: &str, width: usize) -> &str {
let len = s.len(); let len = s.len();
@ -21,6 +24,22 @@ pub fn tabs_to_spaces(input: String) -> String {
} }
} }
/// This function will return a str slice which start at specified offset.
/// As src is a unicode str, start offset has to be calculated with each character.
pub fn trim_offset(src: &str, mut offset: usize) -> &str {
let mut start = 0;
for c in UnicodeSegmentation::graphemes(src, true) {
let w = c.width();
if w <= offset {
offset -= w;
start += c.len();
} else {
break;
}
}
&src[start..]
}
#[cfg(test)] #[cfg(test)]
mod test { mod test {
use pretty_assertions::assert_eq; use pretty_assertions::assert_eq;

View file

@ -105,8 +105,8 @@ impl DrawableComponent for Status {
.constraints( .constraints(
if self.focus == Focus::Diff { if self.focus == Focus::Diff {
[ [
Constraint::Percentage(30), Constraint::Percentage(0),
Constraint::Percentage(70), Constraint::Percentage(100),
] ]
} else { } else {
[ [
@ -680,7 +680,7 @@ impl Status {
let focus_on_diff = self.is_focus_on_diff(); let focus_on_diff = self.is_focus_on_diff();
out.push( out.push(
CommandInfo::new( CommandInfo::new(
strings::commands::diff_focus_left(&self.key_config), strings::commands::close_popup(&self.key_config),
true, true,
(self.visible && focus_on_diff) || force_all, (self.visible && focus_on_diff) || force_all,
) )
@ -858,7 +858,7 @@ impl Component for Status {
self.switch_focus(Focus::Diff).map(Into::into) self.switch_focus(Focus::Diff).map(Into::into)
} else if key_match( } else if key_match(
k, k,
self.key_config.keys.focus_left, self.key_config.keys.exit_popup,
) { ) {
self.switch_focus(match self.diff_target { self.switch_focus(match self.diff_target {
DiffTarget::Stage => Focus::Stage, DiffTarget::Stage => Focus::Stage,

View file

@ -6,7 +6,7 @@ pub mod style;
mod syntax_text; mod syntax_text;
use filetreelist::MoveSelection; use filetreelist::MoveSelection;
pub use scrollbar::draw_scrollbar; pub use scrollbar::{draw_scrollbar, Orientation};
pub use scrolllist::{draw_list, draw_list_block}; pub use scrolllist::{draw_list, draw_list_block};
pub use stateful_paragraph::{ pub use stateful_paragraph::{
ParagraphState, ScrollPos, StatefulParagraph, ParagraphState, ScrollPos, StatefulParagraph,

View file

@ -1,6 +1,6 @@
use crate::string_utils::trim_offset;
use easy_cast::Cast; use easy_cast::Cast;
use tui::text::StyledGrapheme; use tui::text::StyledGrapheme;
use unicode_segmentation::UnicodeSegmentation;
use unicode_width::UnicodeWidthStr; use unicode_width::UnicodeWidthStr;
const NBSP: &str = "\u{00a0}"; const NBSP: &str = "\u{00a0}";
@ -233,22 +233,6 @@ impl<'a, 'b> LineComposer<'a> for LineTruncator<'a, 'b> {
} }
} }
/// This function will return a str slice which start at specified offset.
/// As src is a unicode str, start offset has to be calculated with each character.
fn trim_offset(src: &str, mut offset: usize) -> &str {
let mut start = 0;
for c in UnicodeSegmentation::graphemes(src, true) {
let w = c.width();
if w <= offset {
offset -= w;
start += c.len();
} else {
break;
}
}
&src[start..]
}
#[cfg(test)] #[cfg(test)]
mod test { mod test {
use super::*; use super::*;

View file

@ -6,32 +6,40 @@ use tui::{
buffer::Buffer, buffer::Buffer,
layout::{Margin, Rect}, layout::{Margin, Rect},
style::Style, style::Style,
symbols::{block::FULL, line::DOUBLE_VERTICAL}, symbols::{
block::FULL,
line::{DOUBLE_HORIZONTAL, DOUBLE_VERTICAL},
},
widgets::Widget, widgets::Widget,
Frame, Frame,
}; };
pub enum Orientation {
Vertical,
Horizontal,
}
/// ///
struct Scrollbar { struct Scrollbar {
max: u16, max: u16,
pos: u16, pos: u16,
style_bar: Style, style_bar: Style,
style_pos: Style, style_pos: Style,
orientation: Orientation,
} }
impl Scrollbar { impl Scrollbar {
fn new(max: usize, pos: usize) -> Self { fn new(max: usize, pos: usize, orientation: Orientation) -> Self {
Self { Self {
max: u16::try_from(max).unwrap_or_default(), max: u16::try_from(max).unwrap_or_default(),
pos: u16::try_from(pos).unwrap_or_default(), pos: u16::try_from(pos).unwrap_or_default(),
style_pos: Style::default(), style_pos: Style::default(),
style_bar: Style::default(), style_bar: Style::default(),
orientation,
} }
} }
}
impl Widget for Scrollbar { fn render_vertical(self, area: Rect, buf: &mut Buffer) {
fn render(self, area: Rect, buf: &mut Buffer) {
if area.height <= 2 { if area.height <= 2 {
return; return;
} }
@ -67,6 +75,59 @@ impl Widget for Scrollbar {
buf.set_string(right, bar_top + pos, FULL, self.style_pos); buf.set_string(right, bar_top + pos, FULL, self.style_pos);
} }
fn render_horizontal(self, area: Rect, buf: &mut Buffer) {
if area.width <= 2 {
return;
}
if self.max == 0 {
return;
}
let bottom = area.bottom().saturating_sub(1);
if bottom <= area.top() {
return;
};
let (bar_left, bar_width) = {
let scrollbar_area = area.inner(&Margin {
horizontal: 1,
vertical: 0,
});
(scrollbar_area.left(), scrollbar_area.width)
};
for x in bar_left..(bar_left + bar_width) {
buf.set_string(
x,
bottom,
DOUBLE_HORIZONTAL,
self.style_bar,
);
}
let progress = f32::from(self.pos) / f32::from(self.max);
let progress = if progress > 1.0 { 1.0 } else { progress };
let pos = f32::from(bar_width) * progress;
let pos: u16 = pos.cast_nearest();
let pos = pos.saturating_sub(1);
buf.set_string(bar_left + pos, bottom, FULL, self.style_pos);
}
}
impl Widget for Scrollbar {
fn render(self, area: Rect, buf: &mut Buffer) {
match &self.orientation {
Orientation::Vertical => self.render_vertical(area, buf),
Orientation::Horizontal => {
self.render_horizontal(area, buf);
}
}
}
} }
pub fn draw_scrollbar<B: Backend>( pub fn draw_scrollbar<B: Backend>(
@ -75,8 +136,9 @@ pub fn draw_scrollbar<B: Backend>(
theme: &SharedTheme, theme: &SharedTheme,
max: usize, max: usize,
pos: usize, pos: usize,
orientation: Orientation,
) { ) {
let mut widget = Scrollbar::new(max, pos); let mut widget = Scrollbar::new(max, pos, orientation);
widget.style_pos = theme.scroll_bar_pos(); widget.style_pos = theme.scroll_bar_pos();
f.render_widget(widget, r); f.render_widget(widget, r);
} }