History

feat: toggle align mode with a single key

Codex GPT-5 committed 3 months ago

Commit 28fa84d1 parent ba30a933 files · +68 −52Browse files

Unified · Split

3 files
  1. gix-tix/src/app.rs +8 −10
  2. gix-tix/src/lib.rs +2 −7
  3. gix-tix/src/ui.rs +58 −35
gix-tix/src/app.rs+8−10
@@ -81,8 +81,7 @@ pub(crate) enum Action { · 80 unmodified lines
8181 ToggleMailmap,
8282 ToggleSpecialRefs,
8383 ToggleHidden,
84− PinMetadata,
85− UnpinMetadata,
84+ ToggleAlign,
8685 Cancel,
8786 Copy,
8887 Quit,
@@ -112,7 +111,7 @@ pub(crate) struct App { · 23 unmodified lines
112111 pub show_special_refs: bool,
113112 pub has_hidden_filter: bool,
114113 pub show_hidden: bool,
115− pub pin_metadata: Option<bool>,
114+ pub align_metadata: bool,
116115 pub horizontal_offset: usize,
117116 horizontal_page: usize,
118117 horizontal_max: usize,
@@ -136,7 +135,7 @@ impl App { · 17 unmodified lines
136135 show_special_refs: false,
137136 has_hidden_filter: false,
138137 show_hidden: false,
139− pin_metadata: None,
138+ align_metadata: true,
140139 horizontal_offset: 0,
141140 horizontal_page: 1,
142141 horizontal_max: 0,
@@ -232,8 +231,7 @@ impl App { · 89 unmodified lines
232231 {
233232 return vec![Effect::Reload(!self.show_hidden)];
234233 }
235− Action::PinMetadata => self.pin_metadata = Some(true),
236− Action::UnpinMetadata => self.pin_metadata = Some(false),
234+ Action::ToggleAlign => self.align_metadata = !self.align_metadata,
237235 Action::Cancel if self.state == State::Loading => {
238236 self.state = State::Cancelling;
239237 return vec![Effect::Cancel];
@@ -620,16 +618,16 @@ mod tests { · 380 unmodified lines
620618 app.update(Action::ToggleTrailers);
621619 app.update(Action::ToggleMailmap);
622620 app.update(Action::ToggleSpecialRefs);
623− app.update(Action::PinMetadata);
621+ app.update(Action::ToggleAlign);
624622
625623 assert!(!app.show_committer_date);
626624 assert!(!app.show_author_name);
627625 assert!(!app.show_trailers);
628626 assert!(!app.use_mailmap);
629627 assert!(app.show_special_refs);
630− assert_eq!(app.pin_metadata, Some(true));
631− app.update(Action::UnpinMetadata);
632− assert_eq!(app.pin_metadata, Some(false));
628+ assert!(!app.align_metadata);
629+ app.update(Action::ToggleAlign);
630+ assert!(app.align_metadata);
633631 }
634632
635633 #[test]
gix-tix/src/lib.rs+2−7
@@ -331,8 +331,7 @@ fn action(key: KeyEvent) -> Option<Action> { · 330 unmodified lines
331331 KeyCode::Char('m') => Some(Action::ToggleMailmap),
332332 KeyCode::Char('r') => Some(Action::ToggleSpecialRefs),
333333 KeyCode::Char('v') => Some(Action::ToggleHidden),
334− KeyCode::Char('[') => Some(Action::PinMetadata),
335− KeyCode::Char(']') => Some(Action::UnpinMetadata),
334+ KeyCode::Char('[') => Some(Action::ToggleAlign),
336335 KeyCode::Char('y') => Some(Action::Copy),
337336 _ => None,
338337 }
@@ -432,11 +431,7 @@ mod tests { · 93 unmodified lines
432431 );
433432 assert_eq!(
434433 action(KeyEvent::new(KeyCode::Char('['), KeyModifiers::NONE)),
435− Some(Action::PinMetadata)
436− );
437− assert_eq!(
438− action(KeyEvent::new(KeyCode::Char(']'), KeyModifiers::NONE)),
439− Some(Action::UnpinMetadata)
434+ Some(Action::ToggleAlign)
440435 );
441436 assert_eq!(
442437 action(KeyEvent::new(KeyCode::Char('c'), KeyModifiers::CONTROL)),
gix-tix/src/ui.rs+58−35
@@ -30,13 +30,12 @@ pub(crate) fn draw(frame: &mut Frame<'_>, app: &mut App, decorations: &Decoratio · 29 unmodified lines
3030 .map(|row| row.lane.trim_end().chars().count().saturating_add(1))
3131 .max()
3232 .unwrap_or_default();
33− let pane_limit = ((body.width as usize) / 3)
33+ let align_limit = ((body.width as usize) / 3)
3434 .saturating_sub(2)
3535 .max(1)
3636 .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;
4039 let show_committer_date = app.show_committer_date;
4140 let show_author_name = app.show_author_name;
4241 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
6261 )
6362 })
6463 .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)
6777 } else {
6878 visible_rows
6979 .iter()
@@ -74,7 +84,9 @@ pub(crate) fn draw(frame: &mut Frame<'_>, app: &mut App, decorations: &Decoratio · 4 unmodified lines
7484 .saturating_sub(content.width as usize)
7585 }
7686 .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);
7890
7991 let visible_rows = &app.rows[start..end];
8092 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
92104 );
93105
94106 let row_area = Rect::new(content.x, y, content.width, 1);
95− if pin_metadata {
107+ if align_metadata {
96108 frame.render_widget(
97109 Paragraph::new(row.lane.as_str())
98110 .style(style)
99− .scroll((0, horizontal_offset)),
111+ .scroll((0, graph_offset as u16)),
100112 row_area,
101113 );
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),
105117 y,
106− content.width.saturating_sub(pane_width as u16),
118+ content.width.saturating_sub(align_width as u16),
107119 1,
108120 );
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);
111123 } else {
112124 let mut spans = Vec::with_capacity(metadata.spans.len() + 1);
113125 spans.push(Span::styled(row.lane.as_str(), style));
114126 spans.extend(metadata.spans);
115127 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)),
117129 row_area,
118130 );
119− color_graph(frame, row_area, &row.lane, horizontal_offset as usize, selected);
131+ color_graph(frame, row_area, &row.lane, horizontal_offset, selected);
120132 }
121133 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))
124136 } else {
125137 row.lane
126138 .chars()
127139 .count()
128140 .saturating_add(metadata_width)
129− .saturating_sub(horizontal_offset as usize)
141+ .saturating_sub(horizontal_offset)
130142 };
131143 let marker_x = content
132144 .x
@@ -145,9 +157,10 @@ pub(crate) fn draw(frame: &mut Frame<'_>, app: &mut App, decorations: &Decoratio · 12 unmodified lines
145157 State::Cancelled => "cancelled",
146158 };
147159 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",
149161 app.rows.len()
150162 ))];
163+ footer_spans.extend([Span::raw(" · "), toggle("[ align", app.align_metadata)]);
151164 if app.has_hidden_filter {
152165 footer_spans.extend([
153166 Span::raw(" · "),
@@ -533,7 +546,7 @@ mod tests { · 379 unmodified lines
533546
534547 terminal.draw(|frame| super::draw(frame, &mut app, &decorations, &mailmap))?;
535548
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";
537550 let selected_line = "> ● 0101010 (HEAD) 1970-01-01 mapped author subject";
538551 let mut expected = Buffer::with_lines([format!("{selected_line:<140}"), format!("{footer_text:<140}")]);
539552 for x in 0..11 {
@@ -780,38 +793,36 @@ mod tests { · 240 unmodified lines
780793 committer_time: gix::date::Time::default(),
781794 author: author(b"author", b"author@example.com"),
782795 attributions: Box::default(),
783− title: "subject".into(),
796+ title: format!("{} subject-tail", "a".repeat(50)).into(),
784797 }]);
785798 app.update(Action::Complete);
786799 app.rows[0].lane = format!("{}{}", "A".repeat(40), "B".repeat(40));
787800 let mut terminal = Terminal::new(TestBackend::new(60, 2))?;
788801
789802 terminal.draw(|frame| draw(frame, &mut app, &Decorations::new()))?;
790− let pane_column = rendered_row(&terminal)
803+ let aligned_column = rendered_row(&terminal)
791804 .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");
797807
798808 app.update(Action::ScrollRight);
799809 terminal.draw(|frame| draw(frame, &mut app, &Decorations::new()))?;
800810 assert_eq!(terminal.backend().buffer()[(2, 0)].symbol(), "B");
801811 assert_eq!(
802812 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"
805815 );
806816
807817 app.update(Action::ScrollLeft);
808818 terminal.draw(|frame| draw(frame, &mut app, &Decorations::new()))?;
809− app.update(Action::UnpinMetadata);
819+ app.update(Action::ToggleAlign);
810820 terminal.draw(|frame| draw(frame, &mut app, &Decorations::new()))?;
811821 assert!(
812822 !rendered_row(&terminal).contains("0101010"),
813− "] restores natural post-graph placement"
823+ "[ restores natural post-graph placement"
814824 );
825+ assert!(footer_is_dim(&terminal, "[ align"), "disabled alignment is dimmed");
815826
816827 app.update(Action::ScrollRight);
817828 terminal.draw(|frame| draw(frame, &mut app, &Decorations::new()))?;
@@ -820,13 +831,25 @@ mod tests { · 2 unmodified lines
820831 "l pages far enough right to reveal natural metadata"
821832 );
822833
834+ app.update(Action::ScrollLeft);
823835 app.rows[0].lane = "● ".into();
824− app.update(Action::PinMetadata);
836+ app.update(Action::ToggleAlign);
825837 terminal.draw(|frame| draw(frame, &mut app, &Decorations::new()))?;
826838 assert_eq!(
827839 terminal.backend().buffer()[(4, 0)].symbol(),
828840 "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"
830853 );
831854 Ok(())
832855 }