app.rs (25458B)
1 use std::ops::Range; 2 use std::path::PathBuf; 3 use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH}; 4 5 use eframe::egui; 6 use egui::{Color32, FontId, Key, Modifiers}; 7 use perilus_core::perilus::{Perilus, Register}; 8 9 use crate::state::{Focus, RunState, ViewMode, row_digits}; 10 11 pub(crate) const FONT_SIZE: f32 = 16.0; 12 pub(crate) const ROW_H: f32 = 22.0; 13 pub(crate) const GROUP_GAP: f32 = 10.0; 14 pub(crate) const ADDR_W: f32 = 76.0; 15 pub(crate) const DIVIDER_GAP: f32 = 9.0; 16 pub(crate) const REG_LABEL_W: f32 = 58.0; 17 pub(crate) const REG_COL_GAP: f32 = 20.0; 18 pub(crate) const BYTES_PER_ROW: usize = 32; 19 pub(crate) const WORDS_PER_ROW: usize = 8; 20 pub(crate) const REG_COLS: usize = 4; 21 pub(crate) const REG_ROWS: usize = 8; 22 pub(crate) const NUM_CELLS: usize = REG_COLS * REG_ROWS; 23 pub(crate) const PC_COLOR: Color32 = Color32::from_rgb(40, 200, 40); 24 25 const MAX_RUN_CYCLES: u64 = 50_000_000; 26 const FREE_RUN_BUDGET: Duration = Duration::from_millis(8); 27 const CTRL_SHIFT: Modifiers = Modifiers { 28 ctrl: true, 29 shift: true, 30 alt: false, 31 mac_cmd: false, 32 command: false, 33 }; 34 35 // Hex keys for the nibble editor. 36 const HEX_KEYS: [(Key, u8); 16] = [ 37 (Key::Num0, 0), 38 (Key::Num1, 1), 39 (Key::Num2, 2), 40 (Key::Num3, 3), 41 (Key::Num4, 4), 42 (Key::Num5, 5), 43 (Key::Num6, 6), 44 (Key::Num7, 7), 45 (Key::Num8, 8), 46 (Key::Num9, 9), 47 (Key::A, 10), 48 (Key::B, 11), 49 (Key::C, 12), 50 (Key::D, 13), 51 (Key::E, 14), 52 (Key::F, 15), 53 ]; 54 55 #[derive(Clone, Copy)] 56 pub(crate) enum Direction { 57 Left, 58 Right, 59 Up, 60 Down, 61 } 62 63 #[derive(Default)] 64 struct Actions { 65 step: bool, 66 next_instruction: bool, 67 reset: bool, 68 free_run: bool, 69 run_to_cursor: bool, 70 stop: bool, 71 focus_registers: bool, 72 focus_memory: bool, 73 focus_address: bool, 74 follow: bool, 75 names: bool, 76 view: bool, 77 set_pc_cursor: bool, 78 clear: bool, 79 group_left: bool, 80 group_right: bool, 81 page_up: bool, 82 page_down: bool, 83 left: bool, 84 right: bool, 85 up: bool, 86 down: bool, 87 backspace: bool, 88 trace: bool, 89 nibble: Option<u8>, 90 } 91 92 pub(crate) struct SimApp { 93 pub(crate) perilus: Perilus, 94 pub(crate) current_pc: u32, 95 pub(crate) run_state: RunState, 96 pub(crate) view: ViewMode, 97 pub(crate) show_aliases: bool, 98 pub(crate) focus: Focus, 99 /// Memory cursor: byte address and nibble (0 = high, 1 = low). 100 pub(crate) mem_addr: u32, 101 pub(crate) mem_nib: u8, 102 /// Register-grid cursor: cell index (0 = PC, 1..=31 = x1..x31) and nibble. 103 pub(crate) reg_index: u8, 104 pub(crate) reg_nib: u8, 105 pub(crate) address_input: String, 106 pub(crate) follow_pc: bool, 107 pub(crate) follow_pc_last: u32, 108 pub(crate) focus_address: bool, 109 pub(crate) scroll_to_row: Option<usize>, 110 pub(crate) mem_viewport: Range<usize>, 111 pub(crate) status: String, 112 pub(crate) tracing: bool, 113 pub(crate) trace_file: PathBuf, 114 run_cycles: u64, 115 } 116 117 impl SimApp { 118 pub(crate) fn new() -> SimApp { 119 let perilus = Perilus::init(); 120 let current_pc = perilus.get_pc(); 121 SimApp { 122 perilus, 123 current_pc, 124 run_state: RunState::Idle, 125 view: ViewMode::Word, 126 show_aliases: true, 127 focus: Focus::Registers, 128 mem_addr: 3, 129 mem_nib: 0, 130 reg_index: 0, 131 reg_nib: 0, 132 address_input: String::new(), 133 follow_pc: false, 134 follow_pc_last: current_pc, 135 focus_address: false, 136 scroll_to_row: None, 137 mem_viewport: 0..0, 138 status: "Ready".into(), 139 tracing: false, 140 trace_file: PathBuf::new(), 141 run_cycles: 0, 142 } 143 } 144 145 /// Captures the model PC once the control unit is back at an instruction boundary. 146 pub(crate) fn sync_pc(&mut self) { 147 if self.perilus.get_control_unit_state() == 0 { 148 self.current_pc = self.perilus.get_pc(); 149 } 150 } 151 152 pub(crate) fn reset(&mut self) { 153 self.perilus.reset(); 154 self.run_state = RunState::Idle; 155 self.sync_pc(); 156 self.status = "Reset".into(); 157 } 158 159 /// Pulse the clock exactly once. 160 pub(crate) fn step_cycle(&mut self) { 161 self.perilus.pulse_clock(); 162 self.sync_pc(); 163 self.status = "Stepped one cycle".into(); 164 } 165 166 /// Run until the control unit returns to an instruction boundary. 167 pub(crate) fn next_instruction(&mut self) { 168 const MAX_STATES_PER_INSTRUCTION: u32 = 64; 169 self.perilus.pulse_clock(); 170 let mut states = 1; 171 while self.perilus.get_control_unit_state() != 0 && states < MAX_STATES_PER_INSTRUCTION { 172 self.perilus.pulse_clock(); 173 states += 1; 174 } 175 self.sync_pc(); 176 if self.perilus.get_control_unit_state() == 0 { 177 self.status = "Stepped one instruction".into(); 178 } else { 179 self.status = "Stopped: control unit did not return to fetch".into(); 180 } 181 } 182 183 pub(crate) fn run_to_pc(&mut self, target: u32) { 184 self.run_cycles = 0; 185 self.run_state = RunState::RunToPc(target); 186 } 187 188 pub(crate) fn run_to_input_pc(&mut self) { 189 match parse_hex(&self.address_input) { 190 Some(target) => self.run_to_pc(target), 191 None => self.status = format!("Invalid PC target {:?}", self.address_input), 192 } 193 } 194 195 pub(crate) fn toggle_free_run(&mut self) { 196 self.run_state = match self.run_state { 197 RunState::FreeRun => RunState::Idle, 198 _ => RunState::FreeRun, 199 }; 200 } 201 202 pub(crate) fn toggle_follow(&mut self) { 203 self.follow_pc = !self.follow_pc; 204 self.follow_pc_last = self.current_pc; 205 self.status = format!("Follow PC {}", if self.follow_pc { "on" } else { "off" }); 206 } 207 208 pub(crate) fn toggle_trace(&mut self) { 209 if self.tracing { 210 self.perilus.stop_trace(); 211 self.status = format!("Closed trace file {}", self.trace_file.display()); 212 self.tracing = false; 213 } else { 214 let timestamp = SystemTime::now() 215 .duration_since(UNIX_EPOCH) 216 .unwrap() 217 .as_secs(); 218 let iso = chrono_lite(timestamp); 219 let trace_file = std::env::temp_dir().join(format!("perilus-{iso}.vcd")); 220 match self.perilus.start_trace(trace_file.clone()) { 221 Ok(()) => { 222 self.status = format!("Opened trace file {}", trace_file.display()); 223 self.tracing = true; 224 self.trace_file = trace_file; 225 } 226 Err(e) => { 227 self.status = format!("Trace start failed: {e}"); 228 } 229 } 230 } 231 } 232 233 pub(crate) fn stop(&mut self) { 234 if self.run_state != RunState::Idle { 235 self.status = "Stopped".into(); 236 } 237 self.run_state = RunState::Idle; 238 } 239 240 pub(crate) fn save_memory(&mut self) { 241 if let Some(path) = rfd::FileDialog::new() 242 .set_file_name("memory.bin") 243 .save_file() 244 { 245 self.status = match self.perilus.save_file(path.clone()) { 246 Ok(()) => format!("Saved memory to {}", path.display()), 247 Err(e) => format!("Save failed: {e}"), 248 }; 249 } 250 } 251 252 pub(crate) fn load_memory(&mut self) { 253 if let Some(path) = rfd::FileDialog::new().pick_file() { 254 self.load_path(path); 255 } 256 } 257 258 pub(crate) fn load_path(&mut self, path: std::path::PathBuf) { 259 self.status = match self.perilus.load_file(path.clone()) { 260 Ok(()) => format!("Loaded memory from {}", path.display()), 261 Err(e) => format!("Load failed: {e}"), 262 }; 263 self.sync_pc(); 264 } 265 266 pub(crate) fn jump_to_address(&mut self) { 267 match parse_hex(&self.address_input) { 268 Some(addr) => { 269 let addr = addr & (Perilus::MEMORY_SIZE_BYTES as u32 - 1); 270 self.mem_addr = addr; 271 self.mem_nib = 0; 272 self.scroll_to_row = Some(addr as usize / BYTES_PER_ROW); 273 self.focus = Focus::Memory; 274 self.status = format!("Selection moved to 0x{addr:08x}"); 275 } 276 None => self.status = format!("Invalid address {:?}", self.address_input), 277 } 278 } 279 280 /// Word containing the memory cursor. 281 pub(crate) fn cursor_word(&self) -> u32 { 282 self.mem_addr & !3 283 } 284 285 pub(crate) fn set_pc_to_cursor(&mut self) { 286 let pc = self.cursor_word(); 287 self.perilus.set_pc(pc); 288 self.current_pc = pc; 289 self.status = format!("Set PC to 0x{pc:08x}"); 290 } 291 292 pub(crate) fn run_to_cursor(&mut self) { 293 self.run_to_pc(self.cursor_word()); 294 } 295 296 fn move_cursor(&mut self, direction: Direction) { 297 match self.focus { 298 Focus::Memory => self.move_memory(direction), 299 Focus::Registers => self.move_register(direction), 300 } 301 } 302 303 fn move_memory(&mut self, direction: Direction) { 304 let order = row_digits(self.view); 305 let row = (self.mem_addr / BYTES_PER_ROW as u32) as usize; 306 let offset = self.mem_addr % BYTES_PER_ROW as u32; 307 let col = order 308 .iter() 309 .position(|&(o, n)| o == offset && n == self.mem_nib) 310 .unwrap_or(0); 311 let total_rows = Perilus::MEMORY_SIZE_WORDS / WORDS_PER_ROW; 312 let (row, col) = match direction { 313 Direction::Left if col == 0 && row > 0 => (row - 1, 63), 314 Direction::Left if col > 0 => (row, col - 1), 315 Direction::Right if col == 63 && row + 1 < total_rows => (row + 1, 0), 316 Direction::Right if col < 63 => (row, col + 1), 317 Direction::Up if row > 0 => (row - 1, col), 318 Direction::Down if row + 1 < total_rows => (row + 1, col), 319 _ => (row, col), 320 }; 321 self.mem_addr = row as u32 * BYTES_PER_ROW as u32 + order[col].0; 322 self.mem_nib = order[col].1; 323 } 324 325 /// Move to the first digit of the previous/next word (word view) or byte 326 /// (byte view). 327 fn move_memory_group(&mut self, forward: bool) { 328 let order = row_digits(self.view); 329 let row = (self.mem_addr / BYTES_PER_ROW as u32) as usize; 330 let offset = self.mem_addr % BYTES_PER_ROW as u32; 331 let col = order 332 .iter() 333 .position(|&(o, n)| o == offset && n == self.mem_nib) 334 .unwrap_or(0); 335 let group = match self.view { 336 ViewMode::Word => 8, 337 ViewMode::Byte => 2, 338 }; 339 let groups_per_row = 64 / group; 340 let index = col / group; 341 let total_rows = Perilus::MEMORY_SIZE_WORDS / WORDS_PER_ROW; 342 let (row, col) = if forward { 343 if index + 1 < groups_per_row { 344 (row, (index + 1) * group) 345 } else if row + 1 < total_rows { 346 (row + 1, 0) 347 } else { 348 (row, col) 349 } 350 } else if index > 0 { 351 (row, (index - 1) * group) 352 } else if row > 0 { 353 (row - 1, (groups_per_row - 1) * group) 354 } else { 355 (row, col) 356 }; 357 self.mem_addr = row as u32 * BYTES_PER_ROW as u32 + order[col].0; 358 self.mem_nib = order[col].1; 359 } 360 361 fn move_register(&mut self, direction: Direction) { 362 match direction { 363 Direction::Left if self.reg_nib == 0 && self.reg_index > 0 => { 364 self.reg_index -= 1; 365 self.reg_nib = 7; 366 } 367 Direction::Left if self.reg_nib > 0 => self.reg_nib -= 1, 368 Direction::Right if self.reg_nib == 7 && (self.reg_index as usize) < NUM_CELLS - 1 => { 369 self.reg_index += 1; 370 self.reg_nib = 0; 371 } 372 Direction::Right if self.reg_nib < 7 => self.reg_nib += 1, 373 Direction::Up if self.reg_index > 0 => self.reg_index -= 1, 374 Direction::Down if (self.reg_index as usize) < NUM_CELLS - 1 => self.reg_index += 1, 375 _ => {} 376 } 377 } 378 379 fn edit_nibble(&mut self, nibble: u8) { 380 match self.focus { 381 Focus::Memory => { 382 let addr = self.mem_addr; 383 let byte = self.perilus.get_memory_byte(addr); 384 let byte = if self.mem_nib == 0 { 385 (byte & 0x0f) | (nibble << 4) 386 } else { 387 (byte & 0xf0) | nibble 388 }; 389 self.perilus.set_memory_byte(addr, byte); 390 self.move_memory(Direction::Right); 391 } 392 Focus::Registers => { 393 let shift = (7 - self.reg_nib as u32) * 4; 394 if self.reg_index == 0 { 395 let value = (self.current_pc & !(0xf << shift)) | ((nibble as u32) << shift); 396 self.perilus.set_pc(value); 397 self.current_pc = value; 398 } else { 399 let index = self.reg_index as usize; 400 let value = self.perilus.get_registers()[index]; 401 let value = (value & !(0xf << shift)) | ((nibble as u32) << shift); 402 if let Some(reg) = Register::new(index as u32) { 403 self.perilus.set_register(reg, value); 404 } 405 } 406 self.move_register(Direction::Right); 407 } 408 } 409 } 410 411 fn backspace(&mut self) { 412 match self.focus { 413 Focus::Memory => { 414 let order = row_digits(self.view); 415 let offset = self.mem_addr % BYTES_PER_ROW as u32; 416 let col = order 417 .iter() 418 .position(|&(o, n)| o == offset && n == self.mem_nib) 419 .unwrap_or(0); 420 if self.mem_addr < BYTES_PER_ROW as u32 && col == 0 { 421 return; 422 } 423 let addr = self.mem_addr; 424 let byte = self.perilus.get_memory_byte(addr); 425 let byte = if self.mem_nib == 0 { 426 byte & 0x0f 427 } else { 428 byte & 0xf0 429 }; 430 self.perilus.set_memory_byte(addr, byte); 431 self.move_memory(Direction::Left); 432 } 433 Focus::Registers => { 434 if self.reg_index == 0 && self.reg_nib == 0 { 435 return; 436 } 437 let shift = (7 - self.reg_nib as u32) * 4; 438 if self.reg_index == 0 { 439 let value = self.current_pc & !(0xf << shift); 440 self.perilus.set_pc(value); 441 self.current_pc = value; 442 } else { 443 let index = self.reg_index as usize; 444 let value = self.perilus.get_registers()[index] & !(0xf << shift); 445 if let Some(reg) = Register::new(index as u32) { 446 self.perilus.set_register(reg, value); 447 } 448 } 449 self.move_register(Direction::Left); 450 } 451 } 452 } 453 454 fn clear_cursor(&mut self) { 455 match self.focus { 456 Focus::Memory => match self.view { 457 ViewMode::Word => { 458 let word = self.mem_addr / 4; 459 self.perilus.set_memory(word, 0); 460 self.status = format!("Cleared word 0x{:08x}", word * 4); 461 } 462 ViewMode::Byte => { 463 self.perilus.set_memory_byte(self.mem_addr, 0); 464 self.status = format!("Cleared byte 0x{:08x}", self.mem_addr); 465 } 466 }, 467 Focus::Registers => { 468 if self.reg_index == 0 { 469 self.perilus.set_pc(0); 470 self.current_pc = 0; 471 self.status = "Cleared PC".into(); 472 } else if let Some(reg) = Register::new(self.reg_index as u32) { 473 self.perilus.set_register(reg, 0); 474 self.status = format!("Cleared x{}", self.reg_index); 475 } 476 } 477 } 478 } 479 480 fn scroll_page(&mut self, down: bool) { 481 let total_rows = Perilus::MEMORY_SIZE_WORDS / WORDS_PER_ROW; 482 let page = self.mem_viewport.len().max(1) as isize; 483 let top = self.mem_viewport.start as isize; 484 let new_top = if down { 485 (top + page).min(total_rows as isize - 1) 486 } else { 487 (top - page).max(0) 488 }; 489 self.scroll_to_row = Some(new_top as usize); 490 } 491 492 fn handle_keys(&mut self, ui: &egui::Ui) { 493 let ctx = ui.ctx(); 494 if ctx.text_edit_focused() { 495 return; 496 } 497 let actions = ctx.input_mut(|i| { 498 let mut actions = Actions { 499 step: i.consume_key(Modifiers::NONE, Key::Space), 500 stop: i.consume_key(Modifiers::NONE, Key::Escape), 501 backspace: i.consume_key(Modifiers::NONE, Key::Backspace), 502 next_instruction: i.consume_key(Modifiers::NONE, Key::N), 503 focus_address: i.consume_key(Modifiers::NONE, Key::G), 504 names: i.consume_key(Modifiers::NONE, Key::X), 505 view: i.consume_key(Modifiers::NONE, Key::V), 506 trace: i.consume_key(Modifiers::NONE, Key::T), 507 clear: i.consume_key(Modifiers::SHIFT, Key::C), 508 follow: i.consume_key(Modifiers::SHIFT, Key::P), 509 set_pc_cursor: i.consume_key(Modifiers::NONE, Key::P), 510 ..Default::default() 511 }; 512 actions.run_to_cursor = i.consume_key(CTRL_SHIFT, Key::Space); 513 actions.free_run = i.consume_key(Modifiers::CTRL, Key::Space); 514 actions.reset = i.consume_key(Modifiers::CTRL, Key::R); 515 actions.focus_registers = i.consume_key(Modifiers::NONE, Key::R); 516 actions.focus_memory = i.consume_key(Modifiers::NONE, Key::M); 517 actions.group_left = i.consume_key(Modifiers::CTRL, Key::ArrowLeft) 518 || i.consume_key(Modifiers::CTRL, Key::H); 519 actions.group_right = i.consume_key(Modifiers::CTRL, Key::ArrowRight) 520 || i.consume_key(Modifiers::CTRL, Key::L); 521 actions.page_up = i.consume_key(Modifiers::NONE, Key::PageUp) 522 || i.consume_key(Modifiers::CTRL, Key::U); 523 actions.page_down = i.consume_key(Modifiers::NONE, Key::PageDown) 524 || i.consume_key(Modifiers::CTRL, Key::D); 525 actions.left = i.consume_key(Modifiers::NONE, Key::ArrowLeft) 526 || i.consume_key(Modifiers::NONE, Key::H); 527 actions.right = i.consume_key(Modifiers::NONE, Key::ArrowRight) 528 || i.consume_key(Modifiers::NONE, Key::L); 529 actions.up = i.consume_key(Modifiers::NONE, Key::ArrowUp) 530 || i.consume_key(Modifiers::NONE, Key::K); 531 actions.down = i.consume_key(Modifiers::NONE, Key::ArrowDown) 532 || i.consume_key(Modifiers::NONE, Key::J); 533 for (key, value) in HEX_KEYS { 534 if i.consume_key(Modifiers::NONE, key) { 535 actions.nibble = Some(value); 536 } 537 } 538 actions 539 }); 540 541 if actions.run_to_cursor { 542 self.run_to_cursor(); 543 } 544 if actions.free_run { 545 self.toggle_free_run(); 546 } 547 if actions.step { 548 self.step_cycle(); 549 } 550 if actions.next_instruction { 551 self.next_instruction(); 552 } 553 if actions.reset { 554 self.reset(); 555 } 556 if actions.focus_registers { 557 self.focus = Focus::Registers; 558 } 559 if actions.focus_memory { 560 self.focus = Focus::Memory; 561 } 562 if actions.set_pc_cursor { 563 self.set_pc_to_cursor(); 564 } 565 if actions.stop { 566 self.stop(); 567 } 568 if actions.focus_address { 569 self.focus_address = true; 570 } 571 if actions.follow { 572 self.toggle_follow(); 573 } 574 if actions.names { 575 self.show_aliases = !self.show_aliases; 576 } 577 if actions.view { 578 self.view = match self.view { 579 ViewMode::Word => ViewMode::Byte, 580 ViewMode::Byte => ViewMode::Word, 581 }; 582 } 583 if actions.trace { 584 self.toggle_trace(); 585 } 586 if actions.clear { 587 self.clear_cursor(); 588 } 589 if actions.group_left && self.focus == Focus::Memory { 590 self.move_memory_group(false); 591 } 592 if actions.group_right && self.focus == Focus::Memory { 593 self.move_memory_group(true); 594 } 595 if actions.page_up { 596 self.scroll_page(false); 597 } 598 if actions.page_down { 599 self.scroll_page(true); 600 } 601 let direction = if actions.left { 602 Some(Direction::Left) 603 } else if actions.right { 604 Some(Direction::Right) 605 } else if actions.up { 606 Some(Direction::Up) 607 } else if actions.down { 608 Some(Direction::Down) 609 } else { 610 None 611 }; 612 if let Some(direction) = direction { 613 self.move_cursor(direction); 614 } 615 if let Some(nibble) = actions.nibble { 616 self.edit_nibble(nibble); 617 } 618 if actions.backspace { 619 self.backspace(); 620 } 621 } 622 } 623 624 impl eframe::App for SimApp { 625 fn logic(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) { 626 match self.run_state { 627 RunState::Idle => {} 628 RunState::FreeRun => { 629 let start = Instant::now(); 630 while start.elapsed() < FREE_RUN_BUDGET { 631 self.perilus.pulse_clock(); 632 } 633 self.sync_pc(); 634 ctx.request_repaint(); 635 } 636 RunState::RunToPc(target) => { 637 let start = Instant::now(); 638 loop { 639 if self.perilus.get_control_unit_state() == 0 && self.perilus.get_pc() == target 640 { 641 self.status = format!("Reached PC 0x{target:08x}"); 642 self.run_state = RunState::Idle; 643 break; 644 } 645 self.perilus.pulse_clock(); 646 self.run_cycles += 1; 647 if self.run_cycles >= MAX_RUN_CYCLES { 648 self.status = format!( 649 "Stopped: did not reach PC 0x{target:08x} after {MAX_RUN_CYCLES} cycles" 650 ); 651 self.run_state = RunState::Idle; 652 break; 653 } 654 if start.elapsed() >= FREE_RUN_BUDGET { 655 ctx.request_repaint(); 656 break; 657 } 658 } 659 self.sync_pc(); 660 } 661 } 662 } 663 664 fn ui(&mut self, ui: &mut egui::Ui, _frame: &mut eframe::Frame) { 665 self.handle_keys(ui); 666 let top_frame = egui::Frame::side_top_panel(ui.style()).shadow(egui::Shadow::NONE); 667 egui::Panel::top("registers_panel") 668 .frame(top_frame) 669 .show_separator_line(false) 670 .show(ui, |ui| { 671 ui.add_space(4.0); 672 self.registers_ui(ui); 673 ui.add_space(4.0); 674 }); 675 let bottom_frame = egui::Frame::side_top_panel(ui.style()).shadow(egui::Shadow::NONE); 676 egui::Panel::bottom("controls_panel") 677 .frame(bottom_frame) 678 .show_separator_line(false) 679 .show(ui, |ui| { 680 ui.add_space(4.0); 681 self.controls_ui(ui); 682 ui.add_space(4.0); 683 }); 684 egui::CentralPanel::default().show(ui, |ui| { 685 self.memory_ui(ui); 686 }); 687 } 688 } 689 690 pub(crate) fn char_width(ui: &egui::Ui, font: &FontId) -> f32 { 691 ui.painter() 692 .layout_no_wrap("0".to_owned(), font.clone(), Color32::WHITE) 693 .size() 694 .x 695 } 696 697 fn parse_hex(text: &str) -> Option<u32> { 698 let trimmed = text.trim(); 699 let digits = trimmed 700 .strip_prefix("0x") 701 .or_else(|| trimmed.strip_prefix("0X")) 702 .unwrap_or(trimmed); 703 if digits.is_empty() { 704 return None; 705 } 706 u32::from_str_radix(digits, 16).ok() 707 } 708 709 /// Format Unix seconds as an ISO-8601 UTC string like `2026-10-04T14:28:19Z`. 710 fn chrono_lite(secs: u64) -> String { 711 let days = secs / 86_400; 712 let mut rem = secs % 86_400; 713 let hours = rem / 3600; 714 rem %= 3600; 715 let minutes = rem / 60; 716 let seconds = rem % 60; 717 718 // Days since epoch → year/month/day (algorithm from Howard Hinnant) 719 let z = days + 719_468; 720 let era = z / 146_097; 721 let doe = z - era * 146_097; 722 let yoe = (doe - doe / 1461 + doe / 36_524 - doe / 146_096) / 365; 723 let y = yoe + era * 400; 724 let doy = doe - (365 * yoe + yoe / 4 - yoe / 100); 725 let mp = (5 * doy + 2) / 153; 726 let d = doy - (153 * mp + 2) / 5 + 1; 727 let m = if mp < 10 { mp + 3 } else { mp - 9 }; 728 let year = if m <= 2 { y + 1 } else { y }; 729 730 format!( 731 "{year:04}-{:02}-{:02}T{:02}:{:02}:{:02}Z", 732 m, d, hours, minutes, seconds 733 ) 734 }