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