1use ratatui::{
2 buffer::Buffer,
3 layout::{Alignment, Constraint, Flex, Layout, Rect, Size},
4 style::{Style, Stylize},
5 text::Line,
6 widgets::{
7 Block, BorderType, Clear, Padding, Paragraph, Scrollbar, ScrollbarOrientation,
8 ScrollbarState, StatefulWidget, Widget, Wrap,
9 },
10};
11
12use crate::app::{calc_scroll_amount, Message as AppMessage, ScrollAmount};
13use crate::config::Theme;
14
15fn modal_area_height(size: Size) -> usize {
16 let vertical = Layout::vertical([Constraint::Percentage(50)]).flex(Flex::Center);
17 let [area] = vertical.areas(Rect::new(0, 0, size.width, size.height.saturating_sub(3)));
18 area.height.into()
19}
20
21#[derive(Clone, Debug, PartialEq)]
22pub enum Message {
23 Toggle,
24 Close,
25 ScrollUp(ScrollAmount),
26 ScrollDown(ScrollAmount),
27}
28
29pub fn update<'a>(
30 message: &Message,
31 screen_size: Size,
32 state: &mut HelpModalState,
33) -> Option<AppMessage<'a>> {
34 match message {
35 Message::Toggle => state.toggle_visibility(),
36 Message::Close => state.hide(),
37 Message::ScrollDown(scroll_amount) => {
38 state.scroll_down(calc_scroll_amount(
39 scroll_amount,
40 modal_area_height(screen_size),
41 ));
42 }
43 Message::ScrollUp(scroll_amount) => {
44 state.scroll_up(calc_scroll_amount(
45 scroll_amount,
46 modal_area_height(screen_size),
47 ));
48 }
49 };
50
51 None
52}
53
54#[derive(Debug, Default, Clone, PartialEq)]
55pub struct HelpModalState {
56 pub scrollbar_state: ScrollbarState,
57 pub scrollbar_position: usize,
58 pub text: String,
59 pub visible: bool,
60}
61
62impl HelpModalState {
63 pub fn new(text: &str) -> Self {
64 Self {
65 text: text.to_string(),
66 scrollbar_state: ScrollbarState::new(text.lines().count()),
67 ..Default::default()
68 }
69 }
70
71 pub fn toggle_visibility(&mut self) {
72 self.visible = !self.visible;
73 }
74
75 pub fn hide(&mut self) {
76 self.visible = false;
77 }
78
79 pub fn scroll_up(&mut self, amount: usize) {
80 let scrollbar_position = self.scrollbar_position.saturating_sub(amount);
81 let scrollbar_state = self.scrollbar_state.position(scrollbar_position);
82
83 self.scrollbar_state = scrollbar_state;
84 self.scrollbar_position = scrollbar_position;
85 }
86
87 pub fn scroll_down(&mut self, amount: usize) {
88 let scrollbar_position = self
89 .scrollbar_position
90 .saturating_add(amount)
91 .min(self.text.lines().count());
92
93 let scrollbar_state = self.scrollbar_state.position(scrollbar_position);
94
95 self.scrollbar_state = scrollbar_state;
96 self.scrollbar_position = scrollbar_position;
97 }
98}
99
100fn modal_area(area: Rect) -> Rect {
101 let vertical = Layout::vertical([Constraint::Percentage(50)]).flex(Flex::Center);
102 let horizontal = Layout::horizontal([Constraint::Length(83)]).flex(Flex::Center);
103 let [area] = vertical.areas(area);
104 let [area] = horizontal.areas(area);
105 area
106}
107
108pub struct HelpModal {
109 pub border_type: BorderType,
110 pub theme: Theme,
111}
112
113impl HelpModal {
114 pub fn new(border_type: BorderType, theme: Theme) -> Self {
115 Self { border_type, theme }
116 }
117}
118
119impl StatefulWidget for HelpModal {
120 type State = HelpModalState;
121
122 fn render(self, area: Rect, buf: &mut Buffer, state: &mut Self::State)
123 where
124 Self: Sized,
125 {
126 let block = Block::bordered()
127 .fg(self.theme.muted)
128 .bg(self.theme.background)
129 .border_type(self.border_type)
130 .padding(Padding::uniform(1))
131 .title_style(Style::default().italic().bold())
132 .title(" Help ")
133 .title(Line::from(" (?) ").alignment(Alignment::Right));
134
135 let area = modal_area(area);
136
137 Widget::render(Clear, area, buf);
138 Widget::render(
139 Paragraph::new(state.text.clone())
140 .wrap(Wrap::default())
141 .scroll((state.scrollbar_position as u16, 0))
142 .block(block)
143 .fg(self.theme.text),
144 area,
145 buf,
146 );
147
148 StatefulWidget::render(
149 Scrollbar::new(ScrollbarOrientation::VerticalRight),
150 area,
151 buf,
152 &mut state.scrollbar_state,
153 );
154 }
155}