Skill Tree page UI tweaks and improvements

This commit is contained in:
2026-02-28 06:03:20 +00:00
parent b37dc72b45
commit 8e4f9bf064
11 changed files with 627 additions and 157 deletions

View File

@@ -32,9 +32,8 @@ fn wrapped_branch_columns(area_width: u16, branch_count: usize) -> usize {
return 1;
}
let width = area_width as usize;
let max_cols_by_width = ((width + BRANCH_CELL_GUTTER)
/ (MIN_BRANCH_CELL_WIDTH + BRANCH_CELL_GUTTER))
.max(1);
let max_cols_by_width =
((width + BRANCH_CELL_GUTTER) / (MIN_BRANCH_CELL_WIDTH + BRANCH_CELL_GUTTER)).max(1);
max_cols_by_width.min(branch_count)
}

View File

@@ -160,12 +160,7 @@ impl Widget for Dashboard<'_> {
];
let lines: Vec<Line> = pack_hint_lines(&hints, inner.width as usize)
.into_iter()
.map(|line| {
Line::from(Span::styled(
line,
Style::default().fg(colors.accent()),
))
})
.map(|line| Line::from(Span::styled(line, Style::default().fg(colors.accent()))))
.collect();
Paragraph::new(lines)
};

View File

@@ -59,6 +59,41 @@ pub fn detail_line_count(branch_id: BranchId) -> usize {
.sum::<usize>()
}
pub fn detail_line_count_with_level_spacing(branch_id: BranchId, level_spacing: bool) -> usize {
let base = detail_line_count(branch_id);
if !level_spacing {
return base;
}
let def = get_branch_definition(branch_id);
base + def.levels.len().saturating_sub(1)
}
pub fn use_expanded_level_spacing(detail_area_height: u16, branch_id: BranchId) -> bool {
let def = get_branch_definition(branch_id);
let base = detail_line_count(branch_id);
let extra = def.levels.len().saturating_sub(1);
(detail_area_height as usize) >= base + extra
}
pub fn use_side_by_side_layout(inner_width: u16) -> bool {
inner_width >= 100
}
pub fn branch_list_spacing_flags(branch_area_height: u16, branch_count: usize) -> (bool, bool) {
if branch_count == 0 {
return (false, false);
}
// Base lines: 2 per branch + 1 separator after lowercase.
let base_lines = branch_count * 2 + 1;
let extra_lines = (branch_area_height as usize).saturating_sub(base_lines);
// Priority 1: one spacer between each progress bar and following branch title.
let inter_branch_needed = branch_count.saturating_sub(1);
let inter_branch_spacing = extra_lines >= inter_branch_needed;
// Priority 2: one extra line above and below "Branches (...)" separator.
let separator_padding = inter_branch_spacing && extra_lines >= inter_branch_needed + 2;
(inter_branch_spacing, separator_padding)
}
impl Widget for SkillTreeWidget<'_> {
fn render(self, area: Rect, buf: &mut Buffer) {
let colors = &self.theme.colors;
@@ -70,9 +105,8 @@ impl Widget for SkillTreeWidget<'_> {
let inner = block.inner(area);
block.render(area, buf);
// Layout: branch list, separator, detail panel, footer (adaptive height)
// Layout: main split (branch list + detail) and 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 {
@@ -84,8 +118,17 @@ impl Widget for SkillTreeWidget<'_> {
],
Some("Complete a-z to unlock branches"),
)
} else if bp.status == BranchStatus::Available || bp.status == BranchStatus::InProgress
{
} else if bp.status == BranchStatus::Available {
(
vec![
"[Enter] Unlock",
"[↑↓/jk] Navigate",
"[PgUp/PgDn or Ctrl+U/Ctrl+D] Scroll",
"[q] Back",
],
None,
)
} else if bp.status == BranchStatus::InProgress {
(
vec![
"[Enter] Start Drill",
@@ -128,28 +171,67 @@ impl Widget for SkillTreeWidget<'_> {
let layout = Layout::default()
.direction(Direction::Vertical)
.constraints([
Constraint::Length(
branch_list_height.min(inner.height.saturating_sub(footer_height + 4)),
),
Constraint::Length(1),
Constraint::Min(4),
Constraint::Length(footer_height),
])
.constraints([Constraint::Min(4), Constraint::Length(footer_height)])
.split(inner);
// --- Branch list ---
self.render_branch_list(layout[0], buf, &branches);
if use_side_by_side_layout(inner.width) {
let main = Layout::default()
.direction(Direction::Horizontal)
.constraints([
Constraint::Percentage(42),
Constraint::Length(1),
Constraint::Percentage(58),
])
.split(layout[0]);
// --- Separator ---
let sep = Paragraph::new(Line::from(Span::styled(
"\u{2500}".repeat(layout[1].width as usize),
Style::default().fg(colors.border()),
)));
sep.render(layout[1], buf);
// --- Branch list (left pane) ---
let (inter_branch_spacing, separator_padding) =
branch_list_spacing_flags(main[0].height, branches.len());
self.render_branch_list(
main[0],
buf,
&branches,
inter_branch_spacing,
separator_padding,
);
// --- Detail panel for selected branch ---
self.render_detail_panel(layout[2], buf, &branches);
// --- Vertical separator ---
let sep_lines: Vec<Line> = (0..main[1].height)
.map(|_| {
Line::from(Span::styled(
"\u{2502}",
Style::default().fg(colors.border()),
))
})
.collect();
Paragraph::new(sep_lines).render(main[1], buf);
// --- Detail panel for selected branch (right pane) ---
self.render_detail_panel(main[2], buf, &branches, true);
} else {
let branch_list_height = branches.len() as u16 * 2 + 1;
let main = Layout::default()
.direction(Direction::Vertical)
.constraints([
Constraint::Length(branch_list_height.min(layout[0].height.saturating_sub(4))),
Constraint::Length(1),
Constraint::Min(3),
])
.split(layout[0]);
// --- Branch list (top pane) ---
self.render_branch_list(main[0], buf, &branches, false, false);
// --- Horizontal separator ---
let sep = Paragraph::new(Line::from(Span::styled(
"\u{2500}".repeat(main[1].width as usize),
Style::default().fg(colors.border()),
)));
sep.render(main[1], buf);
// --- Detail panel (bottom pane) ---
self.render_detail_panel(main[2], buf, &branches, true);
}
// --- Footer ---
let mut footer_lines: Vec<Line> = Vec::new();
@@ -168,16 +250,27 @@ impl Widget for SkillTreeWidget<'_> {
))
}));
let footer = Paragraph::new(footer_lines).wrap(Wrap { trim: false });
footer.render(layout[3], buf);
footer.render(layout[1], buf);
}
}
impl SkillTreeWidget<'_> {
fn render_branch_list(&self, area: Rect, buf: &mut Buffer, branches: &[BranchId]) {
fn render_branch_list(
&self,
area: Rect,
buf: &mut Buffer,
branches: &[BranchId],
inter_branch_spacing: bool,
separator_padding: bool,
) {
let colors = &self.theme.colors;
let mut lines: Vec<Line> = Vec::new();
for (i, &branch_id) in branches.iter().enumerate() {
if i > 0 && inter_branch_spacing {
lines.push(Line::from(""));
}
let bp = self.skill_tree.branch_progress(branch_id);
let def = get_branch_definition(branch_id);
let total_keys = def.levels.iter().map(|l| l.keys.len()).sum::<usize>();
@@ -249,10 +342,18 @@ impl SkillTreeWidget<'_> {
// Add separator after Lowercase (index 0)
if branch_id == BranchId::Lowercase {
if separator_padding {
lines.push(Line::from(""));
}
lines.push(Line::from(Span::styled(
" \u{2500}\u{2500} Branches (unlocked after a-z) \u{2500}\u{2500}",
Style::default().fg(colors.border()),
" \u{2500}\u{2500} Branches (available after a-z) \u{2500}\u{2500}",
Style::default().fg(colors.text_pending()),
)));
// If inter-branch spacing is enabled, the next branch will already
// insert one blank line before its title.
if separator_padding && !inter_branch_spacing {
lines.push(Line::from(""));
}
}
}
@@ -260,7 +361,13 @@ impl SkillTreeWidget<'_> {
paragraph.render(area, buf);
}
fn render_detail_panel(&self, area: Rect, buf: &mut Buffer, branches: &[BranchId]) {
fn render_detail_panel(
&self,
area: Rect,
buf: &mut Buffer,
branches: &[BranchId],
allow_expanded_level_spacing: bool,
) {
let colors = &self.theme.colors;
if self.selected >= branches.len() {
@@ -270,6 +377,8 @@ impl SkillTreeWidget<'_> {
let branch_id = branches[self.selected];
let bp = self.skill_tree.branch_progress(branch_id);
let def = get_branch_definition(branch_id);
let expanded_level_spacing =
allow_expanded_level_spacing && use_expanded_level_spacing(area.height, branch_id);
let mut lines: Vec<Line> = Vec::new();
@@ -401,6 +510,10 @@ impl SkillTreeWidget<'_> {
]));
}
}
if expanded_level_spacing && level_idx + 1 < def.levels.len() {
lines.push(Line::from(""));
}
}
let visible_height = area.height as usize;
@@ -437,4 +550,3 @@ fn dual_progress_bar_parts(
"\u{2591}".repeat(empty_cells),
)
}

View File

@@ -166,12 +166,7 @@ impl Widget for StatsDashboard<'_> {
// Footer
let footer_lines: Vec<Line> = footer_lines_vec
.into_iter()
.map(|line| {
Line::from(Span::styled(
line,
Style::default().fg(colors.accent()),
))
})
.map(|line| Line::from(Span::styled(line, Style::default().fg(colors.accent()))))
.collect();
Paragraph::new(footer_lines).render(layout[2], buf);