mirror of
https://github.com/extrawurst/gitui.git
synced 2024-11-23 03:32:30 +03:00
Allow to scroll diffs horizontally (#1327)
This commit is contained in:
parent
f29178d1b3
commit
9fa5fddd93
@ -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))
|
||||
|
@ -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);
|
||||
|
@ -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(())
|
||||
|
@ -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)
|
||||
};
|
||||
@ -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);
|
||||
}
|
||||
|
@ -1,12 +1,14 @@
|
||||
use super::{
|
||||
utils::scroll_horizontal::HorizontalScroll,
|
||||
utils::scroll_vertical::VerticalScroll, CommandBlocking,
|
||||
Direction, DrawableComponent, ScrollType,
|
||||
Direction, DrawableComponent, HorizontalScrollType, ScrollType,
|
||||
};
|
||||
use crate::{
|
||||
components::{CommandInfo, Component, EventState},
|
||||
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,
|
||||
};
|
||||
@ -102,13 +104,15 @@ impl Selection {
|
||||
pub struct DiffComponent {
|
||||
repo: RepoPathRef,
|
||||
diff: Option<FileDiff>,
|
||||
longest_line: usize,
|
||||
pending: bool,
|
||||
selection: Selection,
|
||||
selected_hunk: Option<usize>,
|
||||
current_size: Cell<(u16, u16)>,
|
||||
focused: bool,
|
||||
current: Current,
|
||||
scroll: VerticalScroll,
|
||||
vertical_scroll: VerticalScroll,
|
||||
horizontal_scroll: HorizontalScroll,
|
||||
queue: Queue,
|
||||
theme: SharedTheme,
|
||||
key_config: SharedKeyConfig,
|
||||
@ -131,9 +135,11 @@ 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(),
|
||||
vertical_scroll: VerticalScroll::new(),
|
||||
horizontal_scroll: HorizontalScroll::new(),
|
||||
theme,
|
||||
key_config,
|
||||
is_immutable,
|
||||
@ -155,7 +161,9 @@ impl DiffComponent {
|
||||
pub fn clear(&mut self, pending: bool) {
|
||||
self.current = Current::default();
|
||||
self.diff = None;
|
||||
self.scroll.reset();
|
||||
self.longest_line = 0;
|
||||
self.vertical_scroll.reset();
|
||||
self.horizontal_scroll.reset();
|
||||
self.selection = Selection::Single(0);
|
||||
self.selected_hunk = None;
|
||||
self.pending = pending;
|
||||
@ -182,8 +190,27 @@ impl DiffComponent {
|
||||
|
||||
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 {
|
||||
self.scroll.reset();
|
||||
self.vertical_scroll.reset();
|
||||
self.selection = Selection::Single(0);
|
||||
self.update_selection(0);
|
||||
} else {
|
||||
@ -241,6 +268,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());
|
||||
@ -340,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;
|
||||
@ -378,6 +410,8 @@ impl DiffComponent {
|
||||
hunk_selected,
|
||||
i == hunk_len - 1,
|
||||
&self.theme,
|
||||
self.horizontal_scroll
|
||||
.get_right(),
|
||||
));
|
||||
lines_added += 1;
|
||||
}
|
||||
@ -400,6 +434,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 +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 {
|
||||
// selected line
|
||||
format!("{:w$}\n", line.content, w = width as usize)
|
||||
format!("{content:w$}\n", w = width as usize)
|
||||
} else {
|
||||
// weird eof missing eol line
|
||||
format!("{}\n", line.content)
|
||||
format!("{content}\n")
|
||||
};
|
||||
|
||||
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),
|
||||
),
|
||||
])
|
||||
@ -606,14 +645,20 @@ 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(
|
||||
self.vertical_scroll.update(
|
||||
self.selection.get_end(),
|
||||
self.lines_count(),
|
||||
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),
|
||||
@ -643,7 +688,11 @@ 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);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
@ -754,6 +803,18 @@ impl Component for DiffComponent {
|
||||
{
|
||||
self.move_selection(ScrollType::PageDown);
|
||||
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(
|
||||
e,
|
||||
self.key_config.keys.stage_unstage_item,
|
||||
|
@ -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);
|
||||
@ -445,7 +446,7 @@ impl DrawableComponent for FileRevlogComponent {
|
||||
) -> Result<()> {
|
||||
if self.visible {
|
||||
let percentages = if self.diff.focused() {
|
||||
(30, 70)
|
||||
(0, 100)
|
||||
} else {
|
||||
(50, 50)
|
||||
};
|
||||
@ -485,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);
|
||||
|
@ -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)
|
||||
};
|
||||
@ -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);
|
||||
}
|
||||
|
@ -185,6 +185,12 @@ pub enum ScrollType {
|
||||
PageDown,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone)]
|
||||
pub enum HorizontalScrollType {
|
||||
Left,
|
||||
Right,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone)]
|
||||
pub enum Direction {
|
||||
Up,
|
||||
|
@ -238,6 +238,7 @@ impl DrawableComponent for SyntaxTextComponent {
|
||||
state.height().saturating_sub(2),
|
||||
)),
|
||||
usize::from(state.scroll().y),
|
||||
ui::Orientation::Vertical,
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -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);
|
||||
|
@ -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;
|
||||
|
||||
|
140
src/components/utils/scroll_horizontal.rs
Normal file
140
src/components/utils/scroll_horizontal.rs
Normal 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);
|
||||
}
|
||||
}
|
@ -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,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
|
@ -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,
|
||||
|
@ -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,
|
||||
|
@ -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::*;
|
||||
|
@ -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<B: Backend>(
|
||||
@ -75,8 +136,9 @@ pub fn draw_scrollbar<B: Backend>(
|
||||
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);
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user