mirror of https://codeberg.org/topola/topola.git
egui: add keyboard shortcuts and basic action abstraction
This commit is contained in:
parent
c36ccc287a
commit
c34cd730c5
|
|
@ -0,0 +1,81 @@
|
||||||
|
pub struct Action {
|
||||||
|
name: String,
|
||||||
|
shortcut: egui::KeyboardShortcut,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Action {
|
||||||
|
pub fn new(name: &str, modifiers: egui::Modifiers, key: egui::Key) -> Self {
|
||||||
|
Self {
|
||||||
|
name: String::from(name),
|
||||||
|
shortcut: egui::KeyboardShortcut::new(modifiers, key),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn widget_text(&self) -> String {
|
||||||
|
format!(
|
||||||
|
"{} ({})",
|
||||||
|
self.name,
|
||||||
|
self.shortcut.format(&egui::ModifierNames::NAMES, false)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct Trigger {
|
||||||
|
action: Action,
|
||||||
|
triggered: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Trigger {
|
||||||
|
pub fn new(action: Action) -> Self {
|
||||||
|
Self {
|
||||||
|
action,
|
||||||
|
triggered: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn button(&mut self, ctx: &egui::Context, ui: &mut egui::Ui) {
|
||||||
|
self.triggered = ui.button(self.action.widget_text()).clicked();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn consume_key_triggered(&mut self, ctx: &egui::Context, ui: &mut egui::Ui) -> bool {
|
||||||
|
self.consume_key(ctx, ui);
|
||||||
|
self.triggered()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn consume_key(&mut self, ctx: &egui::Context, ui: &mut egui::Ui) {
|
||||||
|
if ctx.input_mut(|i| i.consume_shortcut(&self.action.shortcut)) {
|
||||||
|
self.triggered = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn triggered(&self) -> bool {
|
||||||
|
self.triggered
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct Switch {
|
||||||
|
action: Action,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Switch {
|
||||||
|
pub fn new(action: Action) -> Self {
|
||||||
|
Self { action }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn toggle_widget(&mut self, ctx: &egui::Context, ui: &mut egui::Ui, selected: &mut bool) {
|
||||||
|
ui.toggle_value(selected, self.action.widget_text());
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn consume_key_enabled(
|
||||||
|
&mut self,
|
||||||
|
ctx: &egui::Context,
|
||||||
|
ui: &mut egui::Ui,
|
||||||
|
selected: &mut bool,
|
||||||
|
) -> bool {
|
||||||
|
if ctx.input_mut(|i| i.consume_shortcut(&self.action.shortcut)) {
|
||||||
|
*selected = !*selected;
|
||||||
|
}
|
||||||
|
|
||||||
|
*selected
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] // hide console window on Windows in release
|
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] // hide console window on Windows in release
|
||||||
|
|
||||||
|
mod action;
|
||||||
mod app;
|
mod app;
|
||||||
mod bottom;
|
mod bottom;
|
||||||
mod file_receiver;
|
mod file_receiver;
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,7 @@ use topola::{
|
||||||
};
|
};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
|
action::{Action, Switch, Trigger},
|
||||||
app::{channel_text, execute},
|
app::{channel_text, execute},
|
||||||
file_sender::FileSender,
|
file_sender::FileSender,
|
||||||
overlay::Overlay,
|
overlay::Overlay,
|
||||||
|
|
@ -40,123 +41,143 @@ impl Top {
|
||||||
maybe_execute: &mut Option<ExecuteWithStatus>,
|
maybe_execute: &mut Option<ExecuteWithStatus>,
|
||||||
maybe_overlay: &mut Option<Overlay>,
|
maybe_overlay: &mut Option<Overlay>,
|
||||||
) -> Result<(), InvokerError> {
|
) -> Result<(), InvokerError> {
|
||||||
Ok::<(), InvokerError>(
|
let mut open_design =
|
||||||
egui::TopBottomPanel::top("top_panel")
|
Trigger::new(Action::new("Open", egui::Modifiers::CTRL, egui::Key::O));
|
||||||
.show(ctx, |ui| {
|
let mut import_history = Trigger::new(Action::new(
|
||||||
egui::menu::bar(ui, |ui| {
|
"Import history",
|
||||||
ui.menu_button("File", |ui| {
|
egui::Modifiers::CTRL,
|
||||||
if ui.button("Open").clicked() {
|
egui::Key::I,
|
||||||
// `Context` is cheap to clone as it's wrapped in an `Arc`.
|
));
|
||||||
let ctx = ui.ctx().clone();
|
let mut export_history = Trigger::new(Action::new(
|
||||||
// NOTE: On Linux, this requires Zenity to be installed on your system.
|
"Export history",
|
||||||
let task = rfd::AsyncFileDialog::new().pick_file();
|
egui::Modifiers::CTRL,
|
||||||
|
egui::Key::E,
|
||||||
|
));
|
||||||
|
let mut quit = Trigger::new(Action::new("Quit", egui::Modifiers::CTRL, egui::Key::V));
|
||||||
|
let mut autoroute = Trigger::new(Action::new(
|
||||||
|
"Autoroute",
|
||||||
|
egui::Modifiers::CTRL,
|
||||||
|
egui::Key::A,
|
||||||
|
));
|
||||||
|
let mut place_via = Switch::new(Action::new(
|
||||||
|
"Place Via",
|
||||||
|
egui::Modifiers::CTRL,
|
||||||
|
egui::Key::P,
|
||||||
|
));
|
||||||
|
let mut undo = Trigger::new(Action::new("Undo", egui::Modifiers::CTRL, egui::Key::Z));
|
||||||
|
let mut redo = Trigger::new(Action::new("Redo", egui::Modifiers::CTRL, egui::Key::Y));
|
||||||
|
|
||||||
execute(async move {
|
egui::TopBottomPanel::top("top_panel")
|
||||||
if let Some(file_handle) = task.await {
|
.show(ctx, |ui| {
|
||||||
let file_sender = FileSender::new(content_sender);
|
egui::menu::bar(ui, |ui| {
|
||||||
file_sender.send(file_handle).await;
|
ui.menu_button("File", |ui| {
|
||||||
ctx.request_repaint();
|
open_design.button(ctx, ui);
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
ui.separator();
|
ui.separator();
|
||||||
|
|
||||||
if ui.button("Load history").clicked() {
|
import_history.button(ctx, ui);
|
||||||
let ctx = ui.ctx().clone();
|
export_history.button(ctx, ui);
|
||||||
let task = rfd::AsyncFileDialog::new().pick_file();
|
|
||||||
|
|
||||||
execute(async move {
|
ui.separator();
|
||||||
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 ui.button("Save history").clicked() {
|
|
||||||
if let Some(invoker) =
|
|
||||||
arc_mutex_maybe_invoker.clone().lock().unwrap().as_ref()
|
|
||||||
{
|
|
||||||
let ctx = ui.ctx().clone();
|
|
||||||
let task = rfd::AsyncFileDialog::new().save_file();
|
|
||||||
|
|
||||||
// FIXME: I don't think we should be buffering everything in a `Vec<u8>`.
|
// "Quit" button wouldn't work on a Web page.
|
||||||
let mut writebuf = vec![];
|
if !cfg!(target_arch = "wasm32") {
|
||||||
serde_json::to_writer_pretty(&mut writebuf, invoker.history());
|
quit.button(ctx, ui);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
execute(async move {
|
ui.separator();
|
||||||
if let Some(file_handle) = task.await {
|
|
||||||
dbg!(file_handle.write(&writebuf).await);
|
|
||||||
ctx.request_repaint();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ui.separator();
|
autoroute.button(ctx, ui);
|
||||||
|
|
||||||
// "Quit" button wouldn't work on a Web page.
|
place_via.toggle_widget(ctx, ui, &mut self.is_placing_via);
|
||||||
if !cfg!(target_arch = "wasm32") {
|
|
||||||
if ui.button("Quit").clicked() {
|
ui.separator();
|
||||||
ctx.send_viewport_cmd(egui::ViewportCommand::Close);
|
|
||||||
}
|
undo.button(ctx, ui);
|
||||||
|
redo.button(ctx, ui);
|
||||||
|
|
||||||
|
ui.separator();
|
||||||
|
|
||||||
|
ui.toggle_value(&mut self.show_ratsnest, "Show Ratsnest");
|
||||||
|
ui.toggle_value(&mut self.show_navmesh, "Show Navmesh");
|
||||||
|
|
||||||
|
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 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();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
}
|
||||||
ui.separator();
|
} else if quit.consume_key_triggered(ctx, ui) {
|
||||||
|
ctx.send_viewport_cmd(egui::ViewportCommand::Close);
|
||||||
if ui.button("Autoroute").clicked() {
|
} else if autoroute.consume_key_triggered(ctx, ui) {
|
||||||
if maybe_execute.as_mut().map_or(true, |execute| {
|
if maybe_execute.as_mut().map_or(true, |execute| {
|
||||||
matches!(execute.maybe_status(), Some(InvokerStatus::Finished))
|
matches!(execute.maybe_status(), Some(InvokerStatus::Finished))
|
||||||
}) {
|
}) {
|
||||||
if let (Some(invoker), Some(ref mut overlay)) = (
|
if let (Some(invoker), Some(ref mut overlay)) = (
|
||||||
arc_mutex_maybe_invoker.lock().unwrap().as_mut(),
|
arc_mutex_maybe_invoker.lock().unwrap().as_mut(),
|
||||||
maybe_overlay,
|
maybe_overlay,
|
||||||
) {
|
) {
|
||||||
let selection = overlay.selection().clone();
|
let selection = overlay.selection().clone();
|
||||||
overlay.clear_selection();
|
overlay.clear_selection();
|
||||||
maybe_execute.insert(ExecuteWithStatus::new(
|
maybe_execute.insert(ExecuteWithStatus::new(
|
||||||
invoker.execute_walk(Command::Autoroute(selection))?,
|
invoker.execute_walk(Command::Autoroute(selection))?,
|
||||||
));
|
));
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
} else if place_via.consume_key_enabled(ctx, ui, &mut self.is_placing_via) {
|
||||||
|
} 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(ref mut invoker) = arc_mutex_maybe_invoker.lock().unwrap().as_mut()
|
||||||
|
{
|
||||||
|
invoker.redo();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
ui.toggle_value(&mut self.is_placing_via, "Place Via");
|
Ok::<(), InvokerError>(())
|
||||||
|
})
|
||||||
ui.separator();
|
.inner
|
||||||
|
|
||||||
if ui.button("Undo").clicked()
|
|
||||||
|| ctx.input_mut(|i| i.consume_key(egui::Modifiers::CTRL, egui::Key::Z))
|
|
||||||
{
|
|
||||||
if let Some(invoker) = arc_mutex_maybe_invoker.lock().unwrap().as_mut()
|
|
||||||
{
|
|
||||||
invoker.undo();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if ui.button("Redo").clicked()
|
|
||||||
|| ctx.input_mut(|i| i.consume_key(egui::Modifiers::CTRL, egui::Key::Y))
|
|
||||||
{
|
|
||||||
if let Some(ref mut invoker) =
|
|
||||||
arc_mutex_maybe_invoker.lock().unwrap().as_mut()
|
|
||||||
{
|
|
||||||
invoker.redo();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ui.separator();
|
|
||||||
|
|
||||||
ui.toggle_value(&mut self.show_ratsnest, "Show Ratsnest");
|
|
||||||
ui.toggle_value(&mut self.show_navmesh, "Show Navmesh");
|
|
||||||
|
|
||||||
ui.separator();
|
|
||||||
|
|
||||||
egui::widgets::global_dark_light_mode_buttons(ui);
|
|
||||||
Ok::<(), InvokerError>(())
|
|
||||||
});
|
|
||||||
})
|
|
||||||
.inner,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue