Some tweaks to pop-up UIs
This commit is contained in:
44
src/main.rs
44
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 {
|
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,
|
||||||
|
|||||||
@@ -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}"),
|
||||||
{
|
|
||||||
" [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,
|
|
||||||
Style::default().fg(colors.text_pending()),
|
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);
|
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
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user