git_igitt/widgets/
commit_view.rs

1use crate::app::DiffType;
2use crate::util::ctrl_chars::CtrlChars;
3use crate::widgets::list::{ListItem, StatefulList};
4use git2::Oid;
5use tui::buffer::Buffer;
6use tui::layout::Rect;
7use tui::style::Style;
8use tui::widgets::{Block, StatefulWidget, Widget};
9
10#[derive(Default)]
11pub struct CommitViewState {
12    pub content: Option<CommitViewInfo>,
13}
14
15pub struct DiffItem {
16    pub(crate) file: String,
17    pub(crate) diff_type: DiffType,
18}
19
20impl ListItem for DiffItem {
21    fn is_selectable(&self) -> bool {
22        true
23    }
24}
25
26pub struct CommitViewInfo {
27    pub text: Vec<String>,
28    pub diffs: StatefulList<DiffItem>,
29    pub oid: Oid,
30    pub compare_oid: Oid,
31    pub scroll: u16,
32}
33impl CommitViewInfo {
34    pub fn new(
35        text: Vec<String>,
36        diffs: StatefulList<DiffItem>,
37        oid: Oid,
38        compare_oid: Oid,
39    ) -> Self {
40        Self {
41            text,
42            diffs,
43            oid,
44            compare_oid,
45            scroll: 0,
46        }
47    }
48}
49
50#[derive(Default)]
51pub struct CommitView<'a> {
52    block: Option<Block<'a>>,
53    highlight_symbol: Option<&'a str>,
54    style: Style,
55}
56
57impl<'a> CommitView<'a> {
58    pub fn block(mut self, block: Block<'a>) -> CommitView<'a> {
59        self.block = Some(block);
60        self
61    }
62
63    pub fn style(mut self, style: Style) -> CommitView<'a> {
64        self.style = style;
65        self
66    }
67
68    pub fn highlight_symbol(mut self, highlight_symbol: &'a str) -> CommitView<'a> {
69        self.highlight_symbol = Some(highlight_symbol);
70        self
71    }
72}
73
74impl<'a> StatefulWidget for CommitView<'a> {
75    type State = CommitViewState;
76
77    fn render(mut self, area: Rect, buf: &mut Buffer, state: &mut Self::State) {
78        buf.set_style(area, self.style);
79        let list_area = match self.block.take() {
80            Some(b) => {
81                let inner_area = b.inner(area);
82                b.render(area, buf);
83                inner_area
84            }
85            None => area,
86        };
87
88        if list_area.width < 1 || list_area.height < 1 {
89            return;
90        }
91
92        let (x_start, y0) = (list_area.left(), list_area.top());
93        let list_bottom = list_area.top() + list_area.height;
94
95        let max_element_width = list_area.width;
96        if let Some(commit_info) = &state.content {
97            let scroll = commit_info.scroll;
98            let y_start = y0 as i32 - scroll as i32;
99
100            let wrapping =
101                textwrap::Options::new(list_area.width as usize).subsequent_indent("        ");
102            let ellipsis = &format!(
103                "    ...{}",
104                " ".repeat(max_element_width.saturating_sub(7) as usize)
105            )[..max_element_width as usize];
106
107            let mut y = y_start;
108            for (line_idx, text_line) in commit_info.text.iter().enumerate() {
109                if text_line.is_empty() {
110                    y += 1;
111                    if y >= list_bottom as i32 {
112                        buf.set_string(x_start, (y - 1) as u16, ellipsis, self.style);
113                        break;
114                    }
115                } else {
116                    let wrapped = if line_idx > 1 {
117                        #[allow(clippy::needless_borrow)]
118                        textwrap::fill(text_line, &wrapping)
119                    } else {
120                        text_line.clone()
121                    };
122
123                    for line in wrapped.lines() {
124                        let mut x = x_start;
125                        let mut remaining_width = max_element_width;
126
127                        let line_span = CtrlChars::parse(line).into_text();
128                        if y >= y0 as i32 {
129                            for txt in line_span {
130                                for line in txt.lines {
131                                    if remaining_width == 0 {
132                                        break;
133                                    }
134                                    let pos = buf.set_spans(x, y as u16, &line, remaining_width);
135                                    let w = pos.0.saturating_sub(x);
136                                    x = pos.0;
137                                    y = pos.1 as i32;
138                                    remaining_width = remaining_width.saturating_sub(w);
139                                }
140                            }
141                        }
142                        y += 1;
143                        if y >= list_bottom as i32 {
144                            break;
145                        }
146                    }
147                    if y >= list_bottom as i32 {
148                        buf.set_string(x_start, (y - 1) as u16, ellipsis, self.style);
149                        break;
150                    }
151                }
152            }
153        }
154    }
155}
156
157impl<'a> Widget for CommitView<'a> {
158    fn render(self, area: Rect, buf: &mut Buffer) {
159        let mut state = CommitViewState::default();
160        StatefulWidget::render(self, area, buf, &mut state);
161    }
162}