ratatui_toolkit/
hotkey_modal.rs1use ratatui::style::{Color, Modifier, Style};
2use ratatui::text::{Line, Span};
3use ratatui::widgets::{Block, BorderType, Borders, Clear, Paragraph, Wrap};
4use ratatui::Frame;
5
6#[derive(Debug, Clone)]
8pub struct Hotkey {
9 pub key: String,
10 pub description: String,
11}
12
13impl Hotkey {
14 pub fn new(key: impl Into<String>, description: impl Into<String>) -> Self {
15 Self {
16 key: key.into(),
17 description: description.into(),
18 }
19 }
20}
21
22#[derive(Debug, Clone)]
24pub struct HotkeySection {
25 pub title: String,
26 pub hotkeys: Vec<Hotkey>,
27}
28
29impl HotkeySection {
30 pub fn new(title: impl Into<String>) -> Self {
31 Self {
32 title: title.into(),
33 hotkeys: Vec::new(),
34 }
35 }
36
37 pub fn with_hotkeys(mut self, hotkeys: Vec<Hotkey>) -> Self {
38 self.hotkeys = hotkeys;
39 self
40 }
41
42 pub fn add_hotkey(mut self, key: impl Into<String>, description: impl Into<String>) -> Self {
43 self.hotkeys.push(Hotkey::new(key, description));
44 self
45 }
46}
47
48#[derive(Debug, Clone)]
50pub struct HotkeyModalConfig {
51 pub title: String,
53 pub border_color: Color,
55 pub width_percent: f32,
57 pub height_percent: f32,
59 pub footer: Option<String>,
61 pub title_inside: bool,
63}
64
65impl Default for HotkeyModalConfig {
66 fn default() -> Self {
67 Self {
68 title: "Help".to_string(),
69 border_color: Color::Cyan,
70 width_percent: 0.6,
71 height_percent: 0.6,
72 footer: Some("Press any key to close".to_string()),
73 title_inside: false,
74 }
75 }
76}
77
78impl HotkeyModalConfig {
79 pub fn new() -> Self {
80 Self::default()
81 }
82
83 pub fn with_title(mut self, title: impl Into<String>) -> Self {
84 self.title = title.into();
85 self
86 }
87
88 pub fn with_border_color(mut self, color: Color) -> Self {
89 self.border_color = color;
90 self
91 }
92
93 pub fn with_size(mut self, width_percent: f32, height_percent: f32) -> Self {
94 self.width_percent = width_percent.clamp(0.1, 1.0);
95 self.height_percent = height_percent.clamp(0.1, 1.0);
96 self
97 }
98
99 pub fn with_footer(mut self, footer: Option<String>) -> Self {
100 self.footer = footer;
101 self
102 }
103
104 pub fn with_title_inside(mut self, inside: bool) -> Self {
105 self.title_inside = inside;
106 self
107 }
108}
109
110pub fn render_hotkey_modal(
112 frame: &mut Frame,
113 sections: &[HotkeySection],
114 config: &HotkeyModalConfig,
115) {
116 let mut lines = Vec::new();
117
118 if config.title_inside {
120 lines.push(Line::from(vec![Span::styled(
121 &config.title,
122 Style::default()
123 .add_modifier(Modifier::BOLD)
124 .fg(config.border_color),
125 )]));
126 lines.push(Line::from(""));
127 }
128
129 for (i, section) in sections.iter().enumerate() {
131 lines.push(Line::from(vec![Span::styled(
133 §ion.title,
134 Style::default().add_modifier(Modifier::BOLD),
135 )]));
136 lines.push(Line::from(""));
137
138 for hotkey in §ion.hotkeys {
140 let line = format!(" {:<12}{}", hotkey.key, hotkey.description);
142 lines.push(Line::from(line));
143 }
144
145 if i < sections.len() - 1 {
147 lines.push(Line::from(""));
148 }
149 }
150
151 if let Some(ref footer) = config.footer {
153 lines.push(Line::from(""));
154 lines.push(Line::from(vec![Span::styled(
155 footer,
156 Style::default().fg(Color::DarkGray),
157 )]));
158 }
159
160 let area = frame.area();
161
162 let overlay_paragraph = Paragraph::new("").style(
164 Style::default()
165 .bg(Color::Rgb(0, 0, 0)) .fg(Color::Rgb(40, 40, 40)),
167 );
168 frame.render_widget(overlay_paragraph, area);
169
170 let popup_width = (area.width as f32 * config.width_percent) as u16;
172 let popup_height = (area.height as f32 * config.height_percent) as u16;
173 let popup_x = (area.width - popup_width) / 2;
174 let popup_y = (area.height - popup_height) / 2;
175
176 let popup_area = ratatui::layout::Rect {
177 x: popup_x,
178 y: popup_y,
179 width: popup_width,
180 height: popup_height,
181 };
182
183 frame.render_widget(Clear, popup_area);
185
186 let block = if config.title_inside {
188 Block::default()
190 .borders(Borders::ALL)
191 .border_type(BorderType::Rounded)
192 .border_style(Style::default().fg(config.border_color))
193 } else {
194 Block::default()
196 .title(format!(" {} ", config.title))
197 .borders(Borders::ALL)
198 .border_type(BorderType::Rounded)
199 .border_style(Style::default().fg(config.border_color))
200 };
201
202 let paragraph = Paragraph::new(lines)
203 .style(Style::default())
204 .wrap(Wrap { trim: false });
205
206 frame.render_widget(block, popup_area);
208
209 let inner_area = popup_area.inner(ratatui::layout::Margin {
211 horizontal: 2,
212 vertical: 2,
213 });
214 frame.render_widget(paragraph, inner_area);
215}