396 lines
13 KiB
Rust
396 lines
13 KiB
Rust
mod mat;
|
|
mod serial;
|
|
|
|
use clap::Parser;
|
|
use egui::{Button, Color32, Frame, Rect, ScrollArea, Slider, TextEdit, Vec2};
|
|
use eyre::eyre;
|
|
use mat::Mat;
|
|
use serde::{Deserialize, Serialize};
|
|
use serial::{connect_to_serial, scan_for_serial};
|
|
use std::{collections::VecDeque, path::PathBuf};
|
|
use tangentbord1::{
|
|
layer::Layer,
|
|
layout::Layout,
|
|
serial_proto::owned::{ChangeLayer, DeviceMsg, LogRecord},
|
|
};
|
|
use tokio::{
|
|
runtime::Runtime,
|
|
sync::{
|
|
mpsc::{self, Receiver},
|
|
oneshot::{self, error::TryRecvError},
|
|
},
|
|
task::spawn_blocking,
|
|
};
|
|
|
|
#[derive(Parser)]
|
|
struct Opt {
|
|
/// Path to device serial port
|
|
device: Option<PathBuf>,
|
|
}
|
|
|
|
fn main() -> eyre::Result<()> {
|
|
let _opt = Opt::parse();
|
|
|
|
pretty_env_logger::init();
|
|
|
|
let rt = Runtime::new().unwrap();
|
|
let _enter_rt = rt.enter();
|
|
|
|
log::info!("starting application");
|
|
let native_options = eframe::NativeOptions::default();
|
|
eframe::run_native(
|
|
"tgnt keyboard editor",
|
|
native_options,
|
|
Box::new(|cc| Box::new(App::new(cc))),
|
|
)
|
|
.map_err(|e| eyre!("Failed to run program: {e:?}"))?;
|
|
|
|
rt.shutdown_background();
|
|
|
|
Ok(())
|
|
}
|
|
|
|
/// We derive Deserialize/Serialize so we can persist app state on shutdown.
|
|
#[derive(Deserialize, Serialize)]
|
|
#[serde(default)] // if we add new fields, give them default values when deserializing old state
|
|
pub struct App {
|
|
layout: RonEdit<Layout>,
|
|
|
|
layers: Mat<RonEdit<Layer>>,
|
|
|
|
u1: f32,
|
|
margin: f32,
|
|
|
|
#[serde(skip)]
|
|
serial: SerialState,
|
|
}
|
|
|
|
struct SerialState {
|
|
scan_task: Option<oneshot::Receiver<Result<Option<PathBuf>, String>>>,
|
|
dev: Result<Option<PathBuf>, String>,
|
|
reader: Option<Receiver<DeviceMsg>>,
|
|
logs: VecDeque<LogRecord>,
|
|
active_layer: Option<(u16, u16)>,
|
|
}
|
|
|
|
#[derive(Deserialize, Serialize, Clone)]
|
|
struct RonEdit<T> {
|
|
pub t: T,
|
|
pub ron: String,
|
|
pub error: String,
|
|
}
|
|
|
|
impl<T: Serialize> RonEdit<T> {
|
|
pub fn new(t: T) -> Self {
|
|
RonEdit {
|
|
ron: ron::ser::to_string_pretty(&t, Default::default()).unwrap(),
|
|
error: String::new(),
|
|
t,
|
|
}
|
|
}
|
|
}
|
|
|
|
impl<T: Default + Serialize> Default for RonEdit<T> {
|
|
fn default() -> Self {
|
|
RonEdit::new(Default::default())
|
|
}
|
|
}
|
|
|
|
impl Default for App {
|
|
fn default() -> Self {
|
|
Self {
|
|
layers: Default::default(),
|
|
layout: Default::default(),
|
|
u1: 75.0,
|
|
margin: 0.05,
|
|
|
|
serial: Default::default(),
|
|
}
|
|
}
|
|
}
|
|
|
|
impl Default for SerialState {
|
|
fn default() -> Self {
|
|
Self {
|
|
scan_task: Default::default(),
|
|
dev: Ok(None),
|
|
reader: Default::default(),
|
|
logs: Default::default(),
|
|
active_layer: None,
|
|
}
|
|
}
|
|
}
|
|
|
|
impl App {
|
|
/// Called once before the first frame.
|
|
pub fn new(cc: &eframe::CreationContext<'_>) -> Self {
|
|
// This is also where you can customize the look and feel of egui using
|
|
// `cc.egui_ctx.set_visuals` and `cc.egui_ctx.set_fonts`.
|
|
|
|
// Load previous app state (if any).
|
|
// Note that you must enable the `persistence` feature for this to work.
|
|
if let Some(storage) = cc.storage {
|
|
return eframe::get_value(storage, eframe::APP_KEY).unwrap_or_default();
|
|
}
|
|
|
|
let layer_str = include_str!("default_layer.ron");
|
|
let layer: Layer = ron::from_str(&layer_str).expect("Failed to deserialize default layer");
|
|
|
|
let layout_str = include_str!("default_layout.ron");
|
|
let layout: Layout =
|
|
ron::from_str(&layout_str).expect("Failed to deserialize default layout");
|
|
|
|
let mut layers = Mat::default();
|
|
layers.push_row(RonEdit::new(layer));
|
|
|
|
Self {
|
|
layers,
|
|
layout: RonEdit::new(layout),
|
|
..Self::default()
|
|
}
|
|
}
|
|
}
|
|
|
|
impl eframe::App for App {
|
|
/// Called by the frame work to save state before shutdown.
|
|
fn save(&mut self, storage: &mut dyn eframe::Storage) {
|
|
eframe::set_value(storage, eframe::APP_KEY, self);
|
|
}
|
|
|
|
/// Called each time the UI needs repainting, which may be many times per second.
|
|
/// Put your widgets into a `SidePanel`, `TopPanel`, `CentralPanel`, `Window` or `Area`.
|
|
fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) {
|
|
let Self {
|
|
margin,
|
|
u1,
|
|
layout,
|
|
layers,
|
|
serial:
|
|
SerialState {
|
|
scan_task: scan_serial_task,
|
|
dev: serial_devs,
|
|
reader: serial_reader,
|
|
logs: serial_logs,
|
|
active_layer,
|
|
},
|
|
} = self;
|
|
|
|
while let Some(rx) = scan_serial_task {
|
|
match rx.try_recv() {
|
|
Ok(r) => {
|
|
*serial_devs = r;
|
|
*scan_serial_task = None;
|
|
}
|
|
Err(TryRecvError::Empty) => break,
|
|
Err(TryRecvError::Closed) => {
|
|
*scan_serial_task = None;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
while let Some(rx) = serial_reader {
|
|
match rx.try_recv() {
|
|
Ok(DeviceMsg::Log(record)) => {
|
|
serial_logs.push_back(record);
|
|
if serial_logs.len() > 10 {
|
|
serial_logs.pop_front();
|
|
}
|
|
}
|
|
Ok(DeviceMsg::ChangeLayer(ChangeLayer { x, y })) => {
|
|
*active_layer = Some((x, y));
|
|
}
|
|
Ok(_) => {}
|
|
Err(mpsc::error::TryRecvError::Empty) => break,
|
|
Err(mpsc::error::TryRecvError::Disconnected) => {
|
|
*serial_reader = None;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
#[cfg(not(target_arch = "wasm32"))] // no File->Quit on web pages!
|
|
egui::TopBottomPanel::top("top_panel").show(ctx, |ui| {
|
|
// The top panel is often a good place for a menu bar:
|
|
egui::menu::bar(ui, |ui| {
|
|
ui.menu_button("File", |ui| {
|
|
if ui.button("Quit").clicked() {
|
|
todo!("implement quit")
|
|
}
|
|
});
|
|
});
|
|
});
|
|
|
|
egui::SidePanel::left("side_panel")
|
|
.resizable(true)
|
|
.width_range(250.0..=500.0)
|
|
.show(ctx, |ui| {
|
|
ScrollArea::vertical().show(ui, |ui| {
|
|
ui.heading("Side Panel");
|
|
|
|
ui.collapsing("Serial", |ui| {
|
|
match serial_devs {
|
|
Ok(Some(dev)) => {
|
|
if serial_reader.is_none()
|
|
&& ui.button(format!("Connect to {dev:?}")).clicked()
|
|
{
|
|
*serial_reader =
|
|
Some(connect_to_serial(dev.clone(), ctx.clone()));
|
|
};
|
|
}
|
|
Ok(None) => {
|
|
ui.label("No devices found.");
|
|
}
|
|
Err(e) => {
|
|
ui.code_editor(e);
|
|
}
|
|
}
|
|
|
|
if ui.button("scan for serial device").clicked()
|
|
&& scan_serial_task.is_none()
|
|
{
|
|
let (tx, rx) = oneshot::channel();
|
|
let ctx = ctx.clone();
|
|
spawn_blocking(move || {
|
|
let r = scan_for_serial().map_err(|e| e.to_string());
|
|
let _ = tx.send(r);
|
|
ctx.request_repaint();
|
|
});
|
|
|
|
*scan_serial_task = Some(rx);
|
|
}
|
|
|
|
if scan_serial_task.is_some() {
|
|
ui.label("Scanning...");
|
|
}
|
|
|
|
ScrollArea::both().show(ui, |ui| {
|
|
for log in &*serial_logs {
|
|
ui.group(|ui| {
|
|
ui.horizontal(|ui| {
|
|
ui.label(&log.level);
|
|
ui.label(&log.message);
|
|
});
|
|
});
|
|
}
|
|
})
|
|
});
|
|
|
|
ui.label("u1");
|
|
ui.add(Slider::new(u1, 20.0..=150.0));
|
|
ui.label("margin");
|
|
ui.add(Slider::new(margin, 0.0..=0.4));
|
|
|
|
ui.collapsing("Layout", |ui| {
|
|
if ui
|
|
.add(TextEdit::multiline(&mut layout.ron).code_editor())
|
|
.changed()
|
|
{
|
|
layout.error.clear();
|
|
match ron::from_str(&layout.ron) {
|
|
Ok(new) => layout.t = new,
|
|
Err(e) => layout.error = e.to_string(),
|
|
}
|
|
}
|
|
|
|
if !layout.error.is_empty() {
|
|
ui.add(
|
|
TextEdit::multiline(&mut layout.error)
|
|
.interactive(false)
|
|
.text_color(Color32::RED),
|
|
);
|
|
}
|
|
});
|
|
|
|
for x in 0..layers.width() {
|
|
for y in 0..layers.height() {
|
|
let layer = layers.get_mut(x, y).unwrap();
|
|
ui.collapsing(format!("Layer {x},{y}"), |ui| {
|
|
if ui
|
|
.add(TextEdit::multiline(&mut layer.ron).code_editor())
|
|
.changed()
|
|
{
|
|
layer.error.clear();
|
|
match ron::from_str(&layer.ron) {
|
|
Ok(new) => layer.t = new,
|
|
Err(e) => layer.error = e.to_string(),
|
|
}
|
|
}
|
|
|
|
if !layer.error.is_empty() {
|
|
ui.add(
|
|
TextEdit::multiline(&mut layer.error)
|
|
.interactive(false)
|
|
.text_color(Color32::RED),
|
|
);
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
if ui.button("Add layer row").clicked() {
|
|
layers.push_row(RonEdit::default());
|
|
}
|
|
|
|
ui.menu_button("Delete layer row", |ui| {
|
|
for r in 0..layers.height() {
|
|
if ui.button(format!("row {r}")).clicked() {
|
|
layers.remove_row(r);
|
|
}
|
|
}
|
|
});
|
|
|
|
if ui.button("Add layer column").clicked() {
|
|
layers.push_col(RonEdit::default());
|
|
}
|
|
|
|
ui.menu_button("Delete layer column", |ui| {
|
|
for c in 0..layers.width() {
|
|
if ui.button(format!("column {c}")).clicked() {
|
|
layers.remove_col(c);
|
|
}
|
|
}
|
|
});
|
|
});
|
|
});
|
|
|
|
egui::CentralPanel::default().show(ctx, |ui| {
|
|
ScrollArea::both().show(ui, |ui| {
|
|
for (i, layer) in layers.iter_cf().enumerate() {
|
|
let x = (i / layers.height()) as u16;
|
|
let y = (i % layers.height()) as u16;
|
|
|
|
if Some((x, y)) == *active_layer {
|
|
ui.visuals_mut().widgets.inactive.bg_stroke =
|
|
(1.0, Color32::DARK_GREEN).into();
|
|
}
|
|
|
|
Frame::none().show(ui, |ui| {
|
|
for (i, geometry) in layout.t.buttons.iter().enumerate() {
|
|
let margin = *margin * *u1;
|
|
let size = Vec2::new(
|
|
*u1 * geometry.w - margin * 2.0,
|
|
*u1 * geometry.h - margin * 2.0,
|
|
);
|
|
let offset =
|
|
Vec2::new(*u1 * geometry.x + margin, *u1 * geometry.y + margin);
|
|
let rect = Rect::from_min_size(ui.min_rect().min + offset, size);
|
|
|
|
let button = if let Some(button) = layer.t.buttons.get(i) {
|
|
Button::new(button.to_string())
|
|
} else {
|
|
Button::new("")
|
|
};
|
|
|
|
ui.put(rect, button);
|
|
}
|
|
});
|
|
|
|
ui.visuals_mut().widgets = Default::default();
|
|
ui.separator();
|
|
}
|
|
})
|
|
});
|
|
}
|
|
}
|