use ratatui::{
layout::{Alignment, Constraint, Direction, Layout, Rect},
style::{Color, Modifier, Style},
text::{Line, Span},
widgets::{Block, Borders, Paragraph, Wrap},
Frame,
};
use crate::app::App;
use crate::dashboard::utils::themed_rgb;
pub fn get_changelog() -> Vec<Line<'static>> {
const CHANGELOG: &str = include_str!("../../APP_CHANGELOG.md");
let mut lines = Vec::new();
for line in CHANGELOG.lines() {
let trimmed = line.trim();
if trimmed.starts_with("# ") {
continue;
}
if trimmed.starts_with("## ") {
let version = trimmed.trim_start_matches("## ");
lines.push(Line::from(vec![
Span::styled(
version.to_string(),
Style::default()
.fg(Color::Cyan)
.add_modifier(Modifier::BOLD),
),
]));
}
else if trimmed.starts_with("- ") {
let text = trimmed.trim_start_matches("- ");
lines.push(Line::from(vec![
Span::styled("• ", Style::default().fg(Color::Green)),
Span::raw(text.to_string()),
]));
}
else if trimmed.is_empty() {
lines.push(Line::from(""));
}
}
lines
}
pub fn draw_whats_new(f: &mut Frame, app: &App) {
let area = centered_rect(80, 80, f.area());
let block = Block::default()
.borders(Borders::ALL)
.border_style(Style::default().fg(Color::Cyan))
.title(vec![
Span::raw(" "),
Span::styled(
format!("trackWork v{} - What's New", env!("CARGO_PKG_VERSION")),
Style::default()
.fg(Color::Cyan)
.add_modifier(Modifier::BOLD),
),
Span::raw(" "),
]);
let chunks = Layout::default()
.direction(Direction::Vertical)
.constraints([
Constraint::Min(3), Constraint::Length(3), ])
.split(area);
f.render_widget(block, area);
if !app.config.hide_eye_candy {
shimmer_frame(f, area, app);
}
let content_area = Rect {
x: chunks[0].x + 2,
y: chunks[0].y + 1,
width: chunks[0].width.saturating_sub(4),
height: chunks[0].height.saturating_sub(2),
};
let changelog = get_changelog();
let content = Paragraph::new(changelog)
.wrap(Wrap { trim: false })
.alignment(Alignment::Left);
f.render_widget(content, content_area);
let footer_area = Rect {
x: chunks[1].x + 2,
y: chunks[1].y,
width: chunks[1].width.saturating_sub(4),
height: chunks[1].height,
};
let help_text = vec![Line::from(vec![
Span::styled("Press ", Style::default().fg(Color::Gray)),
Span::styled("Enter", Style::default().fg(Color::Green).add_modifier(Modifier::BOLD)),
Span::styled(" or ", Style::default().fg(Color::Gray)),
Span::styled("Esc", Style::default().fg(Color::Green).add_modifier(Modifier::BOLD)),
Span::styled(" to close", Style::default().fg(Color::Gray)),
])];
let footer = Paragraph::new(help_text).alignment(Alignment::Center);
f.render_widget(footer, footer_area);
}
fn shimmer_frame(f: &mut Frame, area: Rect, app: &App) {
const SWEEP_MS: f64 = 1100.0; const PAUSE_MS: f64 = 4000.0; const BAND: f64 = 70.0;
if area.width < 2 || area.height < 2 {
return;
}
let base = themed_rgb(Color::Cyan, &app.term_palette);
let ms = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap_or_default()
.as_millis() as f64;
let span = (area.width + area.height) as f64;
let t = ms % (SWEEP_MS + PAUSE_MS);
let head = if t < SWEEP_MS {
-BAND + (t / SWEEP_MS) * (span + 2.0 * BAND)
} else {
f64::INFINITY
};
let buf = f.buffer_mut();
let (x0, y0) = (area.x, area.y);
let (x1, y1) = (area.x + area.width - 1, area.y + area.height - 1);
let mut paint = |x: u16, y: u16| {
let diag = (x - x0) as f64 + (y - y0) as f64;
let d = (head - diag).abs();
if d >= BAND {
return;
}
let intensity = 0.5 * (1.0 + (std::f64::consts::PI * d / BAND).cos());
let lighten = |c: u8| (c as f64 + (255.0 - c as f64) * (intensity * 0.85)).round() as u8;
let cell = &mut buf[(x, y)];
cell.set_fg(Color::Rgb(lighten(base.0), lighten(base.1), lighten(base.2)));
};
for x in x0..=x1 {
paint(x, y0);
paint(x, y1);
}
for y in y0..=y1 {
paint(x0, y);
paint(x1, y);
}
}
fn centered_rect(percent_x: u16, percent_y: u16, r: Rect) -> Rect {
let popup_layout = Layout::default()
.direction(Direction::Vertical)
.constraints([
Constraint::Percentage((100 - percent_y) / 2),
Constraint::Percentage(percent_y),
Constraint::Percentage((100 - percent_y) / 2),
])
.split(r);
Layout::default()
.direction(Direction::Horizontal)
.constraints([
Constraint::Percentage((100 - percent_x) / 2),
Constraint::Percentage(percent_x),
Constraint::Percentage((100 - percent_x) / 2),
])
.split(popup_layout[1])[1]
}