use eframe::egui; use crate::{ theme::{ENGINEERING_DARK, accent_text, dim_text, group_frame, panel_frame, tag_button}, utils::serial_enum, }; pub struct FloatingPanelState { pub visible: bool, default_pos: egui::Pos2, 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 { visible: true, default_pos: egui::pos2(default_pos[0], default_pos[1]), tag_pos: egui::pos2(tag_pos[0], tag_pos[1]), } } } 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_panel", |ui| { ui.horizontal(|ui| { ui.colored_label(dim_text(), "视图"); let _ = ui.selectable_label(true, "簇"); let _ = ui.selectable_label(false, "三角形"); }); ui.separator(); group_frame().show(ui, |ui| { ui.label("模型 / 材质 / 灯光"); ui.label("目标任务 64"); ui.label("缓存命中 100.0%"); }); }); } 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_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("帧率 / GPU 信息"); ui.label("边界 589.0us"); ui.label("簇 12.8ms"); }); }); } fn draw_floating_panel( ctx: &egui::Context, panel: &mut FloatingPanelState, title: &'static str, id: &'static str, add_contents: impl FnOnce(&mut egui::Ui), ) { if panel.visible { let mut open = true; let mut hide_requested = false; let mut window_rect = None; let window_response = egui::Window::new(title) .id(egui::Id::new(id)) .open(&mut open) .default_pos(panel.default_pos) .title_bar(false) .resizable(true) .frame(panel_frame(ctx)) .show(ctx, |ui| { ui.horizontal(|ui| { if ui.add(tag_button("隐藏")).clicked() { hide_requested = true; } ui.add_space(6.0); ui.colored_label(dim_text(), title); }); ui.separator(); 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"))) .current_pos(panel.tag_pos) .movable(true) .order(egui::Order::Foreground) .show(ctx, |ui| { ui.set_min_width(86.0); if ui .add(tag_button(format!("▸ {title}")).min_size(egui::vec2(86.0, 22.0))) .clicked() { panel.visible = true; } }); 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); }); }); }