cuqueclicker_lib/ui/
toast.rs1use ratatui::{prelude::*, widgets::*};
12
13use crate::game::achievement::ACHIEVEMENTS;
14use crate::game::state::{GameState, TOAST_TICKS};
15use crate::i18n::t;
16
17pub fn draw(frame: &mut Frame, area: Rect, state: &GameState) {
19 let Some(id) = state.active_unlock_id.as_deref() else {
20 return;
21 };
22 if state.active_unlock_ticks == 0 {
23 return;
24 }
25 let Some(idx) = ACHIEVEMENTS.iter().position(|a| a.id == id) else {
28 return;
29 };
30 let lang = t();
31 let Some(name) = lang.achievement_names.get(idx).copied() else {
32 return;
33 };
34
35 let life = state.active_unlock_ticks as f32 / TOAST_TICKS as f32;
39 let strength = ease_in_out(life);
40 if strength <= 0.01 {
41 return;
42 }
43
44 let header_plain = header_plain();
45 let body_text = format!(" {name} ");
46 let inner_w = (header_plain.chars().count().max(body_text.chars().count()) + 2) as u16;
47 let w = (inner_w + 2).min(area.width.saturating_sub(2));
48 let h: u16 = 5;
49 if area.width < w + 2 || area.height < h + 2 {
50 return;
51 }
52
53 let x = area.x + (area.width.saturating_sub(w)) / 2;
56 let y = area.y + 2;
57 let rect = Rect {
58 x,
59 y,
60 width: w,
61 height: h,
62 };
63
64 let bg = Color::Rgb((30.0 * strength) as u8, (20.0 * strength) as u8, 0);
71 let bg_style = Style::default().bg(bg);
72 {
73 let buf = frame.buffer_mut();
74 for dy in 0..rect.height {
75 for dx in 0..rect.width {
76 let cx = rect.x + dx;
77 let cy = rect.y + dy;
78 if cx >= buf.area.x + buf.area.width || cy >= buf.area.y + buf.area.height {
79 continue;
80 }
81 let cell = &mut buf[(cx, cy)];
82 cell.set_char(' ');
83 cell.set_style(bg_style);
84 }
85 }
86 }
87
88 let border_fg = Color::Rgb(
92 (255.0 * strength) as u8,
93 (180.0 * strength) as u8,
94 (40.0 * strength) as u8,
95 );
96 let header_fg = border_fg;
97 let body_fg = Color::Rgb(
98 (255.0 * strength) as u8,
99 (230.0 * strength) as u8,
100 ((180.0 * strength) as u8).max((60.0 * strength) as u8),
101 );
102 let header_style = Style::default()
103 .fg(header_fg)
104 .bg(bg)
105 .add_modifier(Modifier::BOLD);
106 let body_style = Style::default()
107 .fg(body_fg)
108 .bg(bg)
109 .add_modifier(Modifier::BOLD);
110 let block = Block::bordered()
111 .border_style(Style::default().fg(border_fg).bg(bg))
112 .style(bg_style);
113 let lines = vec![
114 Line::from(Span::styled(" ".repeat(w as usize), bg_style)),
117 Line::from(Span::styled(header_plain.to_string(), header_style)),
118 Line::from(Span::styled(body_text, body_style)),
119 Line::from(Span::styled(" ".repeat(w as usize), bg_style)),
120 ];
121 let p = Paragraph::new(lines)
122 .alignment(Alignment::Center)
123 .block(block);
124 frame.render_widget(p, rect);
125}
126
127fn header_plain() -> &'static str {
128 " *** ACHIEVEMENT UNLOCKED *** "
129}
130
131fn smoothstep(t: f32) -> f32 {
132 let t = t.clamp(0.0, 1.0);
133 t * t * (3.0 - 2.0 * t)
134}
135
136fn ease_in_out(life: f32) -> f32 {
143 let life = life.clamp(0.0, 1.0);
144 let entry = smoothstep((1.0 - life) / 0.15);
147 let exit = smoothstep(life / 0.25);
148 entry.min(exit)
149}