Some tweaks to pop-up UIs

This commit is contained in:
2026-02-18 05:26:52 +00:00
parent d0605f8426
commit 4e39e99732
2 changed files with 148 additions and 33 deletions

View File

@@ -807,7 +807,7 @@ fn handle_skill_tree_key(app: &mut App, key: KeyEvent) {
fn skill_tree_detail_max_scroll(app: &App) -> usize { fn skill_tree_detail_max_scroll(app: &App) -> usize {
let (w, h) = crossterm::terminal::size().unwrap_or((120, 40)); let (w, h) = crossterm::terminal::size().unwrap_or((120, 40));
let screen = Rect::new(0, 0, w, h); 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( let inner = Rect::new(
centered.x.saturating_add(1), centered.x.saturating_add(1),
centered.y.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) 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) { fn render(frame: &mut ratatui::Frame, app: &App) {
let area = frame.area(); let area = frame.area();
let colors = &app.theme.colors; let colors = &app.theme.colors;
@@ -1446,7 +1452,7 @@ fn render_code_language_select(frame: &mut ratatui::Frame, app: &App) {
String::new() String::new()
}; };
let style = if is_disabled { let name_style = if is_disabled {
Style::default().fg(colors.text_pending()) Style::default().fg(colors.text_pending())
} else if is_selected { } else if is_selected {
Style::default() Style::default()
@@ -1455,11 +1461,18 @@ fn render_code_language_select(frame: &mut ratatui::Frame, app: &App) {
} else { } else {
Style::default().fg(colors.fg()) Style::default().fg(colors.fg())
}; };
let status_style = Style::default()
.fg(colors.text_pending())
.add_modifier(Modifier::DIM);
lines.push(Line::from(Span::styled( let mut spans = vec![Span::styled(
format!("{indicator}{display}{current_marker}{availability}"), format!("{indicator}{display}{current_marker}"),
style, 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 // 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 { } else {
" (download required)".to_string() " (download required)".to_string()
}; };
let style = if is_disabled { let name_style = if is_disabled {
Style::default().fg(colors.text_pending()) Style::default().fg(colors.text_pending())
} else if is_selected { } else if is_selected {
Style::default() Style::default()
@@ -1559,10 +1572,17 @@ fn render_passage_book_select(frame: &mut ratatui::Frame, app: &App) {
} else { } else {
Style::default().fg(colors.fg()) Style::default().fg(colors.fg())
}; };
lines.push(Line::from(Span::styled( let status_style = Style::default()
format!("{indicator}[{}] {label}{availability}", i + 1), .fg(colors.text_pending())
style, .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()); 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) { fn render_skill_tree(frame: &mut ratatui::Frame, app: &App) {
let area = frame.area(); let area = frame.area();
let centered = ui::layout::centered_rect(70, 90, area); let centered = skill_tree_popup_rect(area);
let widget = SkillTreeWidget::new( let widget = SkillTreeWidget::new(
&app.skill_tree, &app.skill_tree,
&app.key_stats, &app.key_stats,

View File

@@ -2,7 +2,7 @@ use ratatui::buffer::Buffer;
use ratatui::layout::{Constraint, Direction, Layout, Rect}; use ratatui::layout::{Constraint, Direction, Layout, Rect};
use ratatui::style::{Modifier, Style}; use ratatui::style::{Modifier, Style};
use ratatui::text::{Line, Span}; 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::key_stats::KeyStatsStore;
use crate::engine::skill_tree::{ use crate::engine::skill_tree::{
@@ -69,17 +69,71 @@ impl Widget for SkillTreeWidget<'_> {
let inner = block.inner(area); let inner = block.inner(area);
block.render(area, buf); 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 branches = selectable_branches();
let branch_list_height = branches.len() as u16 * 2 + 1; // all branches * 2 lines + separator after Lowercase 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() let layout = Layout::default()
.direction(Direction::Vertical) .direction(Direction::Vertical)
.constraints([ .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::Length(1),
Constraint::Min(4), Constraint::Min(4),
Constraint::Length(2), Constraint::Length(footer_height),
]) ])
.split(inner); .split(inner);
@@ -97,24 +151,19 @@ impl Widget for SkillTreeWidget<'_> {
self.render_detail_panel(layout[2], buf, &branches); self.render_detail_panel(layout[2], buf, &branches);
// --- Footer --- // --- Footer ---
let footer_text = if self.selected < branches.len() { let mut footer_lines: Vec<Line> = Vec::new();
let bp = self.skill_tree.branch_progress(branches[self.selected]); if show_notice {
if *self.skill_tree.branch_status(branches[self.selected]) == BranchStatus::Locked { if let Some(notice) = footer_notice {
" Complete a-z to unlock branches [\u{2191}\u{2193}/jk] Navigate [PgUp/PgDn or Ctrl+U/Ctrl+D] Scroll [q] Back " footer_lines.push(Line::from(Span::styled(
} else if bp.status == BranchStatus::Available || bp.status == BranchStatus::InProgress format!(" {notice}"),
{ Style::default().fg(colors.text_pending()),
" [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 "
} }
} else { }
" [\u{2191}\u{2193}/jk] Navigate [PgUp/PgDn or Ctrl+U/Ctrl+D] Scroll [q] Back " footer_lines.extend(hint_lines.into_iter().map(|line| {
}; Line::from(Span::styled(line, Style::default().fg(colors.text_pending())))
}));
let footer = Paragraph::new(Line::from(Span::styled( let footer = Paragraph::new(footer_lines).wrap(Wrap { trim: false });
footer_text,
Style::default().fg(colors.text_pending()),
)));
footer.render(layout[3], buf); footer.render(layout[3], buf);
} }
} }
@@ -384,3 +433,49 @@ fn dual_progress_bar_parts(
"\u{2591}".repeat(empty_cells), "\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<String> {
if width == 0 || hints.is_empty() {
return Vec::new();
}
let prefix = " ";
let separator = " ";
let mut out: Vec<String> = 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
}