git-igitt 0.1.21

Interactive Git terminal application to browse and visualize Git history graphs arranged for your branching model
Documentation
use crate::widgets::list::{ListItem, ListState};
use tui::buffer::Buffer;
use tui::layout::{Corner, Rect};
use tui::style::Style;
use tui::text::Span;
use tui::widgets::{Block, StatefulWidget, Widget};
use unicode_width::UnicodeWidthStr;

const SCROLL_MARGIN: usize = 2;
const SCROLLBAR_STR: &str = "\u{2588}";

#[derive(Debug, Clone, PartialEq, Eq)]
pub enum BranchItemType {
    LocalBranch,
    RemoteBranch,
    Tag,
    Heading,
}

impl BranchItemType {
    pub fn is_selectable(&self) -> bool {
        self != &BranchItemType::Heading
    }
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub struct BranchListItem<'a> {
    pub content: Span<'a>,
    pub style: Style,
    pub item_type: &'a BranchItemType,
}

impl<'a> BranchListItem<'a> {
    pub fn new<T>(content: T, item_type: &'a BranchItemType) -> BranchListItem<'a>
    where
        T: Into<Span<'a>>,
    {
        BranchListItem {
            content: content.into(),
            style: Style::default(),
            item_type,
        }
    }

    pub fn style(mut self, style: Style) -> BranchListItem<'a> {
        self.style = style;
        self
    }
}

pub struct BranchItem {
    pub(crate) name: String,
    pub(crate) index: Option<usize>,
    pub(crate) color: u8,
    pub(crate) branch_type: BranchItemType,
}

impl BranchItem {
    pub fn new(name: String, index: Option<usize>, color: u8, branch_type: BranchItemType) -> Self {
        Self {
            name,
            index,
            color,
            branch_type,
        }
    }
}

impl ListItem for BranchItem {
    fn is_selectable(&self) -> bool {
        self.branch_type != BranchItemType::Heading
    }
}

impl ListItem for BranchListItem<'_> {
    fn is_selectable(&self) -> bool {
        self.item_type != &BranchItemType::Heading
    }
}

#[derive(Debug, Clone)]
pub struct BranchList<'a> {
    block: Option<Block<'a>>,
    items: Vec<BranchListItem<'a>>,
    style: Style,
    start_corner: Corner,
    /// Style used to render selected item
    highlight_style: Style,
    /// Symbol in front of the selected item (Shift all items to the right)
    highlight_symbol: Option<&'a str>,
}

impl<'a> BranchList<'a> {
    pub fn new<T>(items: T) -> BranchList<'a>
    where
        T: Into<Vec<BranchListItem<'a>>>,
    {
        BranchList {
            block: None,
            style: Style::default(),
            items: items.into(),
            start_corner: Corner::TopLeft,
            highlight_style: Style::default(),
            highlight_symbol: None,
        }
    }

    pub fn block(mut self, block: Block<'a>) -> BranchList<'a> {
        self.block = Some(block);
        self
    }

    pub fn style(mut self, style: Style) -> BranchList<'a> {
        self.style = style;
        self
    }

    pub fn highlight_symbol(mut self, highlight_symbol: &'a str) -> BranchList<'a> {
        self.highlight_symbol = Some(highlight_symbol);
        self
    }

    pub fn highlight_style(mut self, style: Style) -> BranchList<'a> {
        self.highlight_style = style;
        self
    }

    pub fn start_corner(mut self, corner: Corner) -> BranchList<'a> {
        self.start_corner = corner;
        self
    }
}

impl StatefulWidget for BranchList<'_> {
    type State = ListState;

    fn render(mut self, area: Rect, buf: &mut Buffer, state: &mut Self::State) {
        buf.set_style(area, self.style);
        let list_area = match self.block.take() {
            Some(b) => {
                let inner_area = b.inner(area);
                b.render(area, buf);
                inner_area
            }
            None => area,
        };

        if list_area.width < 1 || list_area.height < 1 {
            return;
        }

        if self.items.is_empty() {
            return;
        }
        let list_height = list_area.height as usize;

        let mut start = state.offset;
        let height = std::cmp::min(list_height, self.items.len().saturating_sub(state.offset));
        let mut end = start + height;

        let selected = state.selected.unwrap_or(0).min(self.items.len() - 1);

        let move_to_end = (selected + SCROLL_MARGIN).min(self.items.len() - 1);
        let move_to_start = selected.saturating_sub(SCROLL_MARGIN);

        if move_to_end >= end {
            let diff = move_to_end + 1 - end;
            end += diff;
            start += diff;
        }
        if move_to_start < start {
            let diff = start - move_to_start;
            end -= diff;
            start -= diff;
        }
        state.offset = start;

        let highlight_symbol = self.highlight_symbol.unwrap_or("");
        let blank_symbol = " ".repeat(highlight_symbol.width());

        let mut max_scroll = 0;
        let mut current_height = 0;
        for (i, item) in self
            .items
            .iter_mut()
            .enumerate()
            .skip(state.offset)
            .take(end - start)
        {
            let (x, y) = match self.start_corner {
                Corner::BottomLeft => {
                    current_height += 1;
                    (list_area.left(), list_area.bottom() - current_height)
                }
                _ => {
                    let pos = (list_area.left(), list_area.top() + current_height);
                    current_height += 1;
                    pos
                }
            };
            let area = Rect {
                x,
                y,
                width: list_area.width,
                height: 1,
            };
            let item_style = self.style.patch(item.style);
            buf.set_style(area, item_style);

            let is_selected = state.selected.map(|s| s == i).unwrap_or(false);
            let elem_x = if item.is_selectable() {
                let symbol = if is_selected {
                    highlight_symbol
                } else {
                    &blank_symbol
                };
                let (x, _) = buf.set_stringn(x, y, symbol, list_area.width as usize, item_style);
                x
            } else {
                x
            };

            let max_element_width = (list_area.width - (elem_x - x)) as usize;

            if state.scroll_x > 0 && item.content.content.width() > max_element_width {
                if item.content.content.width() - max_element_width > max_scroll {
                    max_scroll = item.content.content.width() - max_element_width;
                }

                let start = std::cmp::min(
                    item.content
                        .content
                        .width()
                        .saturating_sub(max_element_width)
                        + 2,
                    std::cmp::min(item.content.content.width(), state.scroll_x as usize + 2),
                );
                let span = Span::styled(
                    format!("..{}", &item.content.content[start..]),
                    item.content.style,
                );
                buf.set_span(elem_x, y, &span, max_element_width as u16);
            } else {
                buf.set_span(elem_x, y, &item.content, max_element_width as u16);
            }
            if is_selected {
                buf.set_style(area, self.highlight_style);
            }
        }
        if state.scroll_x > max_scroll as u16 {
            state.scroll_x = max_scroll as u16;
        }

        let scroll_start = list_area.top() as usize
            + (((list_height * start) as f32 / self.items.len() as f32).ceil() as usize)
                .min(list_height - 1);
        let scroll_height = (((list_height * list_height) as f32 / self.items.len() as f32).floor()
            as usize)
            .clamp(1, list_height);

        if scroll_height < list_height {
            for y in scroll_start..(scroll_start + scroll_height) {
                buf.set_string(
                    list_area.left() + list_area.width,
                    y as u16,
                    SCROLLBAR_STR,
                    self.style,
                );
            }
        }
    }
}

impl Widget for BranchList<'_> {
    fn render(self, area: Rect, buf: &mut Buffer) {
        let mut state = ListState::default();
        StatefulWidget::render(self, area, buf, &mut state);
    }
}