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 {
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,

View File

@@ -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 "
}
} 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,
let mut footer_lines: Vec<Line> = 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()),
)));
}
}
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<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
}