diff --git a/Cargo.lock b/Cargo.lock index 8a39e29..321ce58 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -36,7 +36,7 @@ dependencies = [ "accesskit", "accesskit_consumer 0.36.0", "atspi-common", - "phf", + "phf 0.13.1", "serde", "zvariant", ] @@ -1079,6 +1079,21 @@ dependencies = [ "winit", ] +[[package]] +name = "egui_extras" +version = "0.34.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c609fc87f6c70ffd3afd679cbb294985096d2fc0be33e762ad5614bde4925bc" +dependencies = [ + "ahash", + "egui", + "enum-map", + "image", + "log", + "mime_guess2", + "profiling", +] + [[package]] name = "egui_glow" version = "0.34.2" @@ -1117,6 +1132,26 @@ version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "66b7e2430c6dff6a955451e2cfc438f09cea1965a9d6f87f7e3b90decc014099" +[[package]] +name = "enum-map" +version = "2.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6866f3bfdf8207509a033af1a75a7b08abda06bbaaeae6669323fd5a097df2e9" +dependencies = [ + "enum-map-derive", +] + +[[package]] +name = "enum-map-derive" +version = "0.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f282cfdfe92516eb26c2af8589c274c7c17681f5ecc03c18255fe741c6aa64eb" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "enumflags2" version = "0.7.12" @@ -1238,9 +1273,11 @@ dependencies = [ "anyhow", "bytemuck", "eframe", + "egui_extras", "env_logger", "glam", "image", + "serialport", ] [[package]] @@ -1854,6 +1891,16 @@ dependencies = [ "syn", ] +[[package]] +name = "io-kit-sys" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "617ee6cf8e3f66f3b4ea67a4058564628cde41901316e19f559e14c7c72c5e7b" +dependencies = [ + "core-foundation-sys", + "mach2", +] + [[package]] name = "is_terminal_polyfill" version = "1.70.2" @@ -2064,6 +2111,26 @@ dependencies = [ "redox_syscall 0.7.5", ] +[[package]] +name = "libudev" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78b324152da65df7bb95acfcaab55e3097ceaab02fb19b228a9eb74d55f135e0" +dependencies = [ + "libc", + "libudev-sys", +] + +[[package]] +name = "libudev-sys" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c8469b4a23b962c1396b9b451dda50ef5b283e8dd309d69033475fa9b334324" +dependencies = [ + "libc", + "pkg-config", +] + [[package]] name = "linebender_resource_handle" version = "0.1.1" @@ -2118,6 +2185,15 @@ dependencies = [ "imgref", ] +[[package]] +name = "mach2" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d640282b302c0bb0a2a8e0233ead9035e3bed871f0b7e81fe4a1ec829765db44" +dependencies = [ + "libc", +] + [[package]] name = "maybe-rayon" version = "0.1.1" @@ -2152,6 +2228,24 @@ dependencies = [ "autocfg", ] +[[package]] +name = "mime" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" + +[[package]] +name = "mime_guess2" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1706dc14a2e140dec0a7a07109d9a3d5890b81e85bd6c60b906b249a77adf0ca" +dependencies = [ + "mime", + "phf 0.11.3", + "phf_shared 0.11.3", + "unicase", +] + [[package]] name = "miniz_oxide" version = "0.8.9" @@ -2234,6 +2328,17 @@ version = "1.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086" +[[package]] +name = "nix" +version = "0.26.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "598beaf3cc6fdd9a5dfb1630c2800c7acd31df7aaf0f565796fba2b53ca1af1b" +dependencies = [ + "bitflags 1.3.2", + "cfg-if", + "libc", +] + [[package]] name = "no_std_io2" version = "0.9.4" @@ -2756,17 +2861,37 @@ version = "2.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" +[[package]] +name = "phf" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd6780a80ae0c52cc120a26a1a42c1ae51b247a253e4e06113d23d2c2edd078" +dependencies = [ + "phf_macros 0.11.3", + "phf_shared 0.11.3", +] + [[package]] name = "phf" version = "0.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c1562dc717473dbaa4c1f85a36410e03c047b2e7df7f45ee938fbef64ae7fadf" dependencies = [ - "phf_macros", - "phf_shared", + "phf_macros 0.13.1", + "phf_shared 0.13.1", "serde", ] +[[package]] +name = "phf_generator" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c80231409c20246a13fddb31776fb942c38553c51e871f8cbd687a4cfb5843d" +dependencies = [ + "phf_shared 0.11.3", + "rand 0.8.6", +] + [[package]] name = "phf_generator" version = "0.13.1" @@ -2774,7 +2899,21 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "135ace3a761e564ec88c03a77317a7c6b80bb7f7135ef2544dbe054243b89737" dependencies = [ "fastrand", - "phf_shared", + "phf_shared 0.13.1", +] + +[[package]] +name = "phf_macros" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f84ac04429c13a7ff43785d75ad27569f2951ce0ffd30a3321230db2fc727216" +dependencies = [ + "phf_generator 0.11.3", + "phf_shared 0.11.3", + "proc-macro2", + "quote", + "syn", + "unicase", ] [[package]] @@ -2783,13 +2922,23 @@ version = "0.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "812f032b54b1e759ccd5f8b6677695d5268c588701effba24601f6932f8269ef" dependencies = [ - "phf_generator", - "phf_shared", + "phf_generator 0.13.1", + "phf_shared 0.13.1", "proc-macro2", "quote", "syn", ] +[[package]] +name = "phf_shared" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67eabc2ef2a60eb7faa00097bd1ffdb5bd28e62bf39990626a582201b7a754e5" +dependencies = [ + "siphasher", + "unicase", +] + [[package]] name = "phf_shared" version = "0.13.1" @@ -3028,6 +3177,15 @@ version = "6.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" +[[package]] +name = "rand" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ca0ecfa931c29007047d1bc58e623ab12e5590e8c7cc53200d5202b69266d8a" +dependencies = [ + "rand_core 0.6.4", +] + [[package]] name = "rand" version = "0.9.4" @@ -3035,7 +3193,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "44c5af06bb1b7d3216d91932aed5265164bf384dc89cd6ba05cf59a35f5f76ea" dependencies = [ "rand_chacha", - "rand_core", + "rand_core 0.9.5", ] [[package]] @@ -3045,9 +3203,15 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" dependencies = [ "ppv-lite86", - "rand_core", + "rand_core 0.9.5", ] +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" + [[package]] name = "rand_core" version = "0.9.5" @@ -3090,7 +3254,7 @@ dependencies = [ "num-traits", "paste", "profiling", - "rand", + "rand 0.9.4", "rand_chacha", "simd_helpers", "thiserror 2.0.18", @@ -3382,6 +3546,25 @@ dependencies = [ "syn", ] +[[package]] +name = "serialport" +version = "4.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4d91116f97173694f1642263b2ff837f80d933aa837e2314969f6728f661df3" +dependencies = [ + "bitflags 2.11.1", + "cfg-if", + "core-foundation 0.10.1", + "core-foundation-sys", + "io-kit-sys", + "libudev", + "mach2", + "nix", + "scopeguard", + "unescaper", + "windows-sys 0.52.0", +] + [[package]] name = "shlex" version = "1.3.0" @@ -3786,6 +3969,21 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "unescaper" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4064ed685c487dbc25bd3f0e9548f2e34bab9d18cefc700f9ec2dba74ba1138e" +dependencies = [ + "thiserror 2.0.18", +] + +[[package]] +name = "unicase" +version = "2.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dbc4bc3a9f746d862c45cb89d705aa10f187bb96c76001afab07a0d35ce60142" + [[package]] name = "unicode-ident" version = "1.0.24" diff --git a/Cargo.toml b/Cargo.toml index 49dfc44..f080f24 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,3 +10,5 @@ bytemuck = { version = "1", features = ["derive"] } glam = "0.32.1" image = { version = "0.25.10", features = ["png", "jpeg"] } anyhow = "1.0.102" +serialport = "4.9.0" +egui_extras = { version = "0.34.2", features = ["image"] } \ No newline at end of file diff --git a/JE-Skin b/JE-Skin new file mode 160000 index 0000000..59e9203 --- /dev/null +++ b/JE-Skin @@ -0,0 +1 @@ +Subproject commit 59e920336315d61e5dcaa31224db97e491317ce4 diff --git a/eskin-finger-sdk b/eskin-finger-sdk new file mode 160000 index 0000000..7053750 --- /dev/null +++ b/eskin-finger-sdk @@ -0,0 +1 @@ +Subproject commit 705375085f17c79a6fbba32c18fb7630da0b67a7 diff --git a/src/app.rs b/src/app.rs index 37b7535..d697dea 100644 --- a/src/app.rs +++ b/src/app.rs @@ -2,22 +2,30 @@ use std::time::Instant; use eframe::{egui, egui_wgpu}; -use crate::theme::{ENGINEERING_DARK, apply_theme}; +use crate::theme::{ENGINEERING_DARK, apply_fonts, apply_theme}; use crate::{ matrix::{MATRIX_COLS, MATRIX_ROWS}, render::{BackgroundRenderResources, WgpuBackgroundCallback}, - ui::{FloatingPanelState, draw_config_panel, draw_scene_panel, draw_stats_panel}, + ui::{ + ConfigPanelState, ConnectPanelState, FloatingPanelState, draw_config_panel, + draw_connect_panel, draw_scene_panel, draw_stats_panel, + }, }; pub struct EskinDesktopApp { + connect_panel: FloatingPanelState, + connect_state: ConnectPanelState, scene_panel: FloatingPanelState, config_panel: FloatingPanelState, + config_state: ConfigPanelState, stats_panel: FloatingPanelState, started_at: Instant, } impl EskinDesktopApp { pub fn new(cc: &eframe::CreationContext<'_>) -> Self { + egui_extras::install_image_loaders(&cc.egui_ctx); + apply_fonts(&cc.egui_ctx); apply_theme(&cc.egui_ctx, &ENGINEERING_DARK); let wgpu_state = cc @@ -36,8 +44,11 @@ impl EskinDesktopApp { )); Self { + connect_panel: FloatingPanelState::new([0.0, 0.0], [0.0, 0.0]), + connect_state: ConnectPanelState::default(), scene_panel: FloatingPanelState::new([16.0, 48.0], [16.0, 48.0]), config_panel: FloatingPanelState::new([840.0, 48.0], [128.0, 48.0]), + config_state: ConfigPanelState::default(), stats_panel: FloatingPanelState::new([16.0, 520.0], [240.0, 48.0]), started_at: Instant::now(), } @@ -61,16 +72,18 @@ impl EskinDesktopApp { fn draw_toolbar(&mut self, ui: &mut egui::Ui) { egui::Panel::top("main_menu").show_inside(ui, |ui| { ui.horizontal(|ui| { - ui.checkbox(&mut self.scene_panel.visible, "Scene"); - ui.checkbox(&mut self.config_panel.visible, "Config"); - ui.checkbox(&mut self.stats_panel.visible, "Stats"); + ui.checkbox(&mut self.connect_panel.visible, "连接"); + ui.checkbox(&mut self.scene_panel.visible, "场景"); + ui.checkbox(&mut self.config_panel.visible, "配置"); + ui.checkbox(&mut self.stats_panel.visible, "统计"); }); }); } fn draw_floating_panels(&mut self, ctx: &egui::Context) { + draw_connect_panel(ctx, &mut self.connect_panel, &mut self.connect_state); draw_scene_panel(ctx, &mut self.scene_panel); - draw_config_panel(ctx, &mut self.config_panel); + draw_config_panel(ctx, &mut self.config_panel, &mut self.config_state); draw_stats_panel(ctx, &mut self.stats_panel); } } diff --git a/src/main.rs b/src/main.rs index c99241c..7257c48 100644 --- a/src/main.rs +++ b/src/main.rs @@ -3,19 +3,23 @@ mod matrix; mod render; mod theme; mod ui; - +mod utils; use app::EskinDesktopApp; +use eframe::egui; fn main() -> eframe::Result<()> { env_logger::init(); let options = eframe::NativeOptions { renderer: eframe::Renderer::Wgpu, + viewport: egui::ViewportBuilder::default() + .with_inner_size([1920.0, 1080.0]) + .with_min_inner_size([1280.0, 720.0]), ..Default::default() }; eframe::run_native( - "Eskin Model Player", + "Eskin 模型播放器", options, Box::new(|cc| Ok(Box::new(EskinDesktopApp::new(cc)))), ) diff --git a/src/theme.rs b/src/theme.rs index 4293769..718dda9 100644 --- a/src/theme.rs +++ b/src/theme.rs @@ -22,8 +22,8 @@ pub const ENGINEERING_DARK: AppTheme = AppTheme { panel_deep: egui::Color32::from_rgb(14, 18, 24), border: egui::Color32::from_rgb(53, 75, 92), border_soft: egui::Color32::from_rgb(36, 53, 66), - text: egui::Color32::from_rgb(210, 218, 232), - text_dim: egui::Color32::from_rgb(155, 168, 190), + text: egui::Color32::from_rgb(242, 246, 252), + text_dim: egui::Color32::from_rgb(206, 216, 230), accent: egui::Color32::from_rgb(255, 118, 47), accent_hot: egui::Color32::from_rgb(255, 169, 77), radius: 2, @@ -43,7 +43,7 @@ pub fn apply_theme(ctx: &egui::Context, theme: &AppTheme) { visuals.widgets.noninteractive.bg_fill = theme.panel_strong; visuals.widgets.noninteractive.bg_stroke = egui::Stroke::new(1.0, theme.border_soft); - visuals.widgets.noninteractive.fg_stroke = egui::Stroke::new(1.0, theme.text_dim); + visuals.widgets.noninteractive.fg_stroke = egui::Stroke::new(1.0, theme.text); visuals.widgets.inactive.bg_fill = theme.panel_strong; visuals.widgets.inactive.bg_stroke = egui::Stroke::new(1.0, theme.border_soft); visuals.widgets.inactive.fg_stroke = egui::Stroke::new(1.0, theme.text); @@ -76,6 +76,46 @@ pub fn apply_theme(ctx: &egui::Context, theme: &AppTheme) { ctx.set_global_style(style); } +pub fn apply_fonts(ctx: &egui::Context) { + let mut fonts = egui::FontDefinitions::default(); + + fonts.font_data.insert( + "Hack-Bold".to_owned(), + egui::FontData::from_static(include_bytes!("../static/Hack-Bold.ttf")).into(), + ); + + if let Ok(font_data) = std::fs::read(r"C:\Windows\Fonts\msyh.ttc") { + fonts.font_data.insert( + "Microsoft-YaHei".to_owned(), + egui::FontData::from_owned(font_data).into(), + ); + } + + fonts + .families + .entry(egui::FontFamily::Proportional) + .or_default() + .insert(0, "Hack-Bold".to_owned()); + fonts + .families + .entry(egui::FontFamily::Proportional) + .or_default() + .push("Microsoft-YaHei".to_owned()); + + fonts + .families + .entry(egui::FontFamily::Monospace) + .or_default() + .insert(0, "Hack-Bold".to_owned()); + fonts + .families + .entry(egui::FontFamily::Monospace) + .or_default() + .push("Microsoft-YaHei".to_owned()); + + ctx.set_fonts(fonts); +} + pub fn panel_frame(ctx: &egui::Context) -> egui::Frame { let style = ctx.global_style(); egui::Frame::window(&style) diff --git a/src/ui.rs b/src/ui.rs index 949505d..f14ec64 100644 --- a/src/ui.rs +++ b/src/ui.rs @@ -1,6 +1,9 @@ use eframe::egui; -use crate::theme::{accent_text, dim_text, group_frame, panel_frame, tag_button}; +use crate::{ + theme::{ENGINEERING_DARK, accent_text, dim_text, group_frame, panel_frame, tag_button}, + utils::serial_enum, +}; pub struct FloatingPanelState { pub visible: bool, @@ -8,6 +11,50 @@ pub struct FloatingPanelState { tag_pos: egui::Pos2, } +pub struct ConfigPanelState { + pub mode: SerialMode, + pub port: String, + pub baud_rate: u32, + pub data_bits: u8, + pub stop_bits: u8, + pub parity: Parity, + pub timeout_ms: u32, + pub module_addr: u8, + pub connected: bool, + pub auto_reconnect: bool, + pub manual_tx: String, + pub model_path: String, +} + +pub struct ConnectPanelState { + pub mode: SerialMode, + pub port: Vec, + pub duration: u8, + pub manual: bool, + pub rows: u8, + pub cols: u8, + pub connection: bool, +} + +#[derive(Clone, Copy, PartialEq, Eq)] +pub enum SerialMode { + SingleModule, + Manual, + Model, +} + +#[derive(Clone, Copy, PartialEq, Eq)] +pub enum Parity { + None, + Odd, + Even, +} + +pub enum IconButtonIcon<'a> { + Font(&'a str), + Png(egui::ImageSource<'a>), +} + impl FloatingPanelState { pub fn new(default_pos: [f32; 2], tag_pos: [f32; 2]) -> Self { Self { @@ -18,49 +65,429 @@ impl FloatingPanelState { } } +impl Default for ConfigPanelState { + fn default() -> Self { + Self { + mode: SerialMode::SingleModule, + port: "COM3".to_owned(), + baud_rate: 115_200, + data_bits: 8, + stop_bits: 1, + parity: Parity::None, + timeout_ms: 1000, + module_addr: 1, + connected: false, + auto_reconnect: true, + manual_tx: "01 03 00 00 00 02".to_owned(), + model_path: "model/default.eskin".to_owned(), + } + } +} + +impl Default for ConnectPanelState { + fn default() -> Self { + let port = serial_enum().unwrap(); + Self { + mode: SerialMode::SingleModule, + port, + duration: 10, + manual: false, + rows: 12, + cols: 7, + connection: false, + } + } +} + pub fn draw_scene_panel(ctx: &egui::Context, panel: &mut FloatingPanelState) { - draw_floating_panel(ctx, panel, "Scene", "scene_panel", |ui| { + draw_floating_panel(ctx, panel, "场景", "scene_panel", |ui| { ui.horizontal(|ui| { - ui.colored_label(dim_text(), "View"); - let _ = ui.selectable_label(true, "Clusters"); - let _ = ui.selectable_label(false, "Triangles"); + ui.colored_label(dim_text(), "视图"); + let _ = ui.selectable_label(true, "簇"); + let _ = ui.selectable_label(false, "三角形"); }); ui.separator(); group_frame().show(ui, |ui| { - ui.label("Models / materials / lights"); - ui.label("target tasks 64"); - ui.label("slack cache 100.0%"); + ui.label("模型 / 材质 / 灯光"); + ui.label("目标任务 64"); + ui.label("缓存命中 100.0%"); }); }); } -pub fn draw_config_panel(ctx: &egui::Context, panel: &mut FloatingPanelState) { - draw_floating_panel(ctx, panel, "Config", "config_panel", |ui| { - ui.horizontal(|ui| { - ui.label("Shader"); - let _ = ui.selectable_label(true, "Clusters"); - let _ = ui.selectable_label(false, "wireframe"); - }); - ui.horizontal(|ui| { - ui.label("Grid"); - ui.label("x 32"); - ui.label("z 32"); - ui.label("side 1.000"); +pub fn draw_connect_panel( + ctx: &egui::Context, + panel: &mut FloatingPanelState, + config: &mut ConnectPanelState, +) { + draw_center_floating_panel(ctx, panel, "connect_center_panel", 64.0, |ui| { + ui.set_min_width(320.0); + + ui.vertical(|ui| { + // ui.horizontal_centered(|ui| { + // mode_button(ui, &mut config.mode, SerialMode::SingleModule, "单模块"); + // ui.add_space(8.0); + // mode_button(ui, &mut config.mode, SerialMode::Manual, "全手"); + // ui.add_space(8.0); + // mode_button(ui, &mut config.mode, SerialMode::Model, "模型"); + // }); + + let button_width = 96.0; + let gap = 8.0; + let total_width = button_width * 3.0 + gap * 2.0; + let left_space = ((ui.available_width() - total_width) * 0.5).max(0.0); + + ui.horizontal(|ui| { + ui.add_space(left_space); + + mode_button(ui, &mut config.mode, SerialMode::SingleModule, "单模块"); + ui.add_space(gap); + mode_button(ui, &mut config.mode, SerialMode::Manual, "全手"); + ui.add_space(gap); + mode_button(ui, &mut config.mode, SerialMode::Model, "模型"); + }); + + ui.add_space(6.0); + + ui.horizontal(|ui| { + ui.add_space(10.0); + ui.add( + egui::Image::new(egui::include_image!("../static/cpu.png")) + .fit_to_exact_size(egui::vec2(72.0, 72.0)), + ); + + ui.vertical(|ui| { + ui.horizontal(|ui| { + ui.colored_label(ENGINEERING_DARK.text, "串口"); + egui::ComboBox::from_id_salt("connect_ports") + .width(130.0) + .selected_text( + config + .port + .first() + .map(String::as_str) + .unwrap_or("无可用串口"), + ) + .show_ui(ui, |ui| { + for port in &config.port { + let _ = ui.selectable_label(false, port); + } + }); + ui.add_space(10.0); + + ui.label("频率"); + ui.add_sized( + egui::vec2(72.0, 20.0), + egui::DragValue::new(&mut config.duration).range(1..=120), + ); + }); + + ui.add_space(10.0); + ui.horizontal(|ui| { + ui.checkbox(&mut config.manual, "手动"); + + ui.add_enabled_ui(config.manual, |ui| { + ui.horizontal(|ui| { + ui.colored_label(ENGINEERING_DARK.text_dim, "行"); + ui.add_sized( + egui::vec2(48.0, 20.0), + egui::DragValue::new(&mut config.rows).range(1..=64), + ); + ui.colored_label(ENGINEERING_DARK.text_dim, "列"); + ui.add_sized( + egui::vec2(48.0, 20.0), + egui::DragValue::new(&mut config.cols).range(1..=64), + ); + }); + }); + }); + }) + }); }); }); } +pub fn draw_config_panel( + ctx: &egui::Context, + panel: &mut FloatingPanelState, + config: &mut ConfigPanelState, +) { + draw_floating_panel(ctx, panel, "配置", "config_panel", |ui| { + ui.set_min_width(560.0); + + draw_mode_row(ui, config); + ui.separator(); + draw_connection_row(ui, config); + ui.add_space(8.0); + draw_serial_grid(ui, config); + ui.add_space(8.0); + draw_mode_body(ui, config); + }); +} + +fn draw_mode_row(ui: &mut egui::Ui, config: &mut ConfigPanelState) { + ui.horizontal(|ui| { + ui.colored_label(dim_text(), "模式"); + ui.add_space(12.0); + + mode_button(ui, &mut config.mode, SerialMode::SingleModule, "单模块"); + mode_button(ui, &mut config.mode, SerialMode::Manual, "全手"); + mode_button(ui, &mut config.mode, SerialMode::Model, "模型"); + + ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| { + ui.checkbox(&mut config.auto_reconnect, "自动"); + ui.colored_label(dim_text(), "重连"); + }); + }); +} + +fn draw_connection_row(ui: &mut egui::Ui, config: &mut ConfigPanelState) { + ui.horizontal(|ui| { + ui.add( + egui::Image::new(egui::include_image!("../static/cpu.png")) + .fit_to_exact_size(egui::vec2(72.0, 72.0)), + ); + + ui.add_space(10.0); + + ui.vertical(|ui| { + ui.label(format!("端口 {}", config.port)); + ui.label(format!("波特率 {}", config.baud_rate)); + let status = if config.connected { + "已连接" + } else { + "未连接" + }; + let status_color = if config.connected { + egui::Color32::from_rgb(158, 184, 101) + } else { + egui::Color32::from_rgb(255, 98, 82) + }; + ui.colored_label(status_color, status); + }); + + ui.add_space(22.0); + + let button_text = if config.connected { "断开" } else { "连接" }; + if ui + .add( + egui::Button::new(button_text) + .fill(ENGINEERING_DARK.accent) + .stroke(egui::Stroke::new(1.0, ENGINEERING_DARK.accent_hot)) + .min_size(egui::vec2(120.0, 30.0)), + ) + .clicked() + { + config.connected = !config.connected; + } + + ui.add_space(18.0); + ui.colored_label( + egui::Color32::from_rgb(158, 184, 101), + if config.auto_reconnect { + "链路保护 开" + } else { + "链路保护 关" + }, + ); + }); +} + +fn draw_serial_grid(ui: &mut egui::Ui, config: &mut ConfigPanelState) { + group_frame().show(ui, |ui| { + egui::Grid::new("serial_config_grid") + .num_columns(4) + .spacing(egui::vec2(10.0, 5.0)) + .striped(true) + .show(ui, |ui| { + ui.label("端口"); + ui.add_sized( + egui::vec2(110.0, 20.0), + egui::TextEdit::singleline(&mut config.port), + ); + + ui.label("波特率"); + baud_combo(ui, config); + ui.end_row(); + + ui.label("数据位"); + ui.add_sized( + egui::vec2(70.0, 20.0), + egui::DragValue::new(&mut config.data_bits).range(5..=8), + ); + + ui.label("校验"); + parity_combo(ui, config); + ui.end_row(); + + ui.label("停止位"); + ui.add_sized( + egui::vec2(70.0, 20.0), + egui::DragValue::new(&mut config.stop_bits).range(1..=2), + ); + + ui.label("超时"); + ui.horizontal(|ui| { + ui.add_sized( + egui::vec2(84.0, 20.0), + egui::DragValue::new(&mut config.timeout_ms) + .range(50..=30_000) + .speed(50), + ); + ui.colored_label(dim_text(), "毫秒"); + }); + ui.end_row(); + }); + }); +} + +fn draw_mode_body(ui: &mut egui::Ui, config: &mut ConfigPanelState) { + group_frame().show(ui, |ui| match config.mode { + SerialMode::SingleModule => { + ui.horizontal(|ui| { + ui.label("模块地址"); + ui.add_sized( + egui::vec2(80.0, 20.0), + egui::DragValue::new(&mut config.module_addr).range(1..=247), + ); + ui.add_space(16.0); + if ui.add(tag_button("读取信息")).clicked() {} + if ui.add(tag_button("探测")).clicked() {} + }); + ui.separator(); + ui.horizontal(|ui| { + ui.colored_label(dim_text(), "状态"); + ui.label("就绪"); + ui.colored_label(dim_text(), "接收"); + ui.label("0 字节"); + ui.colored_label(dim_text(), "发送"); + ui.label("0 字节"); + }); + } + SerialMode::Manual => { + ui.horizontal(|ui| { + ui.label("发送"); + ui.add_sized( + egui::vec2(300.0, 20.0), + egui::TextEdit::singleline(&mut config.manual_tx), + ); + if ui.add(tag_button("发送")).clicked() {} + if ui.add(tag_button("清空")).clicked() { + config.manual_tx.clear(); + } + }); + } + SerialMode::Model => { + ui.horizontal(|ui| { + ui.label("模型"); + ui.add_sized( + egui::vec2(300.0, 20.0), + egui::TextEdit::singleline(&mut config.model_path), + ); + if ui.add(tag_button("加载")).clicked() {} + if ui.add(tag_button("运行")).clicked() {} + }); + } + }); +} + +pub fn icon_button<'a>( + ui: &mut egui::Ui, + icon: IconButtonIcon<'a>, + tooltip: impl Into, +) -> egui::Response { + icon_button_sized(ui, icon, tooltip, egui::vec2(28.0, 24.0)) +} + +pub fn icon_button_sized<'a>( + ui: &mut egui::Ui, + icon: IconButtonIcon<'a>, + tooltip: impl Into, + size: egui::Vec2, +) -> egui::Response { + let button = match icon { + IconButtonIcon::Font(icon) => egui::Button::new( + egui::RichText::new(icon) + .color(egui::Color32::WHITE) + .size(size.y - 8.0), + ), + IconButtonIcon::Png(source) => egui::Button::image( + egui::Image::new(source).fit_to_exact_size(egui::vec2(size.y - 8.0, size.y - 8.0)), + ), + } + .fill(ENGINEERING_DARK.panel_strong) + .stroke(egui::Stroke::new(1.0, ENGINEERING_DARK.border)) + .corner_radius(egui::CornerRadius::same(2)) + .min_size(size); + + ui.add(button).on_hover_text(tooltip) +} + +fn mode_button(ui: &mut egui::Ui, mode: &mut SerialMode, value: SerialMode, label: &'static str) { + let selected = *mode == value; + let fill = if selected { + ENGINEERING_DARK.accent + } else { + ENGINEERING_DARK.panel_strong + }; + let stroke = if selected { + ENGINEERING_DARK.accent_hot + } else { + ENGINEERING_DARK.border_soft + }; + + if ui + .add( + egui::Button::new(egui::RichText::new(label).color(egui::Color32::WHITE)) + .fill(fill) + .stroke(egui::Stroke::new(1.0, stroke)) + .min_size(egui::vec2(96.0, 24.0)), + ) + .clicked() + { + *mode = value; + } +} + +fn baud_combo(ui: &mut egui::Ui, config: &mut ConfigPanelState) { + egui::ComboBox::from_id_salt("serial_baud_rate") + .width(110.0) + .selected_text(config.baud_rate.to_string()) + .show_ui(ui, |ui| { + for baud_rate in [ + 9_600, 19_200, 38_400, 57_600, 115_200, 230_400, 460_800, 921_600, + ] { + ui.selectable_value(&mut config.baud_rate, baud_rate, baud_rate.to_string()); + } + }); +} + +fn parity_combo(ui: &mut egui::Ui, config: &mut ConfigPanelState) { + egui::ComboBox::from_id_salt("serial_parity") + .width(110.0) + .selected_text(match config.parity { + Parity::None => "无", + Parity::Odd => "奇", + Parity::Even => "偶", + }) + .show_ui(ui, |ui| { + ui.selectable_value(&mut config.parity, Parity::None, "无"); + ui.selectable_value(&mut config.parity, Parity::Odd, "奇"); + ui.selectable_value(&mut config.parity, Parity::Even, "偶"); + }); +} + pub fn draw_stats_panel(ctx: &egui::Context, panel: &mut FloatingPanelState) { - draw_floating_panel(ctx, panel, "Stats", "stats_panel", |ui| { + draw_floating_panel(ctx, panel, "统计", "stats_panel", |ui| { ui.horizontal(|ui| { ui.colored_label(accent_text(), "0.030"); ui.label("81m:51s"); }); ui.separator(); group_frame().show(ui, |ui| { - ui.label("FPS / GPU info"); - ui.label("bounds 589.0us"); - ui.label("clusters 12.8ms"); + ui.label("帧率 / GPU 信息"); + ui.label("边界 589.0us"); + ui.label("簇 12.8ms"); }); }); } @@ -75,8 +502,9 @@ fn draw_floating_panel( if panel.visible { let mut open = true; let mut hide_requested = false; + let mut window_rect = None; - egui::Window::new(title) + let window_response = egui::Window::new(title) .id(egui::Id::new(id)) .open(&mut open) .default_pos(panel.default_pos) @@ -85,7 +513,7 @@ fn draw_floating_panel( .frame(panel_frame(ctx)) .show(ctx, |ui| { ui.horizontal(|ui| { - if ui.add(tag_button("Hide")).clicked() { + if ui.add(tag_button("隐藏")).clicked() { hide_requested = true; } ui.add_space(6.0); @@ -95,6 +523,29 @@ fn draw_floating_panel( add_contents(ui); }); + if let Some(response) = window_response { + window_rect = Some(response.response.rect); + } + + if hide_requested { + if let Some(rect) = window_rect { + let screen = ctx.content_rect(); + let tag_size = egui::vec2(86.0, 22.0); + + let distance_to_left = rect.left(); + let distance_to_right = screen.right() - rect.right(); + + let x = if distance_to_left <= distance_to_right { + screen.left() + } else { + screen.right() - tag_size.x + }; + + let y = rect.top().clamp(screen.top(), screen.bottom() - tag_size.y); + + panel.tag_pos = egui::pos2(x, y); + } + } panel.visible = open && !hide_requested; } else { let response = egui::Area::new(egui::Id::new(format!("{id}_tag"))) @@ -114,3 +565,24 @@ fn draw_floating_panel( panel.tag_pos = response.response.rect.min; } } + +fn draw_center_floating_panel( + ctx: &egui::Context, + panel: &mut FloatingPanelState, + id: &'static str, + top_offset: f32, + add_contents: impl FnOnce(&mut egui::Ui), +) { + if !panel.visible { + return; + } + + egui::Area::new(egui::Id::new(id)) + .anchor(egui::Align2::CENTER_TOP, egui::vec2(0.0, top_offset)) + .order(egui::Order::Foreground) + .show(ctx, |ui| { + panel_frame(ctx).show(ui, |ui| { + add_contents(ui); + }); + }); +} diff --git a/src/utils.rs b/src/utils.rs new file mode 100644 index 0000000..2069439 --- /dev/null +++ b/src/utils.rs @@ -0,0 +1,11 @@ +use anyhow; +use serialport::available_ports; + +pub fn serial_enum() -> anyhow::Result> { + let ports = available_ports() + .map_err(|e| anyhow::anyhow!("available_ports failed: {}", e))? + .into_iter() + .map(|info| info.port_name) + .collect(); + Ok(ports) +} diff --git a/static/Hack-Bold.ttf b/static/Hack-Bold.ttf new file mode 100644 index 0000000..7ff4975 Binary files /dev/null and b/static/Hack-Bold.ttf differ diff --git a/static/cpu.png b/static/cpu.png new file mode 100644 index 0000000..11b3304 Binary files /dev/null and b/static/cpu.png differ