|
| 1 | +use ratatui::{ |
| 2 | + prelude::*, |
| 3 | + widgets::{Block, Borders, Paragraph, Clear}, |
| 4 | +}; |
| 5 | +use chrono::Timelike; |
| 6 | + |
| 7 | +use crate::watch::{Watch, WatchMode}; |
| 8 | + |
| 9 | +pub fn ui(f: &mut Frame, watch: &Watch) { |
| 10 | + let size = f.area(); |
| 11 | + let watch_size = 20; |
| 12 | + |
| 13 | + let watch_area = Rect { |
| 14 | + x: (size.width.saturating_sub(watch_size)) / 2, |
| 15 | + y: (size.height.saturating_sub(watch_size)) / 2, |
| 16 | + width: watch_size.min(size.width), |
| 17 | + height: watch_size.min(size.height), |
| 18 | + }; |
| 19 | + |
| 20 | + // draw the frame |
| 21 | + let watch_block = Block::default() |
| 22 | + .title("Casio AE-1200") |
| 23 | + .borders(Borders::ALL) |
| 24 | + .border_style(Style::default().fg(Color::Blue)); |
| 25 | + |
| 26 | + f.render_widget(Clear, watch_area); |
| 27 | + let watch_inner = watch_block.inner(watch_area); |
| 28 | + f.render_widget(watch_block, watch_area); |
| 29 | + |
| 30 | + match watch.mode { |
| 31 | + WatchMode::Home => { |
| 32 | + render_time_display(f, watch_inner, watch); |
| 33 | + } |
| 34 | + WatchMode::WorldTime => { |
| 35 | + render_world_time_display(f, watch_inner, watch); |
| 36 | + } |
| 37 | + WatchMode::Alarm => { |
| 38 | + render_alarm_display(f, watch_inner, watch); |
| 39 | + } |
| 40 | + WatchMode::Timer => { |
| 41 | + render_timer_display(f, watch_inner, watch); |
| 42 | + } |
| 43 | + WatchMode::Stopwatch => { |
| 44 | + render_stopwatch_display(f, watch_inner, watch); |
| 45 | + } |
| 46 | + } |
| 47 | + |
| 48 | + render_status_indicators(f, watch_area, watch); |
| 49 | +} |
| 50 | + |
| 51 | +fn render_time_display(f: &mut Frame, area: Rect, watch: &Watch) { |
| 52 | + let time_text = watch.time_manager.format_time(watch.settings.time_format_24h); |
| 53 | + let date_text = watch.time_manager.format_date(watch.settings.date_format_us); |
| 54 | + let day_text = watch.time_manager.format_day_of_week(); |
| 55 | + |
| 56 | + let time_display = Paragraph::new(vec![ |
| 57 | + Line::from(""), |
| 58 | + Line::from(vec![ |
| 59 | + Span::styled( |
| 60 | + time_text, |
| 61 | + Style::default().fg(Color::Green).add_modifier(Modifier::BOLD), |
| 62 | + ) |
| 63 | + ]).alignment(Alignment::Center), |
| 64 | + Line::from(""), |
| 65 | + Line::from(vec![ |
| 66 | + Span::styled( |
| 67 | + format!("{} {}", day_text, date_text), |
| 68 | + Style::default().fg(Color::Cyan), |
| 69 | + ) |
| 70 | + ]).alignment(Alignment::Center), |
| 71 | + Line::from(""), |
| 72 | + Line::from(""), |
| 73 | + Line::from("press 'M' for mode, 'L' for backlight"), |
| 74 | + ]) |
| 75 | + .block(Block::default()); |
| 76 | + |
| 77 | + f.render_widget(time_display, area); |
| 78 | + |
| 79 | + render_analog_display(f, area, watch); |
| 80 | +} |
| 81 | + |
| 82 | +fn render_analog_display(f: &mut Frame, area: Rect, watch: &Watch) { |
| 83 | + use std::f64::consts::PI; |
| 84 | + |
| 85 | + // create a small area for the analog clock in the top-left corner |
| 86 | + let analog_area = Rect { |
| 87 | + x: area.x + 2, |
| 88 | + y: area.y + 1, |
| 89 | + width: 10.min(area.width), |
| 90 | + height: 5.min(area.height), |
| 91 | + }; |
| 92 | + |
| 93 | + let time = &watch.time_manager.current_time; |
| 94 | + let hour = time.hour() as f64; |
| 95 | + let minute = time.minute() as f64; |
| 96 | + let second = time.second() as f64; |
| 97 | + |
| 98 | + // hour hand: 360 degrees / 12 hours = 30 degrees per hour + minute adjustment |
| 99 | + let hour_angle = (hour % 12.0) * 30.0 + minute * 0.5; |
| 100 | + let _hour_angle_rad = hour_angle * PI / 180.0; |
| 101 | + |
| 102 | + // minute hand: 360 degrees / 60 minutes = 6 degrees per minute + second adjustment |
| 103 | + let minute_angle = minute * 6.0 + second * 0.1; |
| 104 | + let _minute_angle_rad = minute_angle * PI / 180.0; |
| 105 | + |
| 106 | + // second hand: 360 degrees / 60 seconds = 6 degrees per second |
| 107 | + let second_angle = second * 6.0; |
| 108 | + let _second_angle_rad = second_angle * PI / 180.0; |
| 109 | + |
| 110 | + let analog_display = Paragraph::new(vec![ |
| 111 | + Line::from(format!("┌────────┐")), |
| 112 | + Line::from(format!("│ {}{} │", |
| 113 | + if hour_angle >= 270.0 || hour_angle <= 90.0 { "▲" } else { " " }, |
| 114 | + if minute_angle >= 270.0 || minute_angle <= 90.0 { "●" } else { " " } |
| 115 | + )), |
| 116 | + Line::from(format!("│{}{} {}{}{}│", |
| 117 | + if hour_angle > 180.0 && hour_angle <= 360.0 { "◀" } else { " " }, |
| 118 | + if minute_angle > 180.0 && minute_angle <= 360.0 { "●" } else { " " }, |
| 119 | + if second_angle > 180.0 && second_angle <= 360.0 { "·" } else { " " }, |
| 120 | + if minute_angle > 0.0 && minute_angle <= 180.0 { "●" } else { " " }, |
| 121 | + if hour_angle > 0.0 && hour_angle <= 180.0 { "▶" } else { " " } |
| 122 | + )), |
| 123 | + Line::from(format!("│ {}{} │", |
| 124 | + if minute_angle > 90.0 && minute_angle <= 270.0 { "●" } else { " " }, |
| 125 | + if hour_angle > 90.0 && hour_angle <= 270.0 { "▼" } else { " " } |
| 126 | + )), |
| 127 | + Line::from(format!("└────────┘")), |
| 128 | + ]) |
| 129 | + .style(Style::default().fg(Color::Yellow)); |
| 130 | + |
| 131 | + f.render_widget(analog_display, analog_area); |
| 132 | +} |
| 133 | + |
| 134 | +fn render_date_display(f: &mut Frame, area: Rect, watch: &Watch) { |
| 135 | + let date_text = watch.time_manager.format_date(watch.settings.date_format_us); |
| 136 | + let day_text = watch.time_manager.format_day_of_week(); |
| 137 | + let year_text = watch.time_manager.current_time.format("%Y").to_string(); |
| 138 | + |
| 139 | + let date_display = Paragraph::new(vec![ |
| 140 | + Line::from(""), |
| 141 | + Line::from(""), |
| 142 | + Line::from(""), |
| 143 | + Line::from(vec![ |
| 144 | + Span::styled( |
| 145 | + format!("{} {}", day_text, date_text), |
| 146 | + Style::default().fg(Color::Cyan).add_modifier(Modifier::BOLD), |
| 147 | + ) |
| 148 | + ]).alignment(Alignment::Center), |
| 149 | + Line::from(""), |
| 150 | + Line::from(vec![ |
| 151 | + Span::styled( |
| 152 | + year_text, |
| 153 | + Style::default().fg(Color::Cyan), |
| 154 | + ) |
| 155 | + ]).alignment(Alignment::Center), |
| 156 | + Line::from(""), |
| 157 | + Line::from(""), |
| 158 | + Line::from(""), |
| 159 | + Line::from("Press 'M' for mode, 'L' for light"), |
| 160 | + ]) |
| 161 | + .block(Block::default()); |
| 162 | + |
| 163 | + f.render_widget(date_display, area); |
| 164 | +} |
| 165 | + |
| 166 | +fn render_stopwatch_display(f: &mut Frame, area: Rect, watch: &Watch) { |
| 167 | + let time_text = format_stopwatch_time(watch.stopwatch_time); |
| 168 | + let status = if watch.stopwatch_running { "RUNNING" } else { "STOPPED" }; |
| 169 | + |
| 170 | + let stopwatch_display = Paragraph::new(vec![ |
| 171 | + Line::from(""), |
| 172 | + Line::from(""), |
| 173 | + Line::from(vec![ |
| 174 | + Span::styled( |
| 175 | + "STOPWATCH", |
| 176 | + Style::default().fg(Color::Yellow).add_modifier(Modifier::BOLD), |
| 177 | + ) |
| 178 | + ]).alignment(Alignment::Center), |
| 179 | + Line::from(""), |
| 180 | + Line::from(vec![ |
| 181 | + Span::styled( |
| 182 | + time_text, |
| 183 | + Style::default().fg(Color::Green).add_modifier(Modifier::BOLD), |
| 184 | + ) |
| 185 | + ]).alignment(Alignment::Center), |
| 186 | + Line::from(""), |
| 187 | + Line::from(vec![ |
| 188 | + Span::styled( |
| 189 | + status, |
| 190 | + if watch.stopwatch_running { |
| 191 | + Style::default().fg(Color::Red) |
| 192 | + } else { |
| 193 | + Style::default().fg(Color::Blue) |
| 194 | + }, |
| 195 | + ) |
| 196 | + ]).alignment(Alignment::Center), |
| 197 | + Line::from(""), |
| 198 | + Line::from(""), |
| 199 | + Line::from("press 'S' start/stop, 'R' reset"), |
| 200 | + Line::from("press 'M' for mode, 'L' for backlight"), |
| 201 | + ]) |
| 202 | + .block(Block::default()); |
| 203 | + |
| 204 | + f.render_widget(stopwatch_display, area); |
| 205 | +} |
| 206 | + |
| 207 | +fn format_stopwatch_time(milliseconds: u64) -> String { |
| 208 | + let total_seconds = milliseconds / 1000; |
| 209 | + let minutes = total_seconds / 60; |
| 210 | + let seconds = total_seconds % 60; |
| 211 | + let millis = (milliseconds % 1000) / 10; |
| 212 | + |
| 213 | + format!("{:02}:{:02}.{:02}", minutes, seconds, millis) |
| 214 | +} |
| 215 | + |
| 216 | +fn render_status_indicators(f: &mut Frame, area: Rect, watch: &Watch) { |
| 217 | + if watch.light_on { |
| 218 | + let light_indicator = Paragraph::new("LGT") |
| 219 | + .style(Style::default().fg(Color::Yellow).add_modifier(Modifier::BOLD)) |
| 220 | + .alignment(Alignment::Right); |
| 221 | + |
| 222 | + let light_area = Rect { |
| 223 | + x: area.x + area.width.saturating_sub(8), |
| 224 | + y: area.y + 1, |
| 225 | + width: 7.min(area.width), |
| 226 | + height: 1, |
| 227 | + }; |
| 228 | + |
| 229 | + f.render_widget(light_indicator, light_area); |
| 230 | + } |
| 231 | + |
| 232 | + if watch.settings.alarm_enabled { |
| 233 | + let alarm_indicator = Paragraph::new("ALM") |
| 234 | + .style(Style::default().fg(Color::Red).add_modifier(Modifier::BOLD)) |
| 235 | + .alignment(Alignment::Left); |
| 236 | + |
| 237 | + let alarm_area = Rect { |
| 238 | + x: area.x + 1, |
| 239 | + y: area.y + 1, |
| 240 | + width: 8.min(area.width), |
| 241 | + height: 1, |
| 242 | + }; |
| 243 | + |
| 244 | + f.render_widget(alarm_indicator, alarm_area); |
| 245 | + } |
| 246 | +} |
| 247 | + |
| 248 | +fn render_world_time_display(f: &mut Frame, area: Rect, watch: &Watch) { |
| 249 | + let time_text = watch.time_manager.format_time(watch.settings.time_format_24h); |
| 250 | + |
| 251 | + let world_time_display = Paragraph::new(vec![ |
| 252 | + Line::from(""), |
| 253 | + Line::from(""), |
| 254 | + Line::from(vec![ |
| 255 | + Span::styled( |
| 256 | + "WT", |
| 257 | + Style::default().fg(Color::Yellow).add_modifier(Modifier::BOLD), |
| 258 | + ) |
| 259 | + ]).alignment(Alignment::Center), |
| 260 | + Line::from(""), |
| 261 | + Line::from(vec![ |
| 262 | + Span::styled( |
| 263 | + time_text, |
| 264 | + Style::default().fg(Color::Green).add_modifier(Modifier::BOLD), |
| 265 | + ) |
| 266 | + ]).alignment(Alignment::Center), |
| 267 | + Line::from(""), |
| 268 | + Line::from(""), |
| 269 | + Line::from("Press 'M' for mode, 'L' for backlight"), |
| 270 | + ]) |
| 271 | + .block(Block::default()); |
| 272 | + |
| 273 | + f.render_widget(world_time_display, area); |
| 274 | +} |
| 275 | + |
| 276 | +fn render_alarm_display(f: &mut Frame, area: Rect, watch: &Watch) { |
| 277 | + let alarm_status = if watch.settings.alarm_enabled { |
| 278 | + watch.settings.alarm_time.clone().unwrap_or_else(|| "Not set".to_string()) |
| 279 | + } else { |
| 280 | + "Disabled".to_string() |
| 281 | + }; |
| 282 | + |
| 283 | + let alarm_display = Paragraph::new(vec![ |
| 284 | + Line::from(""), |
| 285 | + Line::from(""), |
| 286 | + Line::from(vec![ |
| 287 | + Span::styled( |
| 288 | + "ALM", |
| 289 | + Style::default().fg(Color::Red).add_modifier(Modifier::BOLD), |
| 290 | + ) |
| 291 | + ]).alignment(Alignment::Center), |
| 292 | + Line::from(""), |
| 293 | + Line::from(vec![ |
| 294 | + Span::styled( |
| 295 | + alarm_status, |
| 296 | + Style::default().fg(Color::Cyan), |
| 297 | + ) |
| 298 | + ]).alignment(Alignment::Center), |
| 299 | + Line::from(""), |
| 300 | + Line::from(""), |
| 301 | + Line::from("Press 'A' to toggle, 'M' for mode"), |
| 302 | + ]) |
| 303 | + .block(Block::default()); |
| 304 | + |
| 305 | + f.render_widget(alarm_display, area); |
| 306 | +} |
| 307 | + |
| 308 | +fn render_timer_display(f: &mut Frame, area: Rect, watch: &Watch) { |
| 309 | + let time_text = format_timer_time(watch.timer_time); |
| 310 | + let status = if watch.timer_running { "RUNNING" } else { "STOPPED" }; |
| 311 | + |
| 312 | + let timer_display = Paragraph::new(vec![ |
| 313 | + Line::from(""), |
| 314 | + Line::from(""), |
| 315 | + Line::from(vec![ |
| 316 | + Span::styled( |
| 317 | + "TMR", |
| 318 | + Style::default().fg(Color::Magenta).add_modifier(Modifier::BOLD), |
| 319 | + ) |
| 320 | + ]).alignment(Alignment::Center), |
| 321 | + Line::from(""), |
| 322 | + Line::from(vec![ |
| 323 | + Span::styled( |
| 324 | + time_text, |
| 325 | + Style::default().fg(Color::Green).add_modifier(Modifier::BOLD), |
| 326 | + ) |
| 327 | + ]).alignment(Alignment::Center), |
| 328 | + Line::from(""), |
| 329 | + Line::from(vec![ |
| 330 | + Span::styled( |
| 331 | + status, |
| 332 | + if watch.timer_running { |
| 333 | + Style::default().fg(Color::Red) |
| 334 | + } else { |
| 335 | + Style::default().fg(Color::Blue) |
| 336 | + }, |
| 337 | + ) |
| 338 | + ]).alignment(Alignment::Center), |
| 339 | + Line::from(""), |
| 340 | + Line::from(""), |
| 341 | + Line::from("Press 'S' start/stop, 'R' reset"), |
| 342 | + Line::from("Press 'M' for mode, 'L' for backlight"), |
| 343 | + ]) |
| 344 | + .block(Block::default()); |
| 345 | + |
| 346 | + f.render_widget(timer_display, area); |
| 347 | +} |
| 348 | + |
| 349 | +fn format_timer_time(milliseconds: u64) -> String { |
| 350 | + let total_seconds = milliseconds / 1000; |
| 351 | + let minutes = total_seconds / 60; |
| 352 | + let seconds = total_seconds % 60; |
| 353 | + let millis = (milliseconds % 1000) / 10; |
| 354 | + |
| 355 | + format!("{:02}:{:02}.{:02}", minutes, seconds, millis) |
| 356 | +} |
0 commit comments