diff --git a/src/bin/topola-egui/menu_bar.rs b/src/bin/topola-egui/menu_bar.rs new file mode 100644 index 0000000..ff3219a --- /dev/null +++ b/src/bin/topola-egui/menu_bar.rs @@ -0,0 +1,378 @@ +use std::{ + fs::File, + path::Path, + sync::{mpsc::Sender, Arc, Mutex}, +}; + +use topola::{ + autorouter::{ + execute::Command, + invoker::{Invoker, InvokerError, InvokerStatus}, + AutorouterOptions, + }, + router::RouterOptions, + specctra::{design::SpecctraDesign, mesadata::SpecctraMesadata}, +}; + +use crate::{ + action::{Action, Switch, Trigger}, + activity::{ActivityStatus, ActivityWithStatus}, + app::execute, + file_sender::FileSender, + overlay::Overlay, + translator::Translator, + viewport::Viewport, +}; + +pub struct MenuBar { + 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, + pub show_layer_manager: bool, + pub frame_timestep: f32, +} + +impl MenuBar { + 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, + show_layer_manager: true, + frame_timestep: 0.1, + } + } + + pub fn update( + &mut self, + ctx: &egui::Context, + tr: &Translator, + content_sender: Sender, + history_sender: Sender, + arc_mutex_maybe_invoker: Arc>>>, + maybe_activity: &mut Option, + viewport: &mut Viewport, + 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::Q, + )); + 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("menu_bar") + .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.menu_button(tr.text("menu-edit"), |ui| { + undo.button(ctx, ui); + redo.button(ctx, ui); + + ui.separator(); + + remove_bands.button(ctx, ui); + }); + + ui.menu_button(tr.text("menu-view"), |ui| { + ui.toggle_value( + &mut viewport.scheduled_zoom_to_fit, + tr.text("zoom-to-fit"), + ); + + ui.separator(); + + 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(); + + ui.checkbox(&mut self.show_layer_manager, tr.text("show-layer-manager")); + + ui.separator(); + + ui.label(tr.text("frame-timestep")); + ui.add( + egui::widgets::Slider::new(&mut self.frame_timestep, 0.0..=3.0) + .suffix(" s"), + ); + }); + + ui.menu_button(tr.text("menu-place"), |ui| { + place_via.toggle_widget(ctx, ui, &mut self.is_placing_via); + }); + + ui.menu_button(tr.text("menu-route"), |ui| { + autoroute.button(ctx, ui); + ui.separator(); + + 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.menu_button(tr.text("menu-inspect"), |ui| { + 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.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.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_activity.as_mut().map_or(true, |activity| { + matches!(activity.maybe_status(), Some(ActivityStatus::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_activity.insert(ActivityWithStatus::new_execute( + 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_activity.as_mut().map_or(true, |activity| { + matches!(activity.maybe_status(), Some(ActivityStatus::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_activity.insert(ActivityWithStatus::new_execute( + invoker.execute_stepper(Command::RemoveBands( + selection.band_selection, + ))?, + )); + } + } + } else if measure_length.consume_key_triggered(ctx, ui) { + if maybe_activity.as_mut().map_or(true, |activity| { + matches!(activity.maybe_status(), Some(ActivityStatus::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_activity.insert(ActivityWithStatus::new_execute( + 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_activity.as_mut().map_or(true, |activity| { + matches!(activity.maybe_status(), Some(ActivityStatus::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_activity.insert(ActivityWithStatus::new_execute( + invoker.execute_stepper(Command::CompareDetours( + selection.pin_selection, + self.autorouter_options, + ))?, + )); + } + } + } + + Ok::<(), InvokerError>(()) + }) + .inner + } +} diff --git a/src/bin/topola-egui/status_bar.rs b/src/bin/topola-egui/status_bar.rs new file mode 100644 index 0000000..097ffc3 --- /dev/null +++ b/src/bin/topola-egui/status_bar.rs @@ -0,0 +1,41 @@ +use topola::autorouter::invoker::InvokerStatus; + +use crate::{ + activity::{ActivityStatus, ActivityWithStatus}, + translator::Translator, + viewport::Viewport, +}; + +pub struct StatusBar {} + +impl StatusBar { + pub fn new() -> Self { + Self {} + } + + pub fn update( + &mut self, + ctx: &egui::Context, + tr: &Translator, + viewport: &Viewport, + maybe_activity: &Option, + ) { + egui::TopBottomPanel::bottom("status_bar").show(ctx, |ui| { + let latest_pos = viewport.transform.inverse() + * ctx.input(|i| i.pointer.latest_pos().unwrap_or_default()); + + let mut message = String::from(""); + + if let Some(activity) = maybe_activity { + if let Some(ActivityStatus::Finished(msg)) = activity.maybe_status() { + message = msg; + } + } + + ui.label(format!( + "x: {} y: {} \t {}", + latest_pos.x, -latest_pos.y, message + )); + }); + } +}