use crate::app::calibration_wizard::{
CalibrationStep, CalibrationWizard, KeyStatus, PendingConfirmation,
};
use crate::view::theme::Theme;
use ratatui::{
layout::{Constraint, Layout, Rect},
style::{Modifier, Style},
text::{Line, Span},
widgets::{Block, Borders, Clear, Paragraph, Wrap},
Frame,
};
use rust_i18n::t;
const DIALOG_WIDTH: u16 = 60;
const MIN_DIALOG_HEIGHT: u16 = 20;
pub fn render_calibration_wizard(
frame: &mut Frame,
area: Rect,
wizard: &CalibrationWizard,
theme: &Theme,
) {
let dialog_height = MIN_DIALOG_HEIGHT.min(area.height.saturating_sub(4));
let dialog_width = DIALOG_WIDTH.min(area.width.saturating_sub(4));
let dialog_x = (area.width.saturating_sub(dialog_width)) / 2;
let dialog_y = (area.height.saturating_sub(dialog_height)) / 2;
let dialog_area = Rect {
x: dialog_x,
y: dialog_y,
width: dialog_width,
height: dialog_height,
};
frame.render_widget(Clear, dialog_area);
if wizard.has_pending_confirmation() {
render_confirmation_dialog(frame, dialog_area, wizard, theme);
return;
}
let title = match &wizard.step {
CalibrationStep::Capture { .. } => t!("calibration.title_capture").to_string(),
CalibrationStep::Verify => t!("calibration.title_verify").to_string(),
};
let block = Block::default()
.title(title)
.borders(Borders::ALL)
.border_style(Style::default().fg(theme.editor_fg))
.style(Style::default().bg(theme.editor_bg).fg(theme.editor_fg));
let inner_area = block.inner(dialog_area);
frame.render_widget(block, dialog_area);
let chunks = Layout::vertical([
Constraint::Length(5), Constraint::Min(8), Constraint::Length(4), ])
.split(inner_area);
match &wizard.step {
CalibrationStep::Capture { group_idx, key_idx } => {
render_capture_phase(frame, &chunks, wizard, *group_idx, *key_idx, theme);
}
CalibrationStep::Verify => {
render_verify_phase(frame, &chunks, wizard, theme);
}
}
}
fn render_confirmation_dialog(
frame: &mut Frame,
area: Rect,
wizard: &CalibrationWizard,
theme: &Theme,
) {
let (title, message, confirm_key, confirm_action, cancel_action) =
match wizard.pending_confirmation {
PendingConfirmation::Abort => (
t!("calibration.confirm_abort_title").to_string(),
t!("calibration.confirm_abort_message").to_string(),
"d",
t!("calibration.action_discard").to_string(),
t!("calibration.action_cancel").to_string(),
),
PendingConfirmation::Restart => (
t!("calibration.confirm_restart_title").to_string(),
t!("calibration.confirm_restart_message").to_string(),
"r",
t!("calibration.action_restart").to_string(),
t!("calibration.action_cancel").to_string(),
),
PendingConfirmation::None => return,
};
let block = Block::default()
.title(title)
.borders(Borders::ALL)
.border_style(Style::default().fg(theme.diagnostic_warning_fg))
.style(Style::default().bg(theme.editor_bg).fg(theme.editor_fg));
let inner_area = block.inner(area);
frame.render_widget(block, area);
let content = vec![
Line::from(""),
Line::from(vec![Span::styled(
message,
Style::default().add_modifier(Modifier::BOLD),
)]),
Line::from(""),
Line::from(""),
Line::from(vec![
Span::styled(
format!("[{}]", confirm_key),
Style::default().fg(theme.diagnostic_error_fg),
),
Span::raw(format!(" {} ", confirm_action)),
Span::styled("[c]", Style::default().fg(theme.help_key_fg)),
Span::raw(format!(" {}", cancel_action)),
]),
];
let para = Paragraph::new(content)
.style(Style::default().fg(theme.editor_fg))
.wrap(Wrap { trim: true });
frame.render_widget(para, inner_area);
}
fn render_capture_phase(
frame: &mut Frame,
chunks: &[Rect],
wizard: &CalibrationWizard,
group_idx: usize,
key_idx: usize,
theme: &Theme,
) {
let groups = wizard.groups();
let group = &groups[group_idx];
let target = &group.targets[key_idx];
let (step, total) = wizard.current_step_info();
let instructions = vec![
Line::from(vec![
Span::raw(format!("{}: ", t!("calibration.group"))),
Span::styled(group.name, Style::default().fg(theme.help_key_fg)),
]),
Line::from(""),
Line::from(vec![Span::styled(
t!("calibration.press_key").to_string(),
Style::default().add_modifier(Modifier::BOLD),
)]),
Line::from(vec![Span::styled(
format!(" {}", target.name),
Style::default()
.fg(theme.diagnostic_warning_fg)
.add_modifier(Modifier::BOLD),
)]),
];
let instructions_para = Paragraph::new(instructions)
.style(Style::default().fg(theme.editor_fg))
.wrap(Wrap { trim: true });
frame.render_widget(instructions_para, chunks[0]);
let mut progress_lines: Vec<Line> = Vec::new();
let flat_base = groups[..group_idx]
.iter()
.map(|g| g.targets.len())
.sum::<usize>();
for (idx, t) in group.targets.iter().enumerate() {
let flat_idx = flat_base + idx;
let status = wizard.key_status(flat_idx);
let (status_char, style) = match status {
KeyStatus::Pending => {
if idx == key_idx {
(
'>',
Style::default()
.fg(theme.diagnostic_warning_fg)
.add_modifier(Modifier::BOLD),
)
} else {
(' ', Style::default().fg(theme.line_number_fg))
}
}
KeyStatus::Captured => ('*', Style::default().fg(theme.diagnostic_info_fg)),
KeyStatus::Skipped => ('-', Style::default().fg(theme.line_number_fg)),
KeyStatus::Verified => ('v', Style::default().fg(theme.help_key_fg)),
};
progress_lines.push(Line::from(vec![
Span::styled(format!(" {} ", status_char), style),
Span::styled(t.name, style),
]));
}
progress_lines.push(Line::from(""));
progress_lines.push(Line::from(vec![Span::styled(
format!("{} {}/{}", t!("calibration.step"), step, total),
Style::default().fg(theme.line_number_fg),
)]));
let available_height = chunks[1].height.saturating_sub(2) as usize;
let scroll_offset = if available_height > 0 && key_idx >= available_height {
(key_idx - available_height + 1) as u16
} else {
0
};
let progress_para = Paragraph::new(progress_lines)
.style(Style::default().fg(theme.editor_fg))
.scroll((scroll_offset, 0));
frame.render_widget(progress_para, chunks[1]);
let controls = vec![
Line::from(vec![
Span::styled("[s]", Style::default().fg(theme.help_key_fg)),
Span::raw(format!(" {} ", t!("calibration.skip"))),
Span::styled("[b]", Style::default().fg(theme.help_key_fg)),
Span::raw(format!(" {} ", t!("calibration.back"))),
Span::styled("[g]", Style::default().fg(theme.help_key_fg)),
Span::raw(format!(" {} ", t!("calibration.skip_group"))),
Span::styled("[a]", Style::default().fg(theme.diagnostic_error_fg)),
Span::raw(format!(" {}", t!("calibration.abort"))),
]),
Line::from(""),
Line::from(wizard.status_message.as_deref().unwrap_or("")),
];
let controls_para = Paragraph::new(controls).style(Style::default().fg(theme.editor_fg));
frame.render_widget(controls_para, chunks[2]);
}
fn render_verify_phase(
frame: &mut Frame,
chunks: &[Rect],
wizard: &CalibrationWizard,
theme: &Theme,
) {
let translation_count = wizard.translation_count();
if translation_count == 0 {
render_all_keys_ok(frame, chunks, wizard, theme);
return;
}
let (verified, total) = wizard.verification_progress();
let instructions = vec![
Line::from(vec![Span::styled(
t!("calibration.verify_title").to_string(),
Style::default().add_modifier(Modifier::BOLD),
)]),
Line::from(""),
Line::from(t!("calibration.verify_instructions").to_string()),
Line::from(""),
Line::from(vec![
Span::raw(format!("{}: ", t!("calibration.translations"))),
Span::styled(
translation_count.to_string(),
Style::default().fg(theme.diagnostic_info_fg),
),
]),
];
let instructions_para = Paragraph::new(instructions)
.style(Style::default().fg(theme.editor_fg))
.wrap(Wrap { trim: true });
frame.render_widget(instructions_para, chunks[0]);
let mut status_lines: Vec<Line> = Vec::new();
status_lines.push(Line::from(vec![Span::raw(format!(
"{}: {}/{}",
t!("calibration.verified"),
verified,
total
))]));
status_lines.push(Line::from(""));
for (_group_idx, _, target, status) in wizard.all_key_info() {
if matches!(status, KeyStatus::Captured | KeyStatus::Verified) {
let (status_char, style) = match status {
KeyStatus::Verified => ('v', Style::default().fg(theme.diagnostic_info_fg)),
KeyStatus::Captured => (' ', Style::default().fg(theme.diagnostic_warning_fg)),
_ => continue,
};
status_lines.push(Line::from(vec![
Span::styled(format!("[{}] ", status_char), style),
Span::styled(target.name, style),
]));
}
}
let status_para = Paragraph::new(status_lines).style(Style::default().fg(theme.editor_fg));
frame.render_widget(status_para, chunks[1]);
let controls = vec![
Line::from(vec![
Span::styled("[y]", Style::default().fg(theme.diagnostic_info_fg)),
Span::raw(format!(" {} ", t!("calibration.save"))),
Span::styled("[b]", Style::default().fg(theme.help_key_fg)),
Span::raw(format!(" {} ", t!("calibration.back"))),
Span::styled("[r]", Style::default().fg(theme.diagnostic_warning_fg)),
Span::raw(format!(" {} ", t!("calibration.restart"))),
Span::styled("[a]", Style::default().fg(theme.diagnostic_error_fg)),
Span::raw(format!(" {}", t!("calibration.abort"))),
]),
Line::from(""),
Line::from(wizard.status_message.as_deref().unwrap_or("")),
];
let controls_para = Paragraph::new(controls).style(Style::default().fg(theme.editor_fg));
frame.render_widget(controls_para, chunks[2]);
}
fn render_all_keys_ok(
frame: &mut Frame,
chunks: &[Rect],
wizard: &CalibrationWizard,
theme: &Theme,
) {
let content = vec![
Line::from(""),
Line::from(vec![Span::styled(
t!("calibration.all_keys_ok_title").to_string(),
Style::default()
.fg(theme.diagnostic_info_fg)
.add_modifier(Modifier::BOLD),
)]),
Line::from(""),
Line::from(t!("calibration.all_keys_ok_message").to_string()),
];
let para = Paragraph::new(content)
.style(Style::default().fg(theme.editor_fg))
.wrap(Wrap { trim: true });
frame.render_widget(para, chunks[0]);
frame.render_widget(
Paragraph::new("").style(Style::default().fg(theme.editor_fg)),
chunks[1],
);
let controls = vec![
Line::from(vec![
Span::styled("[y]", Style::default().fg(theme.diagnostic_info_fg)),
Span::raw(format!(" {} ", t!("calibration.save"))),
Span::styled("[a]", Style::default().fg(theme.diagnostic_error_fg)),
Span::raw(format!(" {}", t!("calibration.abort"))),
]),
Line::from(""),
Line::from(wizard.status_message.as_deref().unwrap_or("")),
];
let controls_para = Paragraph::new(controls).style(Style::default().fg(theme.editor_fg));
frame.render_widget(controls_para, chunks[2]);
}