From e4afecb62c34bb63e552887e7b56cf8ad9bb1a18 Mon Sep 17 00:00:00 2001 From: Michael Zimmermann Date: Tue, 5 Jul 2022 17:13:12 +0200 Subject: [PATCH 01/14] move `trim_offset` to `string_utils` the diff viewer will need it in a future commit --- src/string_utils.rs | 19 +++++++++++++++++++ src/ui/reflow.rs | 18 +----------------- 2 files changed, 20 insertions(+), 17 deletions(-) diff --git a/src/string_utils.rs b/src/string_utils.rs index 065f5554c5..c2cb6808a4 100644 --- a/src/string_utils.rs +++ b/src/string_utils.rs @@ -1,3 +1,6 @@ +use unicode_segmentation::UnicodeSegmentation; +use unicode_width::UnicodeWidthStr; + /// pub fn trim_length_left(s: &str, width: usize) -> &str { 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)] mod test { use pretty_assertions::assert_eq; diff --git a/src/ui/reflow.rs b/src/ui/reflow.rs index a9937361e6..81de4b840c 100644 --- a/src/ui/reflow.rs +++ b/src/ui/reflow.rs @@ -1,6 +1,6 @@ +use crate::string_utils::trim_offset; use easy_cast::Cast; use tui::text::StyledGrapheme; -use unicode_segmentation::UnicodeSegmentation; use unicode_width::UnicodeWidthStr; 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)] mod test { use super::*; From 70af3dd516204cfef7cfd9abcc64916fb11546c4 Mon Sep 17 00:00:00 2001 From: Michael Zimmermann Date: Thu, 7 Jul 2022 17:57:06 +0200 Subject: [PATCH 02/14] ui: scrollbar: add support for horizontal scrolling --- src/components/blame_file.rs | 1 + src/components/commitlist.rs | 3 +- src/components/file_revlog.rs | 3 +- src/components/syntax_text.rs | 1 + src/components/taglist.rs | 1 + src/components/utils/scroll_vertical.rs | 3 +- src/ui/mod.rs | 2 +- src/ui/scrollbar.rs | 74 +++++++++++++++++++++++-- 8 files changed, 78 insertions(+), 10 deletions(-) diff --git a/src/components/blame_file.rs b/src/components/blame_file.rs index 9f41df88f8..afc3d135bf 100644 --- a/src/components/blame_file.rs +++ b/src/components/blame_file.rs @@ -123,6 +123,7 @@ impl DrawableComponent for BlameFileComponent { // // https://github.com/fdehau/tui-rs/issues/448 table_state.selected().unwrap_or(0), + ui::Orientation::Vertical, ); self.table_state.set(table_state); diff --git a/src/components/commitlist.rs b/src/components/commitlist.rs index e58977c278..2d2a42b3f1 100644 --- a/src/components/commitlist.rs +++ b/src/components/commitlist.rs @@ -8,7 +8,7 @@ use crate::{ queue::{InternalEvent, Queue}, strings::{self, symbol}, ui::style::{SharedTheme, Theme}, - ui::{calc_scroll_top, draw_scrollbar}, + ui::{calc_scroll_top, draw_scrollbar, Orientation}, }; use anyhow::Result; use asyncgit::sync::{BranchInfo, CommitId, Tags}; @@ -501,6 +501,7 @@ impl DrawableComponent for CommitList { &self.theme, self.count_total, self.selection, + Orientation::Vertical, ); Ok(()) diff --git a/src/components/file_revlog.rs b/src/components/file_revlog.rs index cabe364f1b..c9b0537240 100644 --- a/src/components/file_revlog.rs +++ b/src/components/file_revlog.rs @@ -11,7 +11,7 @@ use crate::{ keys::SharedKeyConfig, queue::{InternalEvent, NeedsUpdate, Queue}, strings, - ui::{draw_scrollbar, style::SharedTheme}, + ui::{draw_scrollbar, style::SharedTheme, Orientation}, }; use anyhow::Result; use asyncgit::{ @@ -412,6 +412,7 @@ impl FileRevlogComponent { &self.theme, self.count_total, table_state.selected().unwrap_or(0), + Orientation::Vertical, ); self.table_state.set(table_state); diff --git a/src/components/syntax_text.rs b/src/components/syntax_text.rs index bb0b39f886..4b20a89f3e 100644 --- a/src/components/syntax_text.rs +++ b/src/components/syntax_text.rs @@ -238,6 +238,7 @@ impl DrawableComponent for SyntaxTextComponent { state.height().saturating_sub(2), )), usize::from(state.scroll().y), + ui::Orientation::Vertical, ); } diff --git a/src/components/taglist.rs b/src/components/taglist.rs index f2c0dfb31d..2020d8a9fa 100644 --- a/src/components/taglist.rs +++ b/src/components/taglist.rs @@ -128,6 +128,7 @@ impl DrawableComponent for TagListComponent { &self.theme, number_of_rows, table_state.selected().unwrap_or(0), + ui::Orientation::Vertical, ); self.table_state.set(table_state); diff --git a/src/components/utils/scroll_vertical.rs b/src/components/utils/scroll_vertical.rs index 54fa7e1bb9..a8a62a0697 100644 --- a/src/components/utils/scroll_vertical.rs +++ b/src/components/utils/scroll_vertical.rs @@ -4,7 +4,7 @@ use tui::{backend::Backend, layout::Rect, Frame}; use crate::{ components::ScrollType, - ui::{draw_scrollbar, style::SharedTheme}, + ui::{draw_scrollbar, style::SharedTheme, Orientation}, }; pub struct VerticalScroll { @@ -95,6 +95,7 @@ impl VerticalScroll { theme, self.max_top.get(), self.top.get(), + Orientation::Vertical, ); } } diff --git a/src/ui/mod.rs b/src/ui/mod.rs index eac3e3b602..4b1db6ebd1 100644 --- a/src/ui/mod.rs +++ b/src/ui/mod.rs @@ -6,7 +6,7 @@ pub mod style; mod syntax_text; use filetreelist::MoveSelection; -pub use scrollbar::draw_scrollbar; +pub use scrollbar::{draw_scrollbar, Orientation}; pub use scrolllist::{draw_list, draw_list_block}; pub use stateful_paragraph::{ ParagraphState, ScrollPos, StatefulParagraph, diff --git a/src/ui/scrollbar.rs b/src/ui/scrollbar.rs index adf4dec1d9..c0580c2661 100644 --- a/src/ui/scrollbar.rs +++ b/src/ui/scrollbar.rs @@ -6,32 +6,40 @@ use tui::{ buffer::Buffer, layout::{Margin, Rect}, style::Style, - symbols::{block::FULL, line::DOUBLE_VERTICAL}, + symbols::{ + block::FULL, + line::{DOUBLE_HORIZONTAL, DOUBLE_VERTICAL}, + }, widgets::Widget, Frame, }; +pub enum Orientation { + Vertical, + Horizontal, +} + /// struct Scrollbar { max: u16, pos: u16, style_bar: Style, style_pos: Style, + orientation: Orientation, } impl Scrollbar { - fn new(max: usize, pos: usize) -> Self { + fn new(max: usize, pos: usize, orientation: Orientation) -> Self { Self { max: u16::try_from(max).unwrap_or_default(), pos: u16::try_from(pos).unwrap_or_default(), style_pos: Style::default(), style_bar: Style::default(), + orientation, } } -} -impl Widget for Scrollbar { - fn render(self, area: Rect, buf: &mut Buffer) { + fn render_vertical(self, area: Rect, buf: &mut Buffer) { if area.height <= 2 { return; } @@ -67,6 +75,59 @@ impl Widget for Scrollbar { 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( @@ -75,8 +136,9 @@ pub fn draw_scrollbar( theme: &SharedTheme, max: 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(); f.render_widget(widget, r); } From e7019b6d3838c09285f2a629527d13b5a092dc11 Mon Sep 17 00:00:00 2001 From: Michael Zimmermann Date: Tue, 5 Jul 2022 17:14:14 +0200 Subject: [PATCH 03/14] diff: implement horizontal scrolling converting tabs to spaces had to be moved from after the formatting to before it because `trim_offset` seems to remove leading tabs from the output string (not the input string) for some reason. calculating the longtest line is cached to prevent a theoretical decrease in performance. --- CHANGELOG.md | 1 + src/components/diff.rs | 68 +++++++++++++++++++++++++++++++++++++++--- 2 files changed, 65 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 503dd7b09b..edf9bb57ed 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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)) * 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)) +* support horizontal scrolling in diff view ([#1017](https://github.com/extrawurst/gitui/issues/1017)) ### Changed * minimum supported rust version bumped to 1.60 ([#1279](https://github.com/extrawurst/gitui/pull/1279)) diff --git a/src/components/diff.rs b/src/components/diff.rs index cbc27e8737..bc54f47608 100644 --- a/src/components/diff.rs +++ b/src/components/diff.rs @@ -7,8 +7,9 @@ use crate::{ keys::{key_match, SharedKeyConfig}, queue::{Action, InternalEvent, NeedsUpdate, Queue, ResetItem}, string_utils::tabs_to_spaces, + string_utils::trim_offset, strings, try_or_popup, - ui::style::SharedTheme, + ui::{draw_scrollbar, style::SharedTheme, Orientation}, }; use anyhow::Result; use asyncgit::{ @@ -102,6 +103,7 @@ impl Selection { pub struct DiffComponent { repo: RepoPathRef, diff: Option, + longest_line: usize, pending: bool, selection: Selection, selected_hunk: Option, @@ -113,6 +115,7 @@ pub struct DiffComponent { theme: SharedTheme, key_config: SharedKeyConfig, is_immutable: bool, + scrolled_right: usize, } impl DiffComponent { @@ -131,6 +134,7 @@ impl DiffComponent { pending: false, selected_hunk: None, diff: None, + longest_line: 0, current_size: Cell::new((0, 0)), selection: Selection::Single(0), scroll: VerticalScroll::new(), @@ -138,6 +142,7 @@ impl DiffComponent { key_config, is_immutable, repo, + scrolled_right: 0, } } /// @@ -155,10 +160,12 @@ impl DiffComponent { pub fn clear(&mut self, pending: bool) { self.current = Current::default(); self.diff = None; + self.longest_line = 0; self.scroll.reset(); self.selection = Selection::Single(0); self.selected_hunk = None; self.pending = pending; + self.scrolled_right = 0; } /// pub fn update( @@ -182,6 +189,24 @@ impl DiffComponent { self.diff = Some(diff); + self.longest_line = self + .diff + .as_ref() + .map(|diff| { + diff.hunks + .iter() + .map(|hunk| { + hunk.lines + .iter() + .map(|line| line.content.len()) + .max() + .unwrap_or(0) + }) + .max() + .unwrap_or(0) + }) + .unwrap_or(0); + if reset_selection { self.scroll.reset(); self.selection = Selection::Single(0); @@ -241,6 +266,11 @@ impl DiffComponent { 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) { if self.diff.is_some() { self.selection.modify(direction, self.lines_count()); @@ -378,6 +408,7 @@ impl DiffComponent { hunk_selected, i == hunk_len - 1, &self.theme, + self.scrolled_right, )); lines_added += 1; } @@ -400,6 +431,7 @@ impl DiffComponent { selected_hunk: bool, end_of_hunk: bool, theme: &SharedTheme, + scrolled_right: usize, ) -> Spans<'a> { let style = theme.diff_hunk_marker(selected_hunk); @@ -418,18 +450,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 { // selected line - format!("{:w$}\n", line.content, w = width as usize) + format!("{:w$}\n", content, w = width as usize) } else { // weird eof missing eol line - format!("{}\n", line.content) + format!("{}\n", content) }; Spans::from(vec![ left_side_of_line, Span::styled( - Cow::from(tabs_to_spaces(filled)), + Cow::from(filled), theme.diff_line(line.line_type, selected), ), ]) @@ -644,6 +680,17 @@ impl DrawableComponent for DiffComponent { if self.focused() { self.scroll.draw(f, r, &self.theme); + + if self.scrolled_right != 0 { + draw_scrollbar( + f, + r, + &self.theme, + self.max_scroll_right(), + self.scrolled_right, + Orientation::Horizontal, + ); + } } Ok(()) @@ -754,6 +801,19 @@ impl Component for DiffComponent { { self.move_selection(ScrollType::PageDown); Ok(EventState::Consumed) + } else if key_match( + e, + self.key_config.keys.move_right, + ) { + if self.scrolled_right < self.max_scroll_right() { + self.scrolled_right += 1; + } + Ok(EventState::Consumed) + } else if key_match(e, self.key_config.keys.move_left) + && self.scrolled_right > 0 + { + self.scrolled_right -= 1; + Ok(EventState::Consumed) } else if key_match( e, self.key_config.keys.stage_unstage_item, From 2cb046e1df726e9aeb272ed0e09ca9aa83ee80f0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christoph=20R=C3=BC=C3=9Fler?= Date: Tue, 13 Sep 2022 18:28:03 +0200 Subject: [PATCH 04/14] Convert tabs to spaces when calculating max scroll --- src/components/diff.rs | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/src/components/diff.rs b/src/components/diff.rs index bc54f47608..13feab4703 100644 --- a/src/components/diff.rs +++ b/src/components/diff.rs @@ -198,7 +198,16 @@ impl DiffComponent { .map(|hunk| { hunk.lines .iter() - .map(|line| line.content.len()) + .map(|line| { + let converted_content = + tabs_to_spaces( + line.content + .as_ref() + .to_string(), + ); + + converted_content.len() + }) .max() .unwrap_or(0) }) From d022327c70ac44f7a887884aa924950045b6ac87 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christoph=20R=C3=BC=C3=9Fler?= Date: Tue, 13 Sep 2022 18:28:58 +0200 Subject: [PATCH 05/14] Take vertical selection bar into account --- src/components/diff.rs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/components/diff.rs b/src/components/diff.rs index 13feab4703..7659e9de4a 100644 --- a/src/components/diff.rs +++ b/src/components/diff.rs @@ -209,7 +209,11 @@ impl DiffComponent { converted_content.len() }) .max() - .unwrap_or(0) + .map_or(0, |len| { + // Each hunk uses a 1-character wide vertical bar to its left to indicate + // selection. + len + 1 + }) }) .max() .unwrap_or(0) From 9738ee35082312f400e17603adde5273322d021e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christoph=20R=C3=BC=C3=9Fler?= Date: Tue, 13 Sep 2022 18:31:26 +0200 Subject: [PATCH 06/14] Fix clippy errors --- src/components/diff.rs | 9 +++------ src/ui/scrollbar.rs | 2 +- 2 files changed, 4 insertions(+), 7 deletions(-) diff --git a/src/components/diff.rs b/src/components/diff.rs index 7659e9de4a..cc12afe570 100644 --- a/src/components/diff.rs +++ b/src/components/diff.rs @@ -189,10 +189,8 @@ impl DiffComponent { self.diff = Some(diff); - self.longest_line = self - .diff - .as_ref() - .map(|diff| { + self.longest_line = + self.diff.as_ref().map_or(0, |diff| { diff.hunks .iter() .map(|hunk| { @@ -217,8 +215,7 @@ impl DiffComponent { }) .max() .unwrap_or(0) - }) - .unwrap_or(0); + }); if reset_selection { self.scroll.reset(); diff --git a/src/ui/scrollbar.rs b/src/ui/scrollbar.rs index c0580c2661..3a28fe8213 100644 --- a/src/ui/scrollbar.rs +++ b/src/ui/scrollbar.rs @@ -124,7 +124,7 @@ impl Widget for Scrollbar { match &self.orientation { Orientation::Vertical => self.render_vertical(area, buf), Orientation::Horizontal => { - self.render_horizontal(area, buf) + self.render_horizontal(area, buf); } } } From 602395baa00ca4b91cee64d1515ef8bb1f96067b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christoph=20R=C3=BC=C3=9Fler?= Date: Tue, 13 Sep 2022 21:40:58 +0200 Subject: [PATCH 07/14] Flatten code that calculates line length --- src/components/diff.rs | 43 +++++++++++++++++------------------------- 1 file changed, 17 insertions(+), 26 deletions(-) diff --git a/src/components/diff.rs b/src/components/diff.rs index cc12afe570..596c15c377 100644 --- a/src/components/diff.rs +++ b/src/components/diff.rs @@ -189,32 +189,23 @@ impl DiffComponent { self.diff = Some(diff); - self.longest_line = - self.diff.as_ref().map_or(0, |diff| { - diff.hunks - .iter() - .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 - }) - }) - .max() - .unwrap_or(0) + 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 { From 95c8bea586e107c1822ef05957d6b124eb6be9df Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christoph=20R=C3=BC=C3=9Fler?= Date: Thu, 22 Sep 2022 19:15:56 +0200 Subject: [PATCH 08/14] Use full screen for focussed diff view --- src/components/compare_commits.rs | 2 +- src/components/file_revlog.rs | 2 +- src/components/inspect_commit.rs | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/components/compare_commits.rs b/src/components/compare_commits.rs index c4048a9d9e..dc60205988 100644 --- a/src/components/compare_commits.rs +++ b/src/components/compare_commits.rs @@ -44,7 +44,7 @@ impl DrawableComponent for CompareCommitsComponent { ) -> Result<()> { if self.is_visible() { let percentages = if self.diff.focused() { - (30, 70) + (0, 100) } else { (50, 50) }; diff --git a/src/components/file_revlog.rs b/src/components/file_revlog.rs index c9b0537240..c5f9bc5d8e 100644 --- a/src/components/file_revlog.rs +++ b/src/components/file_revlog.rs @@ -446,7 +446,7 @@ impl DrawableComponent for FileRevlogComponent { ) -> Result<()> { if self.visible { let percentages = if self.diff.focused() { - (30, 70) + (0, 100) } else { (50, 50) }; diff --git a/src/components/inspect_commit.rs b/src/components/inspect_commit.rs index 035f0464bf..2b56ab8f5b 100644 --- a/src/components/inspect_commit.rs +++ b/src/components/inspect_commit.rs @@ -71,7 +71,7 @@ impl DrawableComponent for InspectCommitComponent { ) -> Result<()> { if self.is_visible() { let percentages = if self.diff.focused() { - (30, 70) + (0, 100) } else { (50, 50) }; From 404f9642e4f32c4eb9a307f0a48518bbcf702fd3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christoph=20R=C3=BC=C3=9Fler?= Date: Sun, 27 Nov 2022 12:38:41 +0100 Subject: [PATCH 09/14] Fix clippy errors on nightly --- src/components/diff.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/diff.rs b/src/components/diff.rs index 596c15c377..bf0d3c8304 100644 --- a/src/components/diff.rs +++ b/src/components/diff.rs @@ -457,10 +457,10 @@ impl DiffComponent { let filled = if selected { // selected line - format!("{:w$}\n", content, w = width as usize) + format!("{content:w$}\n", w = width as usize) } else { // weird eof missing eol line - format!("{}\n", content) + format!("{content}\n") }; Spans::from(vec![ From 7cec3a2c557f9e2bc43bc2ce7ea3a892a5ce6ba1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christoph=20R=C3=BC=C3=9Fler?= Date: Fri, 2 Dec 2022 14:39:36 +0100 Subject: [PATCH 10/14] Use Esc to close focused diff Consume left arrow in `DiffComponent` even when `scrolled_right` is already 0. --- src/components/compare_commits.rs | 14 ++++++-------- src/components/diff.rs | 5 +++-- src/components/file_revlog.rs | 13 +++++-------- src/components/inspect_commit.rs | 16 +++++++--------- 4 files changed, 21 insertions(+), 27 deletions(-) diff --git a/src/components/compare_commits.rs b/src/components/compare_commits.rs index dc60205988..11ae5c3ea6 100644 --- a/src/components/compare_commits.rs +++ b/src/components/compare_commits.rs @@ -121,7 +121,12 @@ impl Component for CompareCommitsComponent { if let Event::Key(e) = ev { 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( e, self.key_config.keys.focus_right, @@ -132,13 +137,6 @@ impl Component for CompareCommitsComponent { } else if key_match( e, 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); } diff --git a/src/components/diff.rs b/src/components/diff.rs index bf0d3c8304..f242a0fcb2 100644 --- a/src/components/diff.rs +++ b/src/components/diff.rs @@ -811,9 +811,10 @@ impl Component for DiffComponent { } Ok(EventState::Consumed) } else if key_match(e, self.key_config.keys.move_left) - && self.scrolled_right > 0 { - self.scrolled_right -= 1; + if self.scrolled_right > 0 { + self.scrolled_right -= 1; + } Ok(EventState::Consumed) } else if key_match( e, diff --git a/src/components/file_revlog.rs b/src/components/file_revlog.rs index c5f9bc5d8e..6e679d6f20 100644 --- a/src/components/file_revlog.rs +++ b/src/components/file_revlog.rs @@ -486,20 +486,17 @@ impl Component for FileRevlogComponent { if let Event::Key(key) = event { 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( key, self.key_config.keys.focus_right, ) && self.can_focus_diff() { 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) { if let Some(commit_id) = self.selected_commit() { self.hide_stacked(true); diff --git a/src/components/inspect_commit.rs b/src/components/inspect_commit.rs index 2b56ab8f5b..416a8ab668 100644 --- a/src/components/inspect_commit.rs +++ b/src/components/inspect_commit.rs @@ -126,7 +126,7 @@ impl Component for InspectCommitComponent { )); out.push(CommandInfo::new( - strings::commands::diff_focus_left(&self.key_config), + strings::commands::close_popup(&self.key_config), true, self.diff.focused() || force_all, )); @@ -157,7 +157,12 @@ impl Component for InspectCommitComponent { if let Event::Key(e) = ev { 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( e, self.key_config.keys.focus_right, @@ -168,13 +173,6 @@ impl Component for InspectCommitComponent { } else if key_match( e, 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); } From a5afff07090df72708eb21d38b6a9f16c2c8607e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christoph=20R=C3=BC=C3=9Fler?= Date: Sat, 24 Dec 2022 15:52:00 +0100 Subject: [PATCH 11/14] Show scrollbar if content is scrollable --- src/components/diff.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/diff.rs b/src/components/diff.rs index f242a0fcb2..87d98c5460 100644 --- a/src/components/diff.rs +++ b/src/components/diff.rs @@ -682,7 +682,7 @@ impl DrawableComponent for DiffComponent { if self.focused() { self.scroll.draw(f, r, &self.theme); - if self.scrolled_right != 0 { + if self.max_scroll_right() > 0 { draw_scrollbar( f, r, From 51d7eb965f5e437d0aeea6187972271c796ce590 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christoph=20R=C3=BC=C3=9Fler?= Date: Wed, 4 Jan 2023 10:49:05 +0100 Subject: [PATCH 12/14] Use full screen for focussed diff view in Status Use Esc to close focused diff --- src/tabs/status.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/tabs/status.rs b/src/tabs/status.rs index b7b3d5996e..84085a1991 100644 --- a/src/tabs/status.rs +++ b/src/tabs/status.rs @@ -105,8 +105,8 @@ impl DrawableComponent for Status { .constraints( if self.focus == Focus::Diff { [ - Constraint::Percentage(30), - Constraint::Percentage(70), + Constraint::Percentage(0), + Constraint::Percentage(100), ] } else { [ @@ -674,7 +674,7 @@ impl Status { let focus_on_diff = self.is_focus_on_diff(); out.push( CommandInfo::new( - strings::commands::diff_focus_left(&self.key_config), + strings::commands::close_popup(&self.key_config), true, (self.visible && focus_on_diff) || force_all, ) @@ -846,7 +846,7 @@ impl Component for Status { self.switch_focus(Focus::Diff).map(Into::into) } else if key_match( k, - self.key_config.keys.focus_left, + self.key_config.keys.exit_popup, ) { self.switch_focus(match self.diff_target { DiffTarget::Stage => Focus::Stage, From 571676f1fc562693aaa495e593d59942c531e5f5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christoph=20R=C3=BC=C3=9Fler?= Date: Sat, 7 Jan 2023 14:01:15 +0100 Subject: [PATCH 13/14] Replace scrolled_right by horizontal_scroll Add `HorizontalScroll` --- src/components/diff.rs | 39 +++--- src/components/mod.rs | 6 + src/components/utils/mod.rs | 1 + src/components/utils/scroll_horizontal.rs | 140 ++++++++++++++++++++++ 4 files changed, 166 insertions(+), 20 deletions(-) create mode 100644 src/components/utils/scroll_horizontal.rs diff --git a/src/components/diff.rs b/src/components/diff.rs index 87d98c5460..a95748d834 100644 --- a/src/components/diff.rs +++ b/src/components/diff.rs @@ -1,6 +1,7 @@ use super::{ + utils::scroll_horizontal::HorizontalScroll, utils::scroll_vertical::VerticalScroll, CommandBlocking, - Direction, DrawableComponent, ScrollType, + Direction, DrawableComponent, HorizontalScrollType, ScrollType, }; use crate::{ components::{CommandInfo, Component, EventState}, @@ -9,7 +10,7 @@ use crate::{ string_utils::tabs_to_spaces, string_utils::trim_offset, strings, try_or_popup, - ui::{draw_scrollbar, style::SharedTheme, Orientation}, + ui::style::SharedTheme, }; use anyhow::Result; use asyncgit::{ @@ -111,11 +112,11 @@ pub struct DiffComponent { focused: bool, current: Current, scroll: VerticalScroll, + horizontal_scroll: HorizontalScroll, queue: Queue, theme: SharedTheme, key_config: SharedKeyConfig, is_immutable: bool, - scrolled_right: usize, } impl DiffComponent { @@ -138,11 +139,11 @@ impl DiffComponent { current_size: Cell::new((0, 0)), selection: Selection::Single(0), scroll: VerticalScroll::new(), + horizontal_scroll: HorizontalScroll::new(), theme, key_config, is_immutable, repo, - scrolled_right: 0, } } /// @@ -162,10 +163,10 @@ impl DiffComponent { self.diff = None; self.longest_line = 0; self.scroll.reset(); + self.horizontal_scroll.reset(); self.selection = Selection::Single(0); self.selected_hunk = None; self.pending = pending; - self.scrolled_right = 0; } /// pub fn update( @@ -409,7 +410,8 @@ impl DiffComponent { hunk_selected, i == hunk_len - 1, &self.theme, - self.scrolled_right, + self.horizontal_scroll + .get_right(), )); lines_added += 1; } @@ -643,6 +645,7 @@ impl DrawableComponent for DiffComponent { r.height.saturating_sub(2), )); + let current_width = self.current_size.get().0; let current_height = self.current_size.get().1; self.scroll.update( @@ -651,6 +654,11 @@ impl DrawableComponent for DiffComponent { usize::from(current_height), ); + self.horizontal_scroll.update_no_selection( + self.longest_line, + current_width.into(), + ); + let title = format!( "{}{}", strings::title_diff(&self.key_config), @@ -683,14 +691,7 @@ impl DrawableComponent for DiffComponent { self.scroll.draw(f, r, &self.theme); if self.max_scroll_right() > 0 { - draw_scrollbar( - f, - r, - &self.theme, - self.max_scroll_right(), - self.scrolled_right, - Orientation::Horizontal, - ); + self.horizontal_scroll.draw(f, r, &self.theme); } } @@ -806,15 +807,13 @@ impl Component for DiffComponent { e, self.key_config.keys.move_right, ) { - if self.scrolled_right < self.max_scroll_right() { - self.scrolled_right += 1; - } + self.horizontal_scroll + .move_right(HorizontalScrollType::Right); Ok(EventState::Consumed) } else if key_match(e, self.key_config.keys.move_left) { - if self.scrolled_right > 0 { - self.scrolled_right -= 1; - } + self.horizontal_scroll + .move_right(HorizontalScrollType::Left); Ok(EventState::Consumed) } else if key_match( e, diff --git a/src/components/mod.rs b/src/components/mod.rs index fb42dbec93..8d1b5114d8 100644 --- a/src/components/mod.rs +++ b/src/components/mod.rs @@ -185,6 +185,12 @@ pub enum ScrollType { PageDown, } +#[derive(Copy, Clone)] +pub enum HorizontalScrollType { + Left, + Right, +} + #[derive(Copy, Clone)] pub enum Direction { Up, diff --git a/src/components/utils/mod.rs b/src/components/utils/mod.rs index d003f9588f..06f07bae22 100644 --- a/src/components/utils/mod.rs +++ b/src/components/utils/mod.rs @@ -5,6 +5,7 @@ use unicode_width::UnicodeWidthStr; pub mod emoji; pub mod filetree; pub mod logitems; +pub mod scroll_horizontal; pub mod scroll_vertical; pub mod statustree; diff --git a/src/components/utils/scroll_horizontal.rs b/src/components/utils/scroll_horizontal.rs new file mode 100644 index 0000000000..58ea31a77d --- /dev/null +++ b/src/components/utils/scroll_horizontal.rs @@ -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, + max_right: Cell, +} + +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( + &self, + f: &mut Frame, + 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); + } +} From e603e23fbd523e5a8407a72cbb287977266babfb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christoph=20R=C3=BC=C3=9Fler?= Date: Sat, 7 Jan 2023 14:50:07 +0100 Subject: [PATCH 14/14] Rename scroll to vertical_scroll in DiffComponent --- src/components/diff.rs | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/components/diff.rs b/src/components/diff.rs index a95748d834..6a1500338a 100644 --- a/src/components/diff.rs +++ b/src/components/diff.rs @@ -111,7 +111,7 @@ pub struct DiffComponent { current_size: Cell<(u16, u16)>, focused: bool, current: Current, - scroll: VerticalScroll, + vertical_scroll: VerticalScroll, horizontal_scroll: HorizontalScroll, queue: Queue, theme: SharedTheme, @@ -138,7 +138,7 @@ impl DiffComponent { longest_line: 0, current_size: Cell::new((0, 0)), selection: Selection::Single(0), - scroll: VerticalScroll::new(), + vertical_scroll: VerticalScroll::new(), horizontal_scroll: HorizontalScroll::new(), theme, key_config, @@ -162,7 +162,7 @@ impl DiffComponent { self.current = Current::default(); self.diff = None; self.longest_line = 0; - self.scroll.reset(); + self.vertical_scroll.reset(); self.horizontal_scroll.reset(); self.selection = Selection::Single(0); self.selected_hunk = None; @@ -210,7 +210,7 @@ impl DiffComponent { }); if reset_selection { - self.scroll.reset(); + self.vertical_scroll.reset(); self.selection = Selection::Single(0); self.update_selection(0); } else { @@ -372,7 +372,7 @@ impl DiffComponent { Span::raw(Cow::from(")")), ])]); } else { - let min = self.scroll.get_top(); + let min = self.vertical_scroll.get_top(); let max = min + height as usize; let mut line_cursor = 0_usize; @@ -648,7 +648,7 @@ impl DrawableComponent for DiffComponent { let current_width = self.current_size.get().0; let current_height = self.current_size.get().1; - self.scroll.update( + self.vertical_scroll.update( self.selection.get_end(), self.lines_count(), usize::from(current_height), @@ -688,7 +688,7 @@ impl DrawableComponent for DiffComponent { ); 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);