| @@ -30,13 +30,12 @@ pub(crate) fn draw(frame: &mut Frame<'_>, app: &mut App, decorations: &Decoratio · 29 unmodified lines |
| 30 | 30 | .map(|row| row.lane.trim_end().chars().count().saturating_add(1)) |
| 31 | 31 | .max() |
| 32 | 32 | .unwrap_or_default(); |
| 33 | | − let pane_limit = ((body.width as usize) / 3) |
| 33 | + let align_limit = ((body.width as usize) / 3) |
| 34 | 34 | .saturating_sub(2) |
| 35 | 35 | .max(1) |
| 36 | 36 | .min(content.width as usize); |
| 37 | | − let pane_width = max_lane_width.min(pane_limit); |
| 38 | | − let graph_is_wide = max_lane_width > pane_limit; |
| 39 | | − let pin_metadata = app.pin_metadata.unwrap_or(graph_is_wide); |
| 37 | + let align_width = max_lane_width.min(align_limit); |
| 38 | + let align_metadata = app.align_metadata; |
| 40 | 39 | let show_committer_date = app.show_committer_date; |
| 41 | 40 | let show_author_name = app.show_author_name; |
| 42 | 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 | 63 | .collect(); |
| 65 | | − let max_offset = if pin_metadata { |
| 66 | | − max_lane_width.saturating_sub(pane_width) |
| 64 | + let graph_max_offset = max_lane_width.saturating_sub(align_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 | 77 | } else { |
| 68 | 78 | visible_rows |
| 69 | 79 | .iter() |
| @@ -74,7 +84,9 @@ pub(crate) fn draw(frame: &mut Frame<'_>, app: &mut App, decorations: &Decoratio · 4 unmodified lines |
| 74 | 84 | .saturating_sub(content.width as usize) |
| 75 | 85 | } |
| 76 | 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 | 91 | let visible_rows = &app.rows[start..end]; |
| 80 | 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 | 106 | let row_area = Rect::new(content.x, y, content.width, 1); |
| 95 | | − if pin_metadata { |
| 107 | + if align_metadata { |
| 96 | 108 | frame.render_widget( |
| 97 | 109 | Paragraph::new(row.lane.as_str()) |
| 98 | 110 | .style(style) |
| 99 | | − .scroll((0, horizontal_offset)), |
| 111 | + .scroll((0, graph_offset as u16)), |
| 100 | 112 | row_area, |
| 101 | 113 | ); |
| 102 | | − color_graph(frame, row_area, &row.lane, horizontal_offset as usize, selected); |
| 103 | | − let pane = Rect::new( |
| 104 | | − content.x.saturating_add(pane_width as u16), |
| 114 | + color_graph(frame, row_area, &row.lane, graph_offset, selected); |
| 115 | + let aligned = Rect::new( |
| 116 | + content.x.saturating_add(align_width as u16), |
| 105 | 117 | y, |
| 106 | | − content.width.saturating_sub(pane_width as u16), |
| 118 | + content.width.saturating_sub(align_width as u16), |
| 107 | 119 | 1, |
| 108 | 120 | ); |
| 109 | | − frame.render_widget(Clear, pane); |
| 110 | | − frame.render_widget(Paragraph::new(metadata), pane); |
| 121 | + frame.render_widget(Clear, aligned); |
| 122 | + frame.render_widget(Paragraph::new(metadata).scroll((0, metadata_offset as u16)), aligned); |
| 111 | 123 | } else { |
| 112 | 124 | let mut spans = Vec::with_capacity(metadata.spans.len() + 1); |
| 113 | 125 | spans.push(Span::styled(row.lane.as_str(), style)); |
| 114 | 126 | spans.extend(metadata.spans); |
| 115 | 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 | 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 | 133 | if selected && body.width > 0 { |
| 122 | | − let line_width = if pin_metadata { |
| 123 | | − pane_width.saturating_add(metadata_width) |
| 134 | + let line_width = if align_metadata { |
| 135 | + align_width.saturating_add(metadata_width.saturating_sub(metadata_offset)) |
| 124 | 136 | } else { |
| 125 | 137 | row.lane |
| 126 | 138 | .chars() |
| 127 | 139 | .count() |
| 128 | 140 | .saturating_add(metadata_width) |
| 129 | | − .saturating_sub(horizontal_offset as usize) |
| 141 | + .saturating_sub(horizontal_offset) |
| 130 | 142 | }; |
| 131 | 143 | let marker_x = content |
| 132 | 144 | .x |
| @@ -145,9 +157,10 @@ pub(crate) fn draw(frame: &mut Frame<'_>, app: &mut App, decorations: &Decoratio · 12 unmodified lines |
| 145 | 157 | State::Cancelled => "cancelled", |
| 146 | 158 | }; |
| 147 | 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 | 161 | app.rows.len() |
| 150 | 162 | ))]; |
| 163 | + footer_spans.extend([Span::raw(" · "), toggle("[ align", app.align_metadata)]); |
| 151 | 164 | if app.has_hidden_filter { |
| 152 | 165 | footer_spans.extend([ |
| 153 | 166 | Span::raw(" · "), |
| @@ -533,7 +546,7 @@ mod tests { · 379 unmodified lines |
| 533 | 546 | |
| 534 | 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 | 550 | let selected_line = "> ● 0101010 (HEAD) 1970-01-01 mapped author subject"; |
| 538 | 551 | let mut expected = Buffer::with_lines([format!("{selected_line:<140}"), format!("{footer_text:<140}")]); |
| 539 | 552 | for x in 0..11 { |
| @@ -780,38 +793,36 @@ mod tests { · 240 unmodified lines |
| 780 | 793 | committer_time: gix::date::Time::default(), |
| 781 | 794 | author: author(b"author", b"author@example.com"), |
| 782 | 795 | attributions: Box::default(), |
| 783 | | − title: "subject".into(), |
| 796 | + title: format!("{} subject-tail", "a".repeat(50)).into(), |
| 784 | 797 | }]); |
| 785 | 798 | app.update(Action::Complete); |
| 786 | 799 | app.rows[0].lane = format!("{}{}", "A".repeat(40), "B".repeat(40)); |
| 787 | 800 | let mut terminal = Terminal::new(TestBackend::new(60, 2))?; |
| 788 | 801 | |
| 789 | 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 | 804 | .find("0101010") |
| 792 | | − .expect("wide graphs automatically pin metadata"); |
| 793 | | − assert!( |
| 794 | | − pane_column < 60, |
| 795 | | − "wide graphs automatically pin metadata within the viewport" |
| 796 | | − ); |
| 805 | + .expect("alignment keeps metadata visible beside wide graphs"); |
| 806 | + assert!(aligned_column < 60, "alignment keeps metadata within the viewport"); |
| 797 | 807 | |
| 798 | 808 | app.update(Action::ScrollRight); |
| 799 | 809 | terminal.draw(|frame| draw(frame, &mut app, &Decorations::new()))?; |
| 800 | 810 | assert_eq!(terminal.backend().buffer()[(2, 0)].symbol(), "B"); |
| 801 | 811 | assert_eq!( |
| 802 | 812 | rendered_row(&terminal).find("0101010"), |
| 803 | | − Some(pane_column), |
| 804 | | − "horizontal graph scrolling leaves pinned metadata fixed" |
| 813 | + Some(aligned_column), |
| 814 | + "horizontal graph scrolling leaves aligned metadata fixed" |
| 805 | 815 | ); |
| 806 | 816 | |
| 807 | 817 | app.update(Action::ScrollLeft); |
| 808 | 818 | terminal.draw(|frame| draw(frame, &mut app, &Decorations::new()))?; |
| 809 | | − app.update(Action::UnpinMetadata); |
| 819 | + app.update(Action::ToggleAlign); |
| 810 | 820 | terminal.draw(|frame| draw(frame, &mut app, &Decorations::new()))?; |
| 811 | 821 | assert!( |
| 812 | 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 | 827 | app.update(Action::ScrollRight); |
| 817 | 828 | terminal.draw(|frame| draw(frame, &mut app, &Decorations::new()))?; |
| @@ -820,13 +831,25 @@ mod tests { · 2 unmodified lines |
| 820 | 831 | "l pages far enough right to reveal natural metadata" |
| 821 | 832 | ); |
| 822 | 833 | |
| 834 | + app.update(Action::ScrollLeft); |
| 823 | 835 | app.rows[0].lane = "● ".into(); |
| 824 | | − app.update(Action::PinMetadata); |
| 836 | + app.update(Action::ToggleAlign); |
| 825 | 837 | terminal.draw(|frame| draw(frame, &mut app, &Decorations::new()))?; |
| 826 | 838 | assert_eq!( |
| 827 | 839 | terminal.backend().buffer()[(4, 0)].symbol(), |
| 828 | 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 | 854 | Ok(()) |
| 832 | 855 | } |