diff --git a/src/main.rs b/src/main.rs index 137ccc0..ee671b6 100644 --- a/src/main.rs +++ b/src/main.rs @@ -807,7 +807,7 @@ fn handle_skill_tree_key(app: &mut App, key: KeyEvent) { fn skill_tree_detail_max_scroll(app: &App) -> usize { let (w, h) = crossterm::terminal::size().unwrap_or((120, 40)); let screen = Rect::new(0, 0, w, h); - let centered = ui::layout::centered_rect(70, 90, screen); + let centered = skill_tree_popup_rect(screen); let inner = Rect::new( centered.x.saturating_add(1), centered.y.saturating_add(1), @@ -837,6 +837,12 @@ fn skill_tree_detail_max_scroll(app: &App) -> usize { total_lines.saturating_sub(detail_height) } +fn skill_tree_popup_rect(area: Rect) -> Rect { + let percent_x = if area.width < 120 { 95 } else { 85 }; + let percent_y = if area.height < 40 { 95 } else { 90 }; + ui::layout::centered_rect(percent_x, percent_y, area) +} + fn render(frame: &mut ratatui::Frame, app: &App) { let area = frame.area(); let colors = &app.theme.colors; @@ -1446,7 +1452,7 @@ fn render_code_language_select(frame: &mut ratatui::Frame, app: &App) { String::new() }; - let style = if is_disabled { + let name_style = if is_disabled { Style::default().fg(colors.text_pending()) } else if is_selected { Style::default() @@ -1455,11 +1461,18 @@ fn render_code_language_select(frame: &mut ratatui::Frame, app: &App) { } else { Style::default().fg(colors.fg()) }; + let status_style = Style::default() + .fg(colors.text_pending()) + .add_modifier(Modifier::DIM); - lines.push(Line::from(Span::styled( - format!("{indicator}{display}{current_marker}{availability}"), - style, - ))); + let mut spans = vec![Span::styled( + format!("{indicator}{display}{current_marker}"), + name_style, + )]; + if !availability.is_empty() { + spans.push(Span::styled(availability, status_style)); + } + lines.push(Line::from(spans)); } // Show scroll indicator at bottom if more items below @@ -1550,7 +1563,7 @@ fn render_passage_book_select(frame: &mut ratatui::Frame, app: &App) { } else { " (download required)".to_string() }; - let style = if is_disabled { + let name_style = if is_disabled { Style::default().fg(colors.text_pending()) } else if is_selected { Style::default() @@ -1559,10 +1572,17 @@ fn render_passage_book_select(frame: &mut ratatui::Frame, app: &App) { } else { Style::default().fg(colors.fg()) }; - lines.push(Line::from(Span::styled( - format!("{indicator}[{}] {label}{availability}", i + 1), - style, - ))); + let status_style = Style::default() + .fg(colors.text_pending()) + .add_modifier(Modifier::DIM); + let mut spans = vec![Span::styled( + format!("{indicator}[{}] {label}", i + 1), + name_style, + )]; + if !availability.is_empty() { + spans.push(Span::styled(availability, status_style)); + } + lines.push(Line::from(spans)); } Paragraph::new(lines).render(list_area, frame.buffer_mut()); @@ -2039,7 +2059,7 @@ fn render_code_download_progress(frame: &mut ratatui::Frame, app: &App) { fn render_skill_tree(frame: &mut ratatui::Frame, app: &App) { let area = frame.area(); - let centered = ui::layout::centered_rect(70, 90, area); + let centered = skill_tree_popup_rect(area); let widget = SkillTreeWidget::new( &app.skill_tree, &app.key_stats, diff --git a/src/ui/components/skill_tree.rs b/src/ui/components/skill_tree.rs index bc9b5f5..cc3ad72 100644 --- a/src/ui/components/skill_tree.rs +++ b/src/ui/components/skill_tree.rs @@ -2,7 +2,7 @@ use ratatui::buffer::Buffer; use ratatui::layout::{Constraint, Direction, Layout, Rect}; use ratatui::style::{Modifier, Style}; use ratatui::text::{Line, Span}; -use ratatui::widgets::{Block, Paragraph, Widget}; +use ratatui::widgets::{Block, Paragraph, Widget, Wrap}; use crate::engine::key_stats::KeyStatsStore; use crate::engine::skill_tree::{ @@ -69,17 +69,71 @@ impl Widget for SkillTreeWidget<'_> { let inner = block.inner(area); block.render(area, buf); - // Layout: header (2), branch list (dynamic), separator (1), detail panel (dynamic), footer (2) + // Layout: branch list, separator, detail panel, footer (adaptive height) let branches = selectable_branches(); let branch_list_height = branches.len() as u16 * 2 + 1; // all branches * 2 lines + separator after Lowercase + let (footer_hints, footer_notice) = if self.selected < branches.len() { + let bp = self.skill_tree.branch_progress(branches[self.selected]); + if *self.skill_tree.branch_status(branches[self.selected]) == BranchStatus::Locked { + ( + vec![ + "[↑↓/jk] Navigate", + "[PgUp/PgDn or Ctrl+U/Ctrl+D] Scroll", + "[q] Back", + ], + Some("Complete a-z to unlock branches"), + ) + } else if bp.status == BranchStatus::Available || bp.status == BranchStatus::InProgress + { + ( + vec![ + "[Enter] Start Drill", + "[↑↓/jk] Navigate", + "[PgUp/PgDn or Ctrl+U/Ctrl+D] Scroll", + "[q] Back", + ], + None, + ) + } else { + ( + vec![ + "[↑↓/jk] Navigate", + "[PgUp/PgDn or Ctrl+U/Ctrl+D] Scroll", + "[q] Back", + ], + None, + ) + } + } else { + ( + vec![ + "[↑↓/jk] Navigate", + "[PgUp/PgDn or Ctrl+U/Ctrl+D] Scroll", + "[q] Back", + ], + None, + ) + }; + let hint_lines = pack_hint_lines(&footer_hints, inner.width as usize); + let notice_lines = footer_notice + .map(|text| wrapped_line_count(text, inner.width as usize)) + .unwrap_or(0); + let show_notice = + footer_notice.is_some() && (inner.height as usize >= hint_lines.len() + notice_lines + 8); + let footer_needed = hint_lines.len() + if show_notice { notice_lines } else { 0 } + 1; + let footer_height = footer_needed + .min(inner.height.saturating_sub(5) as usize) + .max(1) as u16; let layout = Layout::default() .direction(Direction::Vertical) .constraints([ - Constraint::Length(branch_list_height.min(inner.height.saturating_sub(6))), + Constraint::Length( + branch_list_height.min(inner.height.saturating_sub(footer_height + 4)), + ), Constraint::Length(1), Constraint::Min(4), - Constraint::Length(2), + Constraint::Length(footer_height), ]) .split(inner); @@ -97,24 +151,19 @@ impl Widget for SkillTreeWidget<'_> { self.render_detail_panel(layout[2], buf, &branches); // --- Footer --- - let footer_text = if self.selected < branches.len() { - let bp = self.skill_tree.branch_progress(branches[self.selected]); - if *self.skill_tree.branch_status(branches[self.selected]) == BranchStatus::Locked { - " Complete a-z to unlock branches [\u{2191}\u{2193}/jk] Navigate [PgUp/PgDn or Ctrl+U/Ctrl+D] Scroll [q] Back " - } else if bp.status == BranchStatus::Available || bp.status == BranchStatus::InProgress - { - " [Enter] Start Drill [\u{2191}\u{2193}/jk] Navigate [PgUp/PgDn or Ctrl+U/Ctrl+D] Scroll [q] Back " - } else { - " [\u{2191}\u{2193}/jk] Navigate [PgUp/PgDn or Ctrl+U/Ctrl+D] Scroll [q] Back " + let mut footer_lines: Vec = Vec::new(); + if show_notice { + if let Some(notice) = footer_notice { + footer_lines.push(Line::from(Span::styled( + format!(" {notice}"), + Style::default().fg(colors.text_pending()), + ))); } - } else { - " [\u{2191}\u{2193}/jk] Navigate [PgUp/PgDn or Ctrl+U/Ctrl+D] Scroll [q] Back " - }; - - let footer = Paragraph::new(Line::from(Span::styled( - footer_text, - Style::default().fg(colors.text_pending()), - ))); + } + footer_lines.extend(hint_lines.into_iter().map(|line| { + Line::from(Span::styled(line, Style::default().fg(colors.text_pending()))) + })); + let footer = Paragraph::new(footer_lines).wrap(Wrap { trim: false }); footer.render(layout[3], buf); } } @@ -384,3 +433,49 @@ fn dual_progress_bar_parts( "\u{2591}".repeat(empty_cells), ) } + +fn wrapped_line_count(text: &str, width: usize) -> usize { + if width == 0 { + return 0; + } + let chars = text.chars().count().max(1); + chars.div_ceil(width) +} + +fn pack_hint_lines(hints: &[&str], width: usize) -> Vec { + if width == 0 || hints.is_empty() { + return Vec::new(); + } + + let prefix = " "; + let separator = " "; + let mut out: Vec = Vec::new(); + let mut current = prefix.to_string(); + let mut has_hint = false; + + for hint in hints { + if hint.is_empty() { + continue; + } + let candidate = if has_hint { + format!("{current}{separator}{hint}") + } else { + format!("{current}{hint}") + }; + if candidate.chars().count() <= width { + current = candidate; + has_hint = true; + } else { + if has_hint { + out.push(current); + } + current = format!("{prefix}{hint}"); + has_hint = true; + } + } + + if has_hint { + out.push(current); + } + out +}