| @@ -30,13 +30,12 @@ pub(crate) fn draw(frame: &mut Frame<'_>, app: &mut App, decorations: &Decoratio · 29 unmodified lines |
| 30 | .map(|row| row.lane.trim_end().chars().count().saturating_add(1)) | 30 | .map(|row| row.lane.trim_end().chars().count().saturating_add(1)) |
| 31 | .max() | 31 | .max() |
| 32 | .unwrap_or_default(); | 32 | .unwrap_or_default(); |
| 33 | let pane_limit = ((body.width as usize) / 3) | 33 | let align_limit = ((body.width as usize) / 3) |
| 34 | .saturating_sub(2) | 34 | .saturating_sub(2) |
| 35 | .max(1) | 35 | .max(1) |
| 36 | .min(content.width as usize); | 36 | .min(content.width as usize); |
| 37 | let pane_width = max_lane_width.min(pane_limit); | 37 | let align_width = max_lane_width.min(align_limit); |
| 38 | let graph_is_wide = max_lane_width > pane_limit; | 38 | let align_metadata = app.align_metadata; |
| 39 | let pin_metadata = app.pin_metadata.unwrap_or(graph_is_wide); | | |
| 40 | let show_committer_date = app.show_committer_date; | 39 | let show_committer_date = app.show_committer_date; |
| 41 | let show_author_name = app.show_author_name; | 40 | let show_author_name = app.show_author_name; |
| 42 | let show_trailers = app.show_trailers; | 41 | let show_trailers = app.show_trailers; |
| @@ -62,8 +61,19 @@ pub(crate) fn draw(frame: &mut Frame<'_>, app: &mut App, decorations: &Decoratio · 19 unmodified lines |
| 62 | ) | 61 | ) |
| 63 | }) | 62 | }) |
| 64 | .collect(); | 63 | .collect(); |
| 65 | let max_offset = if pin_metadata { | 64 | let graph_max_offset = max_lane_width.saturating_sub(align_width); |
| 66 | max_lane_width.saturating_sub(pane_width) | 65 | let metadata_max_offset = if align_metadata { |
| | 66 | metadata |
| | 67 | .iter() |
| | 68 | .map(Line::width) |
| | 69 | .max() |
| | 70 | .unwrap_or_default() |
| | 71 | .saturating_sub((content.width as usize).saturating_sub(align_width)) |
| | 72 | } else { |
| | 73 | 0 |
| | 74 | }; |
| | 75 | let max_offset = if align_metadata { |
| | 76 | graph_max_offset.saturating_add(metadata_max_offset) |
| 67 | } else { | 77 | } else { |
| 68 | visible_rows | 78 | visible_rows |
| 69 | .iter() | 79 | .iter() |
| @@ -74,7 +84,9 @@ pub(crate) fn draw(frame: &mut Frame<'_>, app: &mut App, decorations: &Decoratio · 4 unmodified lines |
| 74 | .saturating_sub(content.width as usize) | 84 | .saturating_sub(content.width as usize) |
| 75 | } | 85 | } |
| 76 | .min(u16::MAX as usize); | 86 | .min(u16::MAX as usize); |
| 77 | let horizontal_offset = app.horizontal_offset.min(max_offset) as u16; | 87 | let horizontal_offset = app.horizontal_offset.min(max_offset); |
| | 88 | let graph_offset = horizontal_offset.min(graph_max_offset); |
| | 89 | let metadata_offset = horizontal_offset.saturating_sub(graph_max_offset); |
| 78 | | 90 | |
| 79 | let visible_rows = &app.rows[start..end]; | 91 | let visible_rows = &app.rows[start..end]; |
| 80 | for (index, (row, metadata)) in visible_rows.iter().zip(metadata).enumerate() { | 92 | for (index, (row, metadata)) in visible_rows.iter().zip(metadata).enumerate() { |
| @@ -92,41 +104,41 @@ pub(crate) fn draw(frame: &mut Frame<'_>, app: &mut App, decorations: &Decoratio · 11 unmodified lines |
| 92 | ); | 104 | ); |
| 93 | | 105 | |
| 94 | let row_area = Rect::new(content.x, y, content.width, 1); | 106 | let row_area = Rect::new(content.x, y, content.width, 1); |
| 95 | if pin_metadata { | 107 | if align_metadata { |
| 96 | frame.render_widget( | 108 | frame.render_widget( |
| 97 | Paragraph::new(row.lane.as_str()) | 109 | Paragraph::new(row.lane.as_str()) |
| 98 | .style(style) | 110 | .style(style) |
| 99 | .scroll((0, horizontal_offset)), | 111 | .scroll((0, graph_offset as u16)), |
| 100 | row_area, | 112 | row_area, |
| 101 | ); | 113 | ); |
| 102 | color_graph(frame, row_area, &row.lane, horizontal_offset as usize, selected); | 114 | color_graph(frame, row_area, &row.lane, graph_offset, selected); |
| 103 | let pane = Rect::new( | 115 | let aligned = Rect::new( |
| 104 | content.x.saturating_add(pane_width as u16), | 116 | content.x.saturating_add(align_width as u16), |
| 105 | y, | 117 | y, |
| 106 | content.width.saturating_sub(pane_width as u16), | 118 | content.width.saturating_sub(align_width as u16), |
| 107 | 1, | 119 | 1, |
| 108 | ); | 120 | ); |
| 109 | frame.render_widget(Clear, pane); | 121 | frame.render_widget(Clear, aligned); |
| 110 | frame.render_widget(Paragraph::new(metadata), pane); | 122 | frame.render_widget(Paragraph::new(metadata).scroll((0, metadata_offset as u16)), aligned); |
| 111 | } else { | 123 | } else { |
| 112 | let mut spans = Vec::with_capacity(metadata.spans.len() + 1); | 124 | let mut spans = Vec::with_capacity(metadata.spans.len() + 1); |
| 113 | spans.push(Span::styled(row.lane.as_str(), style)); | 125 | spans.push(Span::styled(row.lane.as_str(), style)); |
| 114 | spans.extend(metadata.spans); | 126 | spans.extend(metadata.spans); |
| 115 | frame.render_widget( | 127 | frame.render_widget( |
| 116 | Paragraph::new(Line::from(spans)).scroll((0, horizontal_offset)), | 128 | Paragraph::new(Line::from(spans)).scroll((0, horizontal_offset as u16)), |
| 117 | row_area, | 129 | row_area, |
| 118 | ); | 130 | ); |
| 119 | color_graph(frame, row_area, &row.lane, horizontal_offset as usize, selected); | 131 | color_graph(frame, row_area, &row.lane, horizontal_offset, selected); |
| 120 | } | 132 | } |
| 121 | if selected && body.width > 0 { | 133 | if selected && body.width > 0 { |
| 122 | let line_width = if pin_metadata { | 134 | let line_width = if align_metadata { |
| 123 | pane_width.saturating_add(metadata_width) | 135 | align_width.saturating_add(metadata_width.saturating_sub(metadata_offset)) |
| 124 | } else { | 136 | } else { |
| 125 | row.lane | 137 | row.lane |
| 126 | .chars() | 138 | .chars() |
| 127 | .count() | 139 | .count() |
| 128 | .saturating_add(metadata_width) | 140 | .saturating_add(metadata_width) |
| 129 | .saturating_sub(horizontal_offset as usize) | 141 | .saturating_sub(horizontal_offset) |
| 130 | }; | 142 | }; |
| 131 | let marker_x = content | 143 | let marker_x = content |
| 132 | .x | 144 | .x |
| @@ -145,9 +157,10 @@ pub(crate) fn draw(frame: &mut Frame<'_>, app: &mut App, decorations: &Decoratio · 12 unmodified lines |
| 145 | State::Cancelled => "cancelled", | 157 | State::Cancelled => "cancelled", |
| 146 | }; | 158 | }; |
| 147 | let mut footer_spans = vec![Span::raw(format!( | 159 | let mut footer_spans = vec![Span::raw(format!( |
| 148 | "{} commits · {status} · ↑↓/jk move · h/l pan · [ pane · ] natural", | 160 | "{} commits · {status} · ↑↓/jk move · h/l pan", |
| 149 | app.rows.len() | 161 | app.rows.len() |
| 150 | ))]; | 162 | ))]; |
| | 163 | footer_spans.extend([Span::raw(" · "), toggle("[ align", app.align_metadata)]); |
| 151 | if app.has_hidden_filter { | 164 | if app.has_hidden_filter { |
| 152 | footer_spans.extend([ | 165 | footer_spans.extend([ |
| 153 | Span::raw(" · "), | 166 | Span::raw(" · "), |
| @@ -533,7 +546,7 @@ mod tests { · 379 unmodified lines |
| 533 | | 546 | |
| 534 | terminal.draw(|frame| super::draw(frame, &mut app, &decorations, &mailmap))?; | 547 | terminal.draw(|frame| super::draw(frame, &mut app, &decorations, &mailmap))?; |
| 535 | | 548 | |
| 536 | let footer_text = "1 commits · complete · ↑↓/jk move · h/l pan · [ pane · ] natural · d date · n name · m mailmap · t trailers · r refs · y copy · q quit"; | 549 | let footer_text = "1 commits · complete · ↑↓/jk move · h/l pan · [ align · d date · n name · m mailmap · t trailers · r refs · y copy · q quit"; |
| 537 | let selected_line = "> ● 0101010 (HEAD) 1970-01-01 mapped author subject"; | 550 | let selected_line = "> ● 0101010 (HEAD) 1970-01-01 mapped author subject"; |
| 538 | let mut expected = Buffer::with_lines([format!("{selected_line:<140}"), format!("{footer_text:<140}")]); | 551 | let mut expected = Buffer::with_lines([format!("{selected_line:<140}"), format!("{footer_text:<140}")]); |
| 539 | for x in 0..11 { | 552 | for x in 0..11 { |
| @@ -780,38 +793,36 @@ mod tests { · 240 unmodified lines |
| 780 | committer_time: gix::date::Time::default(), | 793 | committer_time: gix::date::Time::default(), |
| 781 | author: author(b"author", b"author@example.com"), | 794 | author: author(b"author", b"author@example.com"), |
| 782 | attributions: Box::default(), | 795 | attributions: Box::default(), |
| 783 | title: "subject".into(), | 796 | title: format!("{} subject-tail", "a".repeat(50)).into(), |
| 784 | }]); | 797 | }]); |
| 785 | app.update(Action::Complete); | 798 | app.update(Action::Complete); |
| 786 | app.rows[0].lane = format!("{}{}", "A".repeat(40), "B".repeat(40)); | 799 | app.rows[0].lane = format!("{}{}", "A".repeat(40), "B".repeat(40)); |
| 787 | let mut terminal = Terminal::new(TestBackend::new(60, 2))?; | 800 | let mut terminal = Terminal::new(TestBackend::new(60, 2))?; |
| 788 | | 801 | |
| 789 | terminal.draw(|frame| draw(frame, &mut app, &Decorations::new()))?; | 802 | terminal.draw(|frame| draw(frame, &mut app, &Decorations::new()))?; |
| 790 | let pane_column = rendered_row(&terminal) | 803 | let aligned_column = rendered_row(&terminal) |
| 791 | .find("0101010") | 804 | .find("0101010") |
| 792 | .expect("wide graphs automatically pin metadata"); | 805 | .expect("alignment keeps metadata visible beside wide graphs"); |
| 793 | assert!( | 806 | assert!(aligned_column < 60, "alignment keeps metadata within the viewport"); |
| 794 | pane_column < 60, | | |
| 795 | "wide graphs automatically pin metadata within the viewport" | | |
| 796 | ); | | |
| 797 | | 807 | |
| 798 | app.update(Action::ScrollRight); | 808 | app.update(Action::ScrollRight); |
| 799 | terminal.draw(|frame| draw(frame, &mut app, &Decorations::new()))?; | 809 | terminal.draw(|frame| draw(frame, &mut app, &Decorations::new()))?; |
| 800 | assert_eq!(terminal.backend().buffer()[(2, 0)].symbol(), "B"); | 810 | assert_eq!(terminal.backend().buffer()[(2, 0)].symbol(), "B"); |
| 801 | assert_eq!( | 811 | assert_eq!( |
| 802 | rendered_row(&terminal).find("0101010"), | 812 | rendered_row(&terminal).find("0101010"), |
| 803 | Some(pane_column), | 813 | Some(aligned_column), |
| 804 | "horizontal graph scrolling leaves pinned metadata fixed" | 814 | "horizontal graph scrolling leaves aligned metadata fixed" |
| 805 | ); | 815 | ); |
| 806 | | 816 | |
| 807 | app.update(Action::ScrollLeft); | 817 | app.update(Action::ScrollLeft); |
| 808 | terminal.draw(|frame| draw(frame, &mut app, &Decorations::new()))?; | 818 | terminal.draw(|frame| draw(frame, &mut app, &Decorations::new()))?; |
| 809 | app.update(Action::UnpinMetadata); | 819 | app.update(Action::ToggleAlign); |
| 810 | terminal.draw(|frame| draw(frame, &mut app, &Decorations::new()))?; | 820 | terminal.draw(|frame| draw(frame, &mut app, &Decorations::new()))?; |
| 811 | assert!( | 821 | assert!( |
| 812 | !rendered_row(&terminal).contains("0101010"), | 822 | !rendered_row(&terminal).contains("0101010"), |
| 813 | "] restores natural post-graph placement" | 823 | "[ restores natural post-graph placement" |
| 814 | ); | 824 | ); |
| | 825 | assert!(footer_is_dim(&terminal, "[ align"), "disabled alignment is dimmed"); |
| 815 | | 826 | |
| 816 | app.update(Action::ScrollRight); | 827 | app.update(Action::ScrollRight); |
| 817 | terminal.draw(|frame| draw(frame, &mut app, &Decorations::new()))?; | 828 | terminal.draw(|frame| draw(frame, &mut app, &Decorations::new()))?; |
| @@ -820,13 +831,25 @@ mod tests { · 2 unmodified lines |
| 820 | "l pages far enough right to reveal natural metadata" | 831 | "l pages far enough right to reveal natural metadata" |
| 821 | ); | 832 | ); |
| 822 | | 833 | |
| | 834 | app.update(Action::ScrollLeft); |
| 823 | app.rows[0].lane = "● ".into(); | 835 | app.rows[0].lane = "● ".into(); |
| 824 | app.update(Action::PinMetadata); | 836 | app.update(Action::ToggleAlign); |
| 825 | terminal.draw(|frame| draw(frame, &mut app, &Decorations::new()))?; | 837 | terminal.draw(|frame| draw(frame, &mut app, &Decorations::new()))?; |
| 826 | assert_eq!( | 838 | assert_eq!( |
| 827 | terminal.backend().buffer()[(4, 0)].symbol(), | 839 | terminal.backend().buffer()[(4, 0)].symbol(), |
| 828 | "0", | 840 | "0", |
| 829 | "the pane starts immediately after the widest visible lane" | 841 | "aligned metadata starts immediately after the widest visible lane" |
| | 842 | ); |
| | 843 | assert!( |
| | 844 | !rendered_row(&terminal).contains("subject-tail"), |
| | 845 | "long aligned metadata starts clipped" |
| | 846 | ); |
| | 847 | |
| | 848 | app.update(Action::ScrollRight); |
| | 849 | terminal.draw(|frame| draw(frame, &mut app, &Decorations::new()))?; |
| | 850 | assert!( |
| | 851 | rendered_row(&terminal).contains("subject-tail"), |
| | 852 | "l reveals clipped aligned metadata after graph panning is exhausted" |
| 830 | ); | 853 | ); |
| 831 | Ok(()) | 854 | Ok(()) |
| 832 | } | 855 | } |