feat: current progress - connect panel layout, manual checkbox, serial enum

This commit is contained in:
lennlouisgeek
2026-05-19 23:44:09 +08:00
parent 83faa0be1e
commit a7b617419d
11 changed files with 788 additions and 46 deletions

View File

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

View File

@@ -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)))),
)

View File

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

524
src/ui.rs
View File

@@ -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<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 {
@@ -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::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", "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);
});
});
}

11
src/utils.rs Normal file
View File

@@ -0,0 +1,11 @@
use anyhow;
use serialport::available_ports;
pub fn serial_enum() -> anyhow::Result<Vec<String>> {
let ports = available_ports()
.map_err(|e| anyhow::anyhow!("available_ports failed: {}", e))?
.into_iter()
.map(|info| info.port_name)
.collect();
Ok(ports)
}