Skip to content

Commit 5131aba

Browse files
authored
Make MsgPopup scrollable (#2120)
1 parent 540a95c commit 5131aba

File tree

2 files changed

+92
-29
lines changed

2 files changed

+92
-29
lines changed

CHANGELOG.md

+1
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1616

1717
### Changed
1818
* re-enable clippy `missing_const_for_fn` linter warning and added const to functions where applicable ([#2116](https://github.com/extrawurst/gitui/issues/2116))
19+
* Make info and error message popups scrollable [[@MichaelAug](https://github.com/MichaelAug)] ([#1138](https://github.com/extrawurst/gitui/issues/1138))
1920

2021
## [0.25.1] - 2024-02-23
2122

src/popups/msg.rs

+91-29
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,16 @@
11
use crate::components::{
22
visibility_blocking, CommandBlocking, CommandInfo, Component,
3-
DrawableComponent, EventState,
3+
DrawableComponent, EventState, ScrollType, VerticalScroll,
44
};
5+
use crate::strings::order;
56
use crate::{
67
app::Environment,
78
keys::{key_match, SharedKeyConfig},
89
strings, ui,
910
};
11+
use anyhow::Result;
1012
use crossterm::event::Event;
13+
use ratatui::text::Line;
1114
use ratatui::{
1215
layout::{Alignment, Rect},
1316
text::Span,
@@ -22,39 +25,68 @@ pub struct MsgPopup {
2225
visible: bool,
2326
theme: SharedTheme,
2427
key_config: SharedKeyConfig,
28+
scroll: VerticalScroll,
2529
}
2630

27-
use anyhow::Result;
31+
const POPUP_HEIGHT: u16 = 25;
32+
const BORDER_WIDTH: u16 = 2;
33+
const MINIMUM_WIDTH: u16 = 60;
2834

2935
impl DrawableComponent for MsgPopup {
3036
fn draw(&self, f: &mut Frame, _rect: Rect) -> Result<()> {
3137
if !self.visible {
3238
return Ok(());
3339
}
3440

41+
let max_width = f.size().width.max(MINIMUM_WIDTH);
42+
3543
// determine the maximum width of text block
36-
let lens = self
44+
let width = self
3745
.msg
38-
.split('\n')
46+
.lines()
3947
.map(str::len)
40-
.collect::<Vec<usize>>();
41-
let mut max = lens.iter().max().expect("max") + 2;
42-
if max > std::u16::MAX as usize {
43-
max = std::u16::MAX as usize;
44-
}
45-
let mut width = u16::try_from(max)
46-
.expect("can't fail due to check above");
47-
// dont overflow screen, and dont get too narrow
48-
if width > f.size().width {
49-
width = f.size().width;
50-
} else if width < 60 {
51-
width = 60;
52-
}
48+
.max()
49+
.unwrap_or(0)
50+
.saturating_add(BORDER_WIDTH.into())
51+
.clamp(MINIMUM_WIDTH.into(), max_width.into())
52+
.try_into()
53+
.expect("can't fail because we're clamping to u16 value");
54+
55+
let area =
56+
ui::centered_rect_absolute(width, POPUP_HEIGHT, f.size());
57+
58+
// Wrap lines and break words if there is not enough space
59+
let wrapped_msg = bwrap::wrap_maybrk!(
60+
&self.msg,
61+
area.width.saturating_sub(BORDER_WIDTH).into()
62+
);
63+
64+
let msg_lines: Vec<String> =
65+
wrapped_msg.lines().map(String::from).collect();
66+
let line_num = msg_lines.len();
67+
68+
let height = POPUP_HEIGHT
69+
.saturating_sub(BORDER_WIDTH)
70+
.min(f.size().height.saturating_sub(BORDER_WIDTH));
71+
72+
let top =
73+
self.scroll.update_no_selection(line_num, height.into());
74+
75+
let scrolled_lines = msg_lines
76+
.iter()
77+
.skip(top)
78+
.take(height.into())
79+
.map(|line| {
80+
Line::from(vec![Span::styled(
81+
line.clone(),
82+
self.theme.text(true, false),
83+
)])
84+
})
85+
.collect::<Vec<Line>>();
5386

54-
let area = ui::centered_rect_absolute(width, 25, f.size());
5587
f.render_widget(Clear, area);
5688
f.render_widget(
57-
Paragraph::new(self.msg.clone())
89+
Paragraph::new(scrolled_lines)
5890
.block(
5991
Block::default()
6092
.title(Span::styled(
@@ -69,6 +101,8 @@ impl DrawableComponent for MsgPopup {
69101
area,
70102
);
71103

104+
self.scroll.draw(f, area, &self.theme);
105+
72106
Ok(())
73107
}
74108
}
@@ -84,6 +118,16 @@ impl Component for MsgPopup {
84118
true,
85119
self.visible,
86120
));
121+
out.push(
122+
CommandInfo::new(
123+
strings::commands::navigate_commit_message(
124+
&self.key_config,
125+
),
126+
true,
127+
self.visible,
128+
)
129+
.order(order::NAV),
130+
);
87131

88132
visibility_blocking(self)
89133
}
@@ -93,6 +137,14 @@ impl Component for MsgPopup {
93137
if let Event::Key(e) = ev {
94138
if key_match(e, self.key_config.keys.enter) {
95139
self.hide();
140+
} else if key_match(
141+
e,
142+
self.key_config.keys.popup_down,
143+
) {
144+
self.scroll.move_top(ScrollType::Down);
145+
} else if key_match(e, self.key_config.keys.popup_up)
146+
{
147+
self.scroll.move_top(ScrollType::Up);
96148
}
97149
}
98150
Ok(EventState::Consumed)
@@ -124,24 +176,34 @@ impl MsgPopup {
124176
visible: false,
125177
theme: env.theme.clone(),
126178
key_config: env.key_config.clone(),
179+
scroll: VerticalScroll::new(),
127180
}
128181
}
129182

130-
///
131-
pub fn show_error(&mut self, msg: &str) -> Result<()> {
132-
self.title = strings::msg_title_error(&self.key_config);
183+
fn set_new_msg(
184+
&mut self,
185+
msg: &str,
186+
title: String,
187+
) -> Result<()> {
188+
self.title = title;
133189
self.msg = msg.to_string();
134-
self.show()?;
190+
self.scroll.reset();
191+
self.show()
192+
}
135193

136-
Ok(())
194+
///
195+
pub fn show_error(&mut self, msg: &str) -> Result<()> {
196+
self.set_new_msg(
197+
msg,
198+
strings::msg_title_error(&self.key_config),
199+
)
137200
}
138201

139202
///
140203
pub fn show_info(&mut self, msg: &str) -> Result<()> {
141-
self.title = strings::msg_title_info(&self.key_config);
142-
self.msg = msg.to_string();
143-
self.show()?;
144-
145-
Ok(())
204+
self.set_new_msg(
205+
msg,
206+
strings::msg_title_info(&self.key_config),
207+
)
146208
}
147209
}

0 commit comments

Comments
 (0)