egui: add keyboard shortcuts and basic action abstraction

This commit is contained in:
Mikolaj Wielgus 2024-07-09 02:41:20 +02:00
parent c36ccc287a
commit c34cd730c5
3 changed files with 209 additions and 106 deletions

View File

@ -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
}
}

View File

@ -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;

View File

@ -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,15 +41,75 @@ 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 =
Trigger::new(Action::new("Open", egui::Modifiers::CTRL, egui::Key::O));
let mut import_history = Trigger::new(Action::new(
"Import history",
egui::Modifiers::CTRL,
egui::Key::I,
));
let mut export_history = Trigger::new(Action::new(
"Export history",
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));
egui::TopBottomPanel::top("top_panel") egui::TopBottomPanel::top("top_panel")
.show(ctx, |ui| { .show(ctx, |ui| {
egui::menu::bar(ui, |ui| { egui::menu::bar(ui, |ui| {
ui.menu_button("File", |ui| { ui.menu_button("File", |ui| {
if ui.button("Open").clicked() { open_design.button(ctx, ui);
// `Context` is cheap to clone as it's wrapped in an `Arc`.
let ctx = ui.ctx().clone(); 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);
place_via.toggle_widget(ctx, ui, &mut self.is_placing_via);
ui.separator();
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. // NOTE: On Linux, this requires Zenity to be installed on your system.
let ctx = ctx.clone();
let task = rfd::AsyncFileDialog::new().pick_file(); let task = rfd::AsyncFileDialog::new().pick_file();
execute(async move { execute(async move {
@ -58,12 +119,8 @@ impl Top {
ctx.request_repaint(); ctx.request_repaint();
} }
}); });
} } else if import_history.consume_key_triggered(ctx, ui) {
let ctx = ctx.clone();
ui.separator();
if ui.button("Load history").clicked() {
let ctx = ui.ctx().clone();
let task = rfd::AsyncFileDialog::new().pick_file(); let task = rfd::AsyncFileDialog::new().pick_file();
execute(async move { execute(async move {
@ -73,11 +130,10 @@ impl Top {
ctx.request_repaint(); ctx.request_repaint();
} }
}); });
} else if ui.button("Save history").clicked() { } else if export_history.consume_key_triggered(ctx, ui) {
if let Some(invoker) = if let Some(invoker) = arc_mutex_maybe_invoker.clone().lock().unwrap().as_ref()
arc_mutex_maybe_invoker.clone().lock().unwrap().as_ref()
{ {
let ctx = ui.ctx().clone(); let ctx = ctx.clone();
let task = rfd::AsyncFileDialog::new().save_file(); let task = rfd::AsyncFileDialog::new().save_file();
// FIXME: I don't think we should be buffering everything in a `Vec<u8>`. // FIXME: I don't think we should be buffering everything in a `Vec<u8>`.
@ -86,26 +142,14 @@ impl Top {
execute(async move { execute(async move {
if let Some(file_handle) = task.await { if let Some(file_handle) = task.await {
dbg!(file_handle.write(&writebuf).await); file_handle.write(&writebuf).await;
ctx.request_repaint(); ctx.request_repaint();
} }
}); });
} }
} } else if quit.consume_key_triggered(ctx, ui) {
ui.separator();
// "Quit" button wouldn't work on a Web page.
if !cfg!(target_arch = "wasm32") {
if ui.button("Quit").clicked() {
ctx.send_viewport_cmd(egui::ViewportCommand::Close); ctx.send_viewport_cmd(egui::ViewportCommand::Close);
} } else if autoroute.consume_key_triggered(ctx, ui) {
}
});
ui.separator();
if ui.button("Autoroute").clicked() {
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))
}) { }) {
@ -120,43 +164,20 @@ impl Top {
)); ));
} }
} }
} } else if place_via.consume_key_enabled(ctx, ui, &mut self.is_placing_via) {
} else if undo.consume_key_triggered(ctx, ui) {
ui.toggle_value(&mut self.is_placing_via, "Place Via"); if let Some(invoker) = arc_mutex_maybe_invoker.lock().unwrap().as_mut() {
ui.separator();
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(); invoker.undo();
} }
} } else if redo.consume_key_triggered(ctx, ui) {
if let Some(ref mut invoker) = arc_mutex_maybe_invoker.lock().unwrap().as_mut()
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(); 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>(()) Ok::<(), InvokerError>(())
});
}) })
.inner, .inner
)
} }
} }