589 lines
18 KiB
Rust
589 lines
18 KiB
Rust
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<String>,
|
|
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::WidgetText>,
|
|
) -> 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<egui::WidgetText>,
|
|
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);
|
|
});
|
|
});
|
|
}
|