1use crate::icons;
4use crate::theme::Theme;
5use arct_core::{Lesson, LessonLibrary};
6use ratatui::{
7 layout::{Alignment, Constraint, Direction, Layout, Rect},
8 text::{Line, Span},
9 widgets::{Block, Borders, Clear, List, ListItem, Paragraph},
10 Frame,
11};
12use std::collections::HashSet;
13
14pub struct LessonMenuPanel {
16 library: LessonLibrary,
17 selected_index: usize,
18 scroll_offset: usize,
19}
20
21impl LessonMenuPanel {
22 pub fn new() -> Self {
23 Self {
24 library: LessonLibrary::new(),
25 selected_index: 0,
26 scroll_offset: 0,
27 }
28 }
29
30 pub fn select_previous(&mut self) {
32 let total = self.library.all().len();
33 if total > 0 {
34 self.selected_index = if self.selected_index == 0 {
35 total - 1
36 } else {
37 self.selected_index - 1
38 };
39 }
40 }
41
42 pub fn select_next(&mut self) {
44 let total = self.library.all().len();
45 if total > 0 {
46 self.selected_index = (self.selected_index + 1) % total;
47 }
48 }
49
50 fn ensure_visible(&mut self, visible_height: usize) {
52 let lines_per_lesson = 3;
54 let visible_lessons = visible_height / lines_per_lesson;
55
56 if visible_lessons == 0 {
57 return;
58 }
59
60 if self.selected_index >= self.scroll_offset + visible_lessons {
62 self.scroll_offset = self.selected_index.saturating_sub(visible_lessons - 1);
63 }
64
65 if self.selected_index < self.scroll_offset {
67 self.scroll_offset = self.selected_index;
68 }
69 }
70
71 pub fn select_by_number(&mut self, number: usize) {
73 let total = self.library.all().len();
74 if number > 0 && number <= total {
75 self.selected_index = number - 1;
76 }
77 }
78
79 pub fn get_selected_lesson(&self) -> Option<Lesson> {
81 let lessons = self.library.all();
82 lessons.get(self.selected_index).map(|&l| l.clone())
83 }
84
85 pub fn render(
87 &mut self,
88 frame: &mut Frame,
89 theme: &Theme,
90 completed_lessons: &HashSet<String>,
91 user_stats: &arct_core::UserStats,
92 recommendation_engine: &arct_core::RecommendationEngine,
93 ) {
94 let area = Self::centered_rect(70, 60, frame.size());
95
96 frame.render_widget(Clear, area);
98
99 let block = Block::default()
100 .title(format!(" {}Lesson Selection Menu ", icons::lesson().content))
101 .title_alignment(Alignment::Center)
102 .borders(Borders::ALL)
103 .border_style(theme.style_border_focused())
104 .style(theme.style_block()); let inner = block.inner(area);
107 frame.render_widget(block, area);
108
109 let chunks = Layout::default()
111 .direction(Direction::Vertical)
112 .constraints([
113 Constraint::Length(3), Constraint::Length(8), Constraint::Min(10), Constraint::Length(4), ])
118 .split(inner);
119
120 let header = Paragraph::new(vec![
122 Line::from(vec![
123 Span::styled("Select a lesson to begin your learning journey!", theme.style_normal()),
124 ]),
125 Line::from(vec![
126 Span::styled("Completed lessons are marked with ", theme.style_dim()),
127 icons::celebration(),
128 ]),
129 ])
130 .alignment(Alignment::Center);
131 frame.render_widget(header, chunks[0]);
132
133 let recommendations = recommendation_engine.get_recommendations(
135 completed_lessons,
136 user_stats,
137 3, );
139
140 let mut rec_items = Vec::new();
141 rec_items.push(ListItem::new(Line::from(vec![
142 icons::target(),
143 Span::styled(" Recommended for You", theme.style_header()),
144 ])));
145 rec_items.push(ListItem::new(Line::from("")));
146
147 if recommendations.is_empty() {
148 rec_items.push(ListItem::new(Line::from(vec![
149 Span::styled(" No recommendations yet. Complete some lessons to get started!", theme.style_dim()),
150 ])));
151 } else {
152 for rec in recommendations {
153 let reason_text = match rec.reason {
154 arct_core::RecommendationReason::NextInSequence => "Next in sequence",
155 arct_core::RecommendationReason::PrerequisiteSatisfied => "Prerequisites met!",
156 arct_core::RecommendationReason::SameDifficulty => "Matches your level",
157 arct_core::RecommendationReason::SkillLevelMatch => "Perfect for you",
158 arct_core::RecommendationReason::RelatedTopic => "Related topic",
159 _ => "Recommended",
160 };
161
162 let lesson_title = rec.lesson.title.clone();
163 rec_items.push(ListItem::new(Line::from(vec![
164 Span::raw(" "),
165 icons::lightning(),
166 Span::styled(lesson_title, theme.style_accent()),
167 Span::raw(" "),
168 Span::styled(format!("({})", reason_text), theme.style_dim()),
169 ])));
170 }
171 }
172
173 let rec_list = List::new(rec_items);
174 frame.render_widget(rec_list, chunks[1]);
175
176 let list_area_height = chunks[2].height as usize;
178
179 self.ensure_visible(list_area_height);
181
182 let lessons = self.library.all();
183
184 let mut items = Vec::new();
185
186 let lines_per_lesson = 3;
188 let visible_lessons = list_area_height / lines_per_lesson;
189 let end_idx = (self.scroll_offset + visible_lessons).min(lessons.len());
190
191 if self.scroll_offset > 0 {
193 items.push(ListItem::new(Line::from(vec![
194 Span::styled("▲ ▲ ▲ Scroll up for more lessons ▲ ▲ ▲", theme.style_dim()),
195 ])));
196 }
197
198 for (idx, lesson) in lessons.iter().enumerate().skip(self.scroll_offset).take(end_idx - self.scroll_offset) {
199 let is_selected = idx == self.selected_index;
200 let is_completed = completed_lessons.contains(&lesson.id);
201
202 let number = format!("[{}] ", idx + 1);
203 let status_icon = if is_completed {
204 icons::celebration()
205 } else {
206 icons::lesson()
207 };
208
209 let difficulty_text = format!("{:?}", lesson.difficulty);
210 let time_text = format!("{}min", lesson.estimated_minutes);
211
212 let mut line_spans = vec![
213 Span::styled(number, theme.style_accent()),
214 status_icon,
215 Span::styled(&lesson.title, if is_selected {
216 theme.style_accent().add_modifier(ratatui::style::Modifier::BOLD)
217 } else {
218 theme.style_normal()
219 }),
220 ];
221
222 let padding = " ".repeat(40_usize.saturating_sub(lesson.title.len()));
224 line_spans.push(Span::raw(padding));
225 line_spans.push(Span::styled(
226 format!(" {} | {} ", difficulty_text, time_text),
227 theme.style_dim(),
228 ));
229
230 let mut description_line = vec![Span::raw(" ")];
231 if is_completed {
232 description_line.push(icons::success());
233 description_line.push(Span::styled("Completed ", theme.style_success()));
234 }
235 description_line.push(Span::styled(&lesson.description, theme.style_dim()));
236
237 items.push(ListItem::new(vec![
238 Line::from(line_spans),
239 Line::from(description_line),
240 Line::from(""),
241 ]));
242 }
243
244 if end_idx < lessons.len() {
246 items.push(ListItem::new(Line::from(vec![
247 Span::styled("▼ ▼ ▼ Scroll down for more lessons ▼ ▼ ▼", theme.style_dim()),
248 ])));
249 }
250
251 let list = List::new(items);
252 frame.render_widget(list, chunks[2]);
253
254 let total_lessons = lessons.len();
256 let quick_select_text = if total_lessons <= 10 {
257 vec![
258 Span::styled("1-9", theme.style_accent()),
259 Span::raw(" for lessons 1-9, "),
260 Span::styled("0", theme.style_accent()),
261 Span::raw(" for lesson 10 | "),
262 ]
263 } else {
264 vec![
265 Span::styled("1-9,0", theme.style_accent()),
266 Span::raw(" for first 10, use "),
267 Span::styled("↑/↓", theme.style_accent()),
268 Span::raw(" for others | "),
269 ]
270 };
271
272 let mut control_line1 = vec![
273 Span::styled("↑/↓", theme.style_accent()),
274 Span::raw(" or "),
275 Span::styled("j/k", theme.style_accent()),
276 Span::raw(" navigate | "),
277 ];
278 control_line1.extend(quick_select_text);
279 control_line1.push(Span::styled("Enter", theme.style_accent()));
280 control_line1.push(Span::raw(" start"));
281
282 let controls = Paragraph::new(vec![
283 Line::from(control_line1),
284 Line::from(vec![
285 Span::styled("Esc", theme.style_accent()),
286 Span::raw(" or "),
287 Span::styled("q", theme.style_accent()),
288 Span::raw(" close menu"),
289 ]),
290 ])
291 .alignment(Alignment::Center);
292 frame.render_widget(controls, chunks[3]);
293 }
294
295 fn centered_rect(percent_x: u16, percent_y: u16, r: Rect) -> Rect {
297 let popup_layout = Layout::default()
298 .direction(Direction::Vertical)
299 .constraints([
300 Constraint::Percentage((100 - percent_y) / 2),
301 Constraint::Percentage(percent_y),
302 Constraint::Percentage((100 - percent_y) / 2),
303 ])
304 .split(r);
305
306 Layout::default()
307 .direction(Direction::Horizontal)
308 .constraints([
309 Constraint::Percentage((100 - percent_x) / 2),
310 Constraint::Percentage(percent_x),
311 Constraint::Percentage((100 - percent_x) / 2),
312 ])
313 .split(popup_layout[1])[1]
314 }
315}
316
317impl Default for LessonMenuPanel {
318 fn default() -> Self {
319 Self::new()
320 }
321}