topola/src/bin/topola-egui/top.rs

349 lines
14 KiB
Rust

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<String>,
history_sender: Sender<String>,
arc_mutex_maybe_invoker: Arc<Mutex<Option<Invoker<SpecctraMesadata>>>>,
maybe_execute: &mut Option<ExecuteWithStatus>,
maybe_overlay: &mut Option<Overlay>,
maybe_design: &Option<SpecctraDesign>,
) -> 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<u8>`.
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
}
}