use std::{ fs::File, path::Path, sync::{mpsc::Sender, Arc, Mutex}, }; use topola::{ autorouter::{ invoker::{Command, Execute, ExecuteWithStatus, Invoker, InvokerError, InvokerStatus}, AutorouterOptions, }, router::RouterOptions, specctra::{design::SpecctraDesign, mesadata::SpecctraMesadata}, }; use crate::{ action::{Action, Switch, Trigger}, app::{channel_text, execute}, file_sender::FileSender, overlay::Overlay, translator::Translator, }; pub struct Top { pub autorouter_options: AutorouterOptions, pub is_placing_via: bool, pub show_ratsnest: bool, pub show_navmesh: bool, pub show_bboxes: bool, pub show_origin_destination: bool, } impl Top { pub fn new() -> Self { Self { autorouter_options: AutorouterOptions { presort_by_pairwise_detours: false, router_options: RouterOptions { wrap_around_bands: true, squeeze_under_bands: true, }, }, is_placing_via: false, show_ratsnest: false, show_navmesh: false, show_bboxes: false, show_origin_destination: false, } } pub fn update( &mut self, ctx: &egui::Context, tr: &Translator, content_sender: Sender, history_sender: Sender, arc_mutex_maybe_invoker: Arc>>>, maybe_execute: &mut Option, maybe_overlay: &mut Option, maybe_design: &Option, ) -> Result<(), InvokerError> { let mut open_design = Trigger::new(Action::new( tr.text("action-open-dsn"), egui::Modifiers::CTRL, egui::Key::O, )); let mut export_session = Trigger::new(Action::new( tr.text("action-export-ses"), egui::Modifiers::CTRL, egui::Key::S, )); let mut import_history = Trigger::new(Action::new( tr.text("action-import-cmd"), egui::Modifiers::CTRL, egui::Key::I, )); let mut export_history = Trigger::new(Action::new( tr.text("action-export-cmd"), egui::Modifiers::CTRL, egui::Key::E, )); let mut quit = Trigger::new(Action::new( tr.text("action-quit"), egui::Modifiers::CTRL, egui::Key::V, )); let mut autoroute = Trigger::new(Action::new( tr.text("action-autoroute"), egui::Modifiers::CTRL, egui::Key::A, )); let mut place_via = Switch::new(Action::new( tr.text("action-place-via"), egui::Modifiers::CTRL, egui::Key::P, )); let mut remove_bands = Trigger::new(Action::new( tr.text("action-remove-bands"), egui::Modifiers::NONE, egui::Key::Delete, )); let mut compare_detours = Trigger::new(Action::new( tr.text("action-compare-detours"), egui::Modifiers::NONE, egui::Key::Minus, )); let mut measure_length = Trigger::new(Action::new( tr.text("action-measure-length"), egui::Modifiers::NONE, egui::Key::Plus, )); let mut undo = Trigger::new(Action::new( tr.text("action-undo"), egui::Modifiers::CTRL, egui::Key::Z, )); let mut redo = Trigger::new(Action::new( tr.text("action-redo"), egui::Modifiers::CTRL, egui::Key::Y, )); egui::TopBottomPanel::top("top_panel") .show(ctx, |ui| { egui::menu::bar(ui, |ui| { ui.menu_button(tr.text("menu-file"), |ui| { open_design.button(ctx, ui); export_session.button(ctx, ui); ui.separator(); import_history.button(ctx, ui); export_history.button(ctx, ui); ui.separator(); // "Quit" button wouldn't work on a Web page. if !cfg!(target_arch = "wasm32") { quit.button(ctx, ui); } }); ui.separator(); autoroute.button(ctx, ui); ui.menu_button(tr.text("menu-options"), |ui| { ui.checkbox( &mut self.autorouter_options.presort_by_pairwise_detours, tr.text("presort-by-pairwise-detours"), ); ui.checkbox( &mut self.autorouter_options.router_options.squeeze_under_bands, tr.text("squeeze-under-bands"), ); ui.checkbox( &mut self.autorouter_options.router_options.wrap_around_bands, tr.text("wrap-around-bands"), ); }); ui.separator(); place_via.toggle_widget(ctx, ui, &mut self.is_placing_via); remove_bands.button(ctx, ui); ui.separator(); undo.button(ctx, ui); redo.button(ctx, ui); ui.separator(); ui.menu_button(tr.text("menu-debug"), |ui| { ui.checkbox(&mut self.show_ratsnest, tr.text("show-ratsnest")); ui.checkbox(&mut self.show_navmesh, tr.text("show-navmesh")); ui.checkbox(&mut self.show_bboxes, tr.text("show-bboxes")); ui.checkbox( &mut self.show_origin_destination, tr.text("show-origin-destination"), ); ui.separator(); compare_detours.button(ctx, ui); measure_length.button(ctx, ui); }); ui.separator(); egui::widgets::global_dark_light_mode_buttons(ui); }); if open_design.consume_key_triggered(ctx, ui) { // NOTE: On Linux, this requires Zenity to be installed on your system. let ctx = ctx.clone(); let task = rfd::AsyncFileDialog::new().pick_file(); execute(async move { if let Some(file_handle) = task.await { let file_sender = FileSender::new(content_sender); file_sender.send(file_handle).await; ctx.request_repaint(); } }); } else if export_session.consume_key_triggered(ctx, ui) { if let Some(design) = maybe_design { if let Some(invoker) = arc_mutex_maybe_invoker.clone().lock().unwrap().as_ref() { let ctx = ui.ctx().clone(); let board = invoker.autorouter().board(); // FIXME: I don't know how to avoid buffering the entire exported file let mut writebuf = vec![]; design.write_ses(board, &mut writebuf); let mut dialog = rfd::AsyncFileDialog::new(); if let Some(filename) = Path::new(design.get_name()).file_stem() { if let Some(filename) = filename.to_str() { dialog = dialog.set_file_name(filename); } } let task = dialog .add_filter(tr.text("specctra-session-file"), &["ses"]) .save_file(); execute(async move { if let Some(file_handle) = task.await { file_handle.write(&writebuf).await; ctx.request_repaint(); } }); } } } else if import_history.consume_key_triggered(ctx, ui) { let ctx = ctx.clone(); let task = rfd::AsyncFileDialog::new().pick_file(); execute(async move { if let Some(file_handle) = task.await { let file_sender = FileSender::new(history_sender); file_sender.send(file_handle).await; ctx.request_repaint(); } }); } else if export_history.consume_key_triggered(ctx, ui) { if let Some(invoker) = arc_mutex_maybe_invoker.clone().lock().unwrap().as_ref() { let ctx = ctx.clone(); let task = rfd::AsyncFileDialog::new().save_file(); // FIXME: I don't think we should be buffering everything in a `Vec`. let mut writebuf = vec![]; serde_json::to_writer_pretty(&mut writebuf, invoker.history()); execute(async move { if let Some(file_handle) = task.await { file_handle.write(&writebuf).await; ctx.request_repaint(); } }); } } else if quit.consume_key_triggered(ctx, ui) { ctx.send_viewport_cmd(egui::ViewportCommand::Close); } else if autoroute.consume_key_triggered(ctx, ui) { if maybe_execute.as_mut().map_or(true, |execute| { matches!(execute.maybe_status(), Some(InvokerStatus::Finished(..))) }) { if let (Some(invoker), Some(ref mut overlay)) = ( arc_mutex_maybe_invoker.lock().unwrap().as_mut(), maybe_overlay, ) { let selection = overlay.selection().clone(); overlay.clear_selection(); maybe_execute.insert(ExecuteWithStatus::new(invoker.execute_stepper( Command::Autoroute( selection.pin_selection, self.autorouter_options, ), )?)); } } } else if place_via.consume_key_enabled(ctx, ui, &mut self.is_placing_via) { } else if remove_bands.consume_key_triggered(ctx, ui) { if maybe_execute.as_mut().map_or(true, |execute| { matches!(execute.maybe_status(), Some(InvokerStatus::Finished(..))) }) { if let (Some(invoker), Some(ref mut overlay)) = ( arc_mutex_maybe_invoker.lock().unwrap().as_mut(), maybe_overlay, ) { let selection = overlay.selection().clone(); overlay.clear_selection(); maybe_execute.insert(ExecuteWithStatus::new(invoker.execute_stepper( Command::RemoveBands(selection.band_selection), )?)); } } } else if measure_length.consume_key_triggered(ctx, ui) { if maybe_execute.as_mut().map_or(true, |execute| { matches!(execute.maybe_status(), Some(InvokerStatus::Finished(..))) }) { if let (Some(invoker), Some(ref mut overlay)) = ( arc_mutex_maybe_invoker.lock().unwrap().as_mut(), maybe_overlay, ) { let selection = overlay.selection().clone(); overlay.clear_selection(); maybe_execute.insert(ExecuteWithStatus::new(invoker.execute_stepper( Command::MeasureLength(selection.band_selection), )?)); } } } else if undo.consume_key_triggered(ctx, ui) { if let Some(invoker) = arc_mutex_maybe_invoker.lock().unwrap().as_mut() { invoker.undo(); } } else if redo.consume_key_triggered(ctx, ui) { if let Some(invoker) = arc_mutex_maybe_invoker.lock().unwrap().as_mut() { invoker.redo(); } } else if compare_detours.consume_key_triggered(ctx, ui) { if maybe_execute.as_mut().map_or(true, |execute| { matches!(execute.maybe_status(), Some(InvokerStatus::Finished(..))) }) { if let (Some(invoker), Some(ref mut overlay)) = ( arc_mutex_maybe_invoker.lock().unwrap().as_mut(), maybe_overlay, ) { let selection = overlay.selection().clone(); overlay.clear_selection(); maybe_execute.insert(ExecuteWithStatus::new(invoker.execute_stepper( Command::CompareDetours( selection.pin_selection, self.autorouter_options, ), )?)); } } } Ok::<(), InvokerError>(()) }) .inner } }