Refine UI panels and render mode flow

This commit is contained in:
lennlouisgeek
2026-06-26 02:03:07 +08:00
parent 156df50d33
commit 0b658d11c5
8 changed files with 1375687 additions and 345 deletions

View File

@@ -1,16 +1,17 @@
use crate::connection::ConnectionManager;
use crate::recording::Recorder;
use crate::theme::{ONE_DARK_PRO, apply_fonts, apply_theme};
use crate::render::{ActiveMode, FingerMode, HandGatewayMode};
use crate::style::{ONE_DARK_PRO, apply_fonts, apply_theme, layout};
use crate::ui::SerialMode;
use crate::{
matrix::{MATRIX_COLS, MATRIX_ROWS},
render::{
BackgroundRenderResources, MarkerMode, PRESSURE_CELL_COUNT, PressureFrame,
WgpuBackgroundCallback,
BackgroundRenderResources, PRESSURE_CELL_COUNT, PressureFrame, WgpuBackgroundCallback,
},
ui::{
ConfigPanelState, ConnectPanelState, FloatingPanelState, MatrixConfigState,
draw_config_panel, draw_connect_panel, draw_export_panel, draw_matrix_config_panel,
draw_scene_panel, draw_stats_panel,
draw_stats_panel, panel_restore_item,
},
};
use eframe::{egui, egui_wgpu};
@@ -35,6 +36,7 @@ pub struct EskinDesktopApp {
matrix_config_panel: FloatingPanelState,
matrix_config: MatrixConfigState,
signal_history: Vec<f32>,
active_mode: ActiveMode,
}
impl EskinDesktopApp {
@@ -60,21 +62,42 @@ impl EskinDesktopApp {
));
Self {
connect_panel: FloatingPanelState::new([0.0, 0.0], [0.0, 0.0]),
connect_panel: FloatingPanelState::new([0.0, 0.0], [layout::RIGHT_TAG_X, 48.0]),
connect_state: ConnectPanelState::default(),
connection: Arc::new(ConnectionManager::new()),
pressure_matrix: [[0.0, 0.0]; PRESSURE_CELL_COUNT],
data_log_frame: 0,
scene_panel: FloatingPanelState::new([16.0, 48.0], [16.0, 48.0]),
config_panel: FloatingPanelState::new([840.0, 48.0], [128.0, 48.0]),
scene_panel: FloatingPanelState::new(
[layout::LEFT_X, layout::TOP_Y],
[layout::LEFT_TAG_X, layout::TOP_Y],
),
config_panel: FloatingPanelState::new(
[layout::RIGHT_X, layout::TOP_Y],
[layout::RIGHT_TAG_X, layout::TOP_Y],
),
config_state: ConfigPanelState::default(),
stats_panel: FloatingPanelState::new([16.0, 520.0], [240.0, 48.0]),
stats_panel: FloatingPanelState::new(
[layout::LEFT_X, layout::LEFT_SECOND_Y],
[layout::LEFT_TAG_X, layout::LEFT_SECOND_Y],
),
recorder: Recorder::full(),
export_panel: FloatingPanelState::new([16.0, 280.0], [16.0, 280.0]),
export_panel: FloatingPanelState::new(
[layout::LEFT_X, layout::TOP_Y],
[layout::LEFT_TAG_X, layout::TOP_Y],
),
export_path: String::new(),
matrix_config_panel: FloatingPanelState::new([840.0, 280.0], [400.0, 48.0]),
matrix_config_panel: FloatingPanelState::new(
[layout::RIGHT_X, layout::RIGHT_SECOND_Y],
[layout::RIGHT_TAG_X, layout::RIGHT_SECOND_Y],
),
matrix_config: MatrixConfigState::default(),
signal_history: Vec::with_capacity(128),
active_mode: ActiveMode::Finger(FingerMode {
rows: 12,
cols: 7,
range: 0..7000,
dot: true,
}),
}
}
@@ -91,7 +114,7 @@ impl EskinDesktopApp {
width,
height,
pressure: self.pressure_matrix,
marker_mode: MarkerMode::Number,
active_mode: self.active_mode.clone(),
},
));
}
@@ -123,7 +146,7 @@ impl EskinDesktopApp {
}
fn draw_title_bar(&mut self, ui: &mut egui::Ui, _frame: &mut eframe::Frame) {
let title_bar_height = 36.0;
let title_bar_height = layout::TITLE_BAR_HEIGHT;
let title_bar_rect = ui
.allocate_space(egui::vec2(ui.available_width(), title_bar_height))
.1;
@@ -242,8 +265,12 @@ impl EskinDesktopApp {
&self.recorder,
&mut self.export_path,
);
draw_scene_panel(ctx, &mut self.scene_panel);
draw_config_panel(ctx, &mut self.config_panel, &mut self.config_state);
// draw_scene_panel(ctx, &mut self.scene_panel);
if let Some(next_mode) =
draw_config_panel(ctx, &mut self.config_panel, &mut self.config_state)
{
self.switch_mode(next_mode);
}
draw_stats_panel(ctx, &mut self.stats_panel);
draw_export_panel(
ctx,
@@ -253,6 +280,54 @@ impl EskinDesktopApp {
);
draw_matrix_config_panel(ctx, &mut self.matrix_config_panel, &mut self.matrix_config);
}
fn draw_panel_context_menu(&mut self, ui: &mut egui::Ui) {
let response = ui.interact(
ui.max_rect(),
egui::Id::new("workspace_panel_context_menu"),
egui::Sense::click(),
);
response.context_menu(|ui| {
ui.label("显示面板");
ui.separator();
panel_restore_item(ui, "连接", &mut self.connect_panel);
panel_restore_item(ui, "配置", &mut self.config_panel);
panel_restore_item(ui, "录制", &mut self.export_panel);
panel_restore_item(ui, "矩阵", &mut self.matrix_config_panel);
panel_restore_item(ui, "统计", &mut self.stats_panel);
ui.separator();
if ui.button("全部显示").clicked() {
self.connect_panel.visible = true;
self.config_panel.visible = true;
self.export_panel.visible = true;
self.matrix_config_panel.visible = true;
self.stats_panel.visible = true;
ui.close();
}
});
}
fn switch_mode(&mut self, next: SerialMode) {
self.connect_state.mode = next;
self.config_state.mode = next;
self.connection.disconnect();
self.pressure_matrix.fill([0.0, 0.0]);
self.data_log_frame = 0;
self.active_mode = match next {
SerialMode::Finger => ActiveMode::Finger(FingerMode {
rows: self.matrix_config.rows,
cols: self.matrix_config.cols,
range: 0..7000,
dot: true,
}),
SerialMode::Hand => ActiveMode::Hand(HandGatewayMode { range: 0..7000 }),
}
}
}
fn log_pressure_sample(raw: &[u32], rows: u32, cols: u32) {
@@ -305,6 +380,7 @@ impl eframe::App for EskinDesktopApp {
let ctx = ui.ctx().clone();
self.draw_wgpu_background(ui);
self.draw_panel_context_menu(ui);
self.draw_title_bar(ui, frame);
self.draw_floating_panels(&ctx);

View File

@@ -6,8 +6,8 @@ mod recording;
mod render;
mod resources;
mod serial_core;
mod style;
mod texture;
mod theme;
mod ui;
mod utils;
use app::EskinDesktopApp;

View File

@@ -8,6 +8,7 @@ use eframe::{
egui_wgpu::{self, wgpu},
wgpu::util::DeviceExt,
};
use std::ops::Range;
pub const PRESSURE_CELL_COUNT: usize =
(crate::matrix::MATRIX_ROWS * crate::matrix::MATRIX_COLS) as usize;
@@ -17,13 +18,26 @@ pub struct WgpuBackgroundCallback {
pub width: f32,
pub height: f32,
pub pressure: PressureFrame,
pub marker_mode: MarkerMode,
pub active_mode: ActiveMode,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum MarkerMode {
Number,
Dot,
#[derive(Clone, PartialEq, Eq, Debug)]
pub enum ActiveMode {
Finger(FingerMode),
Hand(HandGatewayMode),
}
#[derive(Clone, PartialEq, Eq, Debug)]
pub struct FingerMode {
pub rows: u32,
pub cols: u32,
pub range: Range<u32>,
pub dot: bool,
}
#[derive(Clone, PartialEq, Eq, Debug)]
pub struct HandGatewayMode {
pub range: Range<u32>,
}
impl egui_wgpu::CallbackTrait for WgpuBackgroundCallback {
@@ -36,14 +50,7 @@ impl egui_wgpu::CallbackTrait for WgpuBackgroundCallback {
resources: &mut egui_wgpu::CallbackResources,
) -> Vec<wgpu::CommandBuffer> {
let resources: &mut BackgroundRenderResources = resources.get_mut().unwrap();
resources.prepare(
queue,
self.width,
self.height,
&self.pressure,
self.marker_mode,
);
resources.prepare(queue, self.width, self.height, &self.pressure);
Vec::new()
}
@@ -54,7 +61,7 @@ impl egui_wgpu::CallbackTrait for WgpuBackgroundCallback {
resources: &egui_wgpu::CallbackResources,
) {
let resources: &BackgroundRenderResources = resources.get().unwrap();
resources.paint(render_pass);
resources.paint(render_pass, &self.active_mode);
}
}
@@ -65,16 +72,17 @@ pub struct BackgroundRenderResources {
uniform: MatrixUniform,
uniform_buffer: wgpu::Buffer,
uniform_bind_group: wgpu::BindGroup,
background_pipeline: wgpu::RenderPipeline,
glyph_pipeline: wgpu::RenderPipeline,
dot_pipeline: wgpu::RenderPipeline,
model_pipeline: wgpu::RenderPipeline,
model: Option<Model>,
model_instance_buffer: wgpu::Buffer,
glyph_vertex_buffer: wgpu::Buffer,
glyph_instance_buffer: wgpu::Buffer,
glyph_instances: Vec<GlyphInstance>,
marker_mode: MarkerMode,
}
impl BackgroundRenderResources {
@@ -180,10 +188,10 @@ impl BackgroundRenderResources {
create_model_pipeline(device, target_format, &shader, &model_pipeline_layout);
let model =
match resources::load_model("cube.obj", device, _queue, &texture_bind_group_layout) {
match resources::load_model("hand.obj", device, _queue, &texture_bind_group_layout) {
Ok(model) => Some(model),
Err(err) => {
log::warn!("failed to load cube.obj: {err:#}");
log::warn!("failed to load hand.obj: {err:#}");
None
}
};
@@ -239,22 +247,13 @@ impl BackgroundRenderResources {
glyph_vertex_buffer,
glyph_instance_buffer,
glyph_instances,
marker_mode: MarkerMode::Number,
}
}
fn prepare(
&mut self,
queue: &wgpu::Queue,
width: f32,
height: f32,
pressure: &PressureFrame,
marker_mode: MarkerMode,
) {
fn prepare(&mut self, queue: &wgpu::Queue, width: f32, height: f32, pressure: &PressureFrame) {
let aspect = width / height.max(1.0);
self.uniform =
MatrixUniform::new(width, height, build_view_projection(aspect, &self.layout));
self.marker_mode = marker_mode;
queue.write_buffer(
&self.uniform_buffer,
0,
@@ -275,12 +274,36 @@ impl BackgroundRenderResources {
);
}
fn paint(&self, render_pass: &mut wgpu::RenderPass<'_>) {
fn paint(&self, render_pass: &mut wgpu::RenderPass<'_>, active_mode: &ActiveMode) {
render_pass.set_bind_group(0, &self.uniform_bind_group, &[]);
render_pass.set_pipeline(&self.background_pipeline);
render_pass.draw(0..3, 0..1);
match active_mode {
ActiveMode::Finger(mode) => self.paint_finger(render_pass, mode),
ActiveMode::Hand(mode) => self.paint_hand(render_pass, mode),
}
}
fn paint_finger(&self, render_pass: &mut wgpu::RenderPass<'_>, mode: &FingerMode) {
let _range = mode.range.clone();
let marker_pipeline = if mode.dot {
&self.dot_pipeline
} else {
&self.glyph_pipeline
};
let draw_count = self.visible_instance_count(mode.rows, mode.cols);
render_pass.set_pipeline(marker_pipeline);
render_pass.set_vertex_buffer(0, self.glyph_vertex_buffer.slice(..));
render_pass.set_vertex_buffer(1, self.glyph_instance_buffer.slice(..));
render_pass.draw(0..6, 0..draw_count);
}
fn paint_hand(&self, render_pass: &mut wgpu::RenderPass<'_>, mode: &HandGatewayMode) {
let _range = mode.range.clone();
if let Some(model) = &self.model {
render_pass.set_pipeline(&self.model_pipeline);
render_pass.set_vertex_buffer(1, self.model_instance_buffer.slice(..));
@@ -294,17 +317,11 @@ impl BackgroundRenderResources {
render_pass.draw_indexed(0..mesh.num_elements, 0, 0..1);
}
}
}
// let marker_pipeline = match self.marker_mode {
// MarkerMode::Number => &self.glyph_pipeline,
// MarkerMode::Dot => &self.dot_pipeline,
// };
let marker_pipeline = &self.dot_pipeline;
render_pass.set_pipeline(marker_pipeline);
render_pass.set_vertex_buffer(0, self.glyph_vertex_buffer.slice(..));
render_pass.set_vertex_buffer(1, self.glyph_instance_buffer.slice(..));
render_pass.draw(0..6, 0..self.glyph_instances.len() as u32);
fn visible_instance_count(&self, rows: u32, cols: u32) -> u32 {
let requested = rows.saturating_mul(cols);
requested.min(self.glyph_instances.len() as u32)
}
}

319
src/style.rs Normal file
View File

@@ -0,0 +1,319 @@
use eframe::egui;
#[derive(Clone, Copy)]
pub struct AppTheme {
pub bg: egui::Color32,
pub panel: egui::Color32,
pub panel_strong: egui::Color32,
pub panel_deep: egui::Color32,
pub border: egui::Color32,
pub border_soft: egui::Color32,
pub text: egui::Color32,
pub text_dim: egui::Color32,
pub text_subtle: egui::Color32,
pub accent: egui::Color32,
pub accent_hot: egui::Color32,
pub radius: u8,
}
#[derive(Clone, Copy)]
pub struct DesignMetrics {
pub row_gap: f32,
pub item_gap: f32,
pub field_height: f32,
pub button_height: f32,
pub icon_button: egui::Vec2,
pub panel_padding: i8,
pub group_padding_x: i8,
pub group_padding_y: i8,
}
pub const ONE_DARK_PRO: AppTheme = AppTheme {
bg: egui::Color32::from_rgb(30, 40, 50),
panel: egui::Color32::from_rgb(22, 28, 35),
panel_strong: egui::Color32::from_rgb(34, 43, 54),
panel_deep: egui::Color32::from_rgb(15, 20, 27),
border: egui::Color32::from_rgb(78, 93, 110),
border_soft: egui::Color32::from_rgb(50, 62, 76),
text: egui::Color32::from_rgb(235, 241, 247),
text_dim: egui::Color32::from_rgb(174, 185, 198),
text_subtle: egui::Color32::from_rgb(126, 139, 154),
accent: egui::Color32::from_rgb(41, 192, 204),
accent_hot: egui::Color32::from_rgb(247, 190, 84),
radius: 5,
};
pub const ACCENT_GREEN: egui::Color32 = egui::Color32::from_rgb(126, 203, 137);
pub const ACCENT_RED: egui::Color32 = egui::Color32::from_rgb(240, 99, 94);
pub const ACCENT_BLUE: egui::Color32 = egui::Color32::from_rgb(96, 170, 240);
pub const ACCENT_ORANGE: egui::Color32 = egui::Color32::from_rgb(242, 163, 87);
pub const METRICS: DesignMetrics = DesignMetrics {
row_gap: 10.0,
item_gap: 8.0,
field_height: 24.0,
button_height: 28.0,
icon_button: egui::vec2(30.0, 26.0),
panel_padding: 12,
group_padding_x: 10,
group_padding_y: 8,
};
pub mod layout {
pub const TITLE_BAR_HEIGHT: f32 = 36.0;
pub const CENTER_PANEL_TOP: f32 = 48.0;
pub const LEFT_X: f32 = 24.0;
pub const RIGHT_X: f32 = 1328.0;
pub const TOP_Y: f32 = 56.0;
pub const LEFT_SECOND_Y: f32 = 292.0;
pub const RIGHT_SECOND_Y: f32 = 388.0;
pub const LEFT_TAG_X: f32 = 24.0;
pub const RIGHT_TAG_X: f32 = 1810.0;
}
pub fn apply_theme(ctx: &egui::Context, theme: &AppTheme) {
let mut visuals = egui::Visuals::dark();
visuals.override_text_color = Some(theme.text);
visuals.panel_fill = theme.bg;
visuals.window_fill = theme.panel;
visuals.window_stroke = egui::Stroke::new(1.0, theme.border);
visuals.extreme_bg_color = theme.panel_deep;
visuals.faint_bg_color = theme.panel_strong;
visuals.code_bg_color = theme.panel_deep;
visuals.warn_fg_color = theme.accent_hot;
visuals.error_fg_color = ACCENT_RED;
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);
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);
visuals.widgets.hovered.bg_fill = egui::Color32::from_rgb(44, 57, 70);
visuals.widgets.hovered.bg_stroke = egui::Stroke::new(1.0, theme.accent);
visuals.widgets.hovered.fg_stroke = egui::Stroke::new(1.0, egui::Color32::WHITE);
visuals.widgets.active.bg_fill = theme.accent;
visuals.widgets.active.bg_stroke = egui::Stroke::new(1.0, theme.accent_hot);
visuals.widgets.active.fg_stroke = egui::Stroke::new(1.0, egui::Color32::WHITE);
visuals.widgets.open.bg_fill = egui::Color32::from_rgb(39, 50, 62);
visuals.widgets.open.bg_stroke = egui::Stroke::new(1.0, theme.accent);
visuals.widgets.open.fg_stroke = egui::Stroke::new(1.0, theme.text);
visuals.selection.bg_fill = egui::Color32::from_rgb(35, 123, 140);
visuals.selection.stroke = egui::Stroke::new(1.0, egui::Color32::WHITE);
visuals.hyperlink_color = theme.accent_hot;
ctx.set_visuals(visuals);
let mut style = (*ctx.global_style()).clone();
style.spacing.item_spacing = egui::vec2(METRICS.item_gap, 6.0);
style.spacing.button_padding = egui::vec2(12.0, 5.0);
style.spacing.window_margin = egui::Margin::same(METRICS.panel_padding);
style.spacing.interact_size = egui::vec2(72.0, METRICS.field_height);
style.spacing.combo_width = 112.0;
style.spacing.text_edit_width = 112.0;
style.visuals.window_corner_radius = egui::CornerRadius::same(theme.radius);
style.visuals.menu_corner_radius = egui::CornerRadius::same(4);
style.visuals.window_shadow = egui::epaint::Shadow {
offset: [0, 14],
blur: 28,
spread: 0,
color: egui::Color32::from_black_alpha(135),
};
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(),
);
let has_yahei = std::fs::read(r"C:\Windows\Fonts\msyh.ttc")
.or_else(|_| std::fs::read(r"C:\Windows\Fonts\msyhbd.ttc"))
.map(|font_data| {
fonts.font_data.insert(
"Microsoft-YaHei".to_owned(),
egui::FontData::from_owned(font_data).into(),
);
})
.is_ok();
fonts
.families
.entry(egui::FontFamily::Proportional)
.or_default()
.insert(0, "Hack-Bold".to_owned());
if has_yahei {
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());
if has_yahei {
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)
.fill(ONE_DARK_PRO.panel)
.stroke(egui::Stroke::new(1.0, ONE_DARK_PRO.border))
.corner_radius(egui::CornerRadius::same(ONE_DARK_PRO.radius))
.inner_margin(egui::Margin::same(METRICS.panel_padding))
.shadow(egui::epaint::Shadow {
offset: [0, 14],
blur: 28,
spread: 0,
color: egui::Color32::from_black_alpha(135),
})
}
pub fn center_panel_frame() -> egui::Frame {
egui::Frame::new()
.fill(ONE_DARK_PRO.panel)
.corner_radius(egui::CornerRadius::same(ONE_DARK_PRO.radius))
.inner_margin(egui::Margin::symmetric(
METRICS.panel_padding + 2,
METRICS.panel_padding,
))
}
pub fn group_frame() -> egui::Frame {
egui::Frame::new()
.fill(ONE_DARK_PRO.panel_deep)
.stroke(egui::Stroke::new(1.0, ONE_DARK_PRO.border_soft))
.corner_radius(egui::CornerRadius::same(4))
.inner_margin(egui::Margin::symmetric(
METRICS.group_padding_x,
METRICS.group_padding_y,
))
}
pub fn tag_button(label: impl Into<egui::WidgetText>) -> egui::Button<'static> {
egui::Button::new(label)
.fill(ONE_DARK_PRO.panel_strong)
.stroke(egui::Stroke::new(1.0, ONE_DARK_PRO.border))
.corner_radius(egui::CornerRadius::same(4))
.min_size(egui::vec2(0.0, METRICS.button_height))
}
pub fn primary_button(label: impl Into<egui::WidgetText>) -> egui::Button<'static> {
egui::Button::new(label)
.fill(ONE_DARK_PRO.accent)
.stroke(egui::Stroke::new(1.0, ONE_DARK_PRO.accent_hot))
.corner_radius(egui::CornerRadius::same(4))
.min_size(egui::vec2(112.0, METRICS.button_height))
}
pub fn danger_button(label: impl Into<egui::WidgetText>) -> egui::Button<'static> {
egui::Button::new(label)
.fill(ACCENT_RED)
.stroke(egui::Stroke::new(
1.0,
egui::Color32::from_rgb(255, 138, 126),
))
.corner_radius(egui::CornerRadius::same(4))
.min_size(egui::vec2(112.0, METRICS.button_height))
}
pub fn accent_button(
label: impl Into<egui::WidgetText>,
fill: egui::Color32,
) -> egui::Button<'static> {
egui::Button::new(label)
.fill(fill)
.stroke(egui::Stroke::new(1.0, ONE_DARK_PRO.border))
.corner_radius(egui::CornerRadius::same(4))
.min_size(egui::vec2(0.0, METRICS.button_height))
}
pub fn mode_button(label: &'static str, selected: bool) -> egui::Button<'static> {
let fill = if selected {
ONE_DARK_PRO.accent
} else {
ONE_DARK_PRO.panel_strong
};
let stroke = if selected {
ONE_DARK_PRO.accent_hot
} else {
ONE_DARK_PRO.border_soft
};
egui::Button::new(egui::RichText::new(label).color(egui::Color32::WHITE))
.fill(fill)
.stroke(egui::Stroke::new(1.0, stroke))
.corner_radius(egui::CornerRadius::same(4))
.min_size(egui::vec2(96.0, METRICS.button_height))
}
pub fn icon_button<'a>(icon: impl Into<egui::WidgetText>, size: egui::Vec2) -> egui::Button<'a> {
egui::Button::new(icon)
.fill(ONE_DARK_PRO.panel_strong)
.stroke(egui::Stroke::new(1.0, ONE_DARK_PRO.border))
.corner_radius(egui::CornerRadius::same(4))
.min_size(size)
}
pub fn panel_title(title: &'static str) -> egui::RichText {
egui::RichText::new(title)
.color(ONE_DARK_PRO.text_dim)
.size(13.0)
}
pub fn field_label(label: impl Into<String>) -> egui::RichText {
egui::RichText::new(label.into())
.color(ONE_DARK_PRO.text_dim)
.size(12.0)
}
pub fn value_text(text: impl Into<String>) -> egui::RichText {
egui::RichText::new(text.into())
.color(ONE_DARK_PRO.text)
.size(12.0)
}
pub fn subtle_text(text: impl Into<String>) -> egui::RichText {
egui::RichText::new(text.into())
.color(ONE_DARK_PRO.text_subtle)
.size(12.0)
}
pub fn dim_text() -> egui::Color32 {
ONE_DARK_PRO.text_dim
}
pub fn accent_text() -> egui::Color32 {
ONE_DARK_PRO.accent_hot
}
pub fn status_color_ok() -> egui::Color32 {
ACCENT_GREEN
}
pub fn status_color_warn() -> egui::Color32 {
ONE_DARK_PRO.accent_hot
}
pub fn status_color_error() -> egui::Color32 {
ACCENT_RED
}
pub fn status_color_info() -> egui::Color32 {
ACCENT_BLUE
}

590
src/ui.rs
View File

@@ -3,9 +3,9 @@ use eframe::egui;
use crate::{
connection::{ConnectionManager, ConnectionState},
recording::Recorder,
theme::{
ACCENT_BLUE, ACCENT_GREEN, ACCENT_ORANGE, ACCENT_RED, ONE_DARK_PRO, accent_text, dim_text,
group_frame, panel_frame, tag_button,
style::{
self, ACCENT_BLUE, ACCENT_GREEN, ACCENT_ORANGE, ACCENT_RED, METRICS, ONE_DARK_PRO,
accent_text, dim_text, group_frame, layout, panel_frame, tag_button,
},
utils::serial_enum,
};
@@ -47,9 +47,8 @@ pub struct ConnectPanelState {
#[derive(Clone, Copy, PartialEq, Eq)]
pub enum SerialMode {
SingleModule,
Manual,
Model,
Finger,
Hand,
}
#[derive(Clone, Copy, PartialEq, Eq)]
@@ -80,7 +79,7 @@ impl FloatingPanelState {
impl Default for ConfigPanelState {
fn default() -> Self {
Self {
mode: SerialMode::SingleModule,
mode: SerialMode::Finger,
port: "COM3".to_owned(),
baud_rate: 115_200,
data_bits: 8,
@@ -98,10 +97,10 @@ impl Default for ConfigPanelState {
impl Default for ConnectPanelState {
fn default() -> Self {
let port = serial_enum().unwrap();
let port = serial_enum().unwrap_or_default();
let selected_port = port.first().cloned().unwrap_or_default();
Self {
mode: SerialMode::SingleModule,
mode: SerialMode::Finger,
port,
selected_port,
duration: 10,
@@ -143,165 +142,162 @@ pub fn draw_connect_panel(
ConnectionState::Connected | ConnectionState::Streaming
);
draw_center_floating_panel(ctx, panel, "connect_center_panel", 42.0, "连接", |ui| {
ui.set_min_width(320.0);
draw_center_floating_panel(
ctx,
panel,
"connect_center_panel",
layout::CENTER_PANEL_TOP,
"连接",
|ui| {
ui.vertical(|ui| {
// centered_mode_row(ui, &mut config.mode);
ui.add_space(METRICS.row_gap);
ui.vertical(|ui| {
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| {
group_frame().show(ui, |ui| {
ui.horizontal(|ui| {
ui.colored_label(ONE_DARK_PRO.text, "串口");
egui::ComboBox::from_id_salt("connect_ports")
.width(130.0)
.selected_text(if config.selected_port.is_empty() {
"无可用串口".to_owned()
} else {
config.selected_port.clone()
})
.show_ui(ui, |ui| {
for port in &config.port {
let label = port.clone();
ui.selectable_value(
&mut config.selected_port,
port.clone(),
label,
);
}
});
if ui
.add(
egui::Button::new("")
.fill(ONE_DARK_PRO.panel_strong)
.stroke(egui::Stroke::new(1.0, ONE_DARK_PRO.border_soft))
.min_size(egui::vec2(24.0, 20.0)),
)
.on_hover_text("刷新串口")
.clicked()
{
if let Ok(ports) = serial_enum() {
if !ports.contains(&config.selected_port) {
config.selected_port =
ports.first().cloned().unwrap_or_default();
}
config.port = ports;
}
}
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(
egui::Image::new(egui::include_image!("../static/cpu.png"))
.fit_to_exact_size(egui::vec2(64.0, 64.0)),
);
});
ui.add_space(METRICS.item_gap);
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(ONE_DARK_PRO.text_dim, "");
ui.add_sized(
egui::vec2(48.0, 20.0),
egui::DragValue::new(&mut config.rows).range(1..=64),
);
ui.colored_label(ONE_DARK_PRO.text_dim, "");
ui.add_sized(
egui::vec2(48.0, 20.0),
egui::DragValue::new(&mut config.cols).range(1..=64),
);
});
ui.vertical(|ui| {
draw_connect_port_row(ui, config);
ui.add_space(6.0);
draw_connect_matrix_row(ui, config);
});
});
})
});
ui.add_space(METRICS.row_gap);
draw_connect_action_row(ui, conn_state, is_connected, config, connection);
if is_connected {
draw_recording_toolbar(ui, recorder, export_path);
}
});
},
);
}
ui.add_space(8.0);
// fn centered_mode_row(ui: &mut egui::Ui, mode: &mut SerialMode) {
// let button_width = 96.0;
// let gap = METRICS.item_gap;
// let total_width = button_width * 3.0 + gap * 2.0;
// let left_space = ((ui.available_width() - total_width) * 0.5).max(0.0);
// Connection status and button row
ui.horizontal(|ui| {
let status_text = match conn_state {
ConnectionState::Disconnected => "未连接",
ConnectionState::Connecting => "连接中...",
ConnectionState::Connected => "已连接",
ConnectionState::Streaming => "采集中",
ConnectionState::Error => "连接错误",
};
let status_color = match conn_state {
ConnectionState::Disconnected => ONE_DARK_PRO.text_dim,
ConnectionState::Connecting => egui::Color32::from_rgb(200, 180, 60),
ConnectionState::Connected => egui::Color32::from_rgb(158, 184, 101),
ConnectionState::Streaming => egui::Color32::from_rgb(100, 200, 255),
ConnectionState::Error => egui::Color32::from_rgb(255, 98, 82),
};
ui.colored_label(status_color, status_text);
// ui.horizontal(|ui| {
// ui.add_space(left_space);
// mode_button(ui, mode, SerialMode::Finger, "指尖模块");
// ui.add_space(gap);
// mode_button(ui, mode, SerialMode::Hand, "手掌模块");
// // ui.add_space(gap);
// // mode_button(ui, mode, SerialMode::Model, "模型");
// });
// }
let used = 100.0;
let remaining = (ui.available_width() - used).max(0.0);
ui.add_space(remaining);
let btn_label = if is_connected { "断开" } else { "连接" };
let btn_fill = if is_connected {
egui::Color32::from_rgb(180, 60, 60)
} else {
ONE_DARK_PRO.accent
};
let btn_stroke = if is_connected {
egui::Stroke::new(1.0, egui::Color32::from_rgb(220, 80, 80))
} else {
egui::Stroke::new(1.0, ONE_DARK_PRO.accent_hot)
};
if ui
.add(
egui::Button::new(
egui::RichText::new(btn_label).color(egui::Color32::WHITE),
)
.fill(btn_fill)
.stroke(btn_stroke)
.min_size(egui::vec2(96.0, 28.0)),
)
.clicked()
{
if is_connected {
connection.disconnect();
} else if !config.selected_port.is_empty() {
connection.connect(
&config.selected_port,
config.rows as u32,
config.cols as u32,
);
}
fn draw_connect_port_row(ui: &mut egui::Ui, config: &mut ConnectPanelState) {
ui.horizontal(|ui| {
ui.label(style::field_label("串口"));
egui::ComboBox::from_id_salt("connect_ports")
.width(150.0)
.selected_text(if config.selected_port.is_empty() {
"无可用串口".to_owned()
} else {
config.selected_port.clone()
})
.show_ui(ui, |ui| {
for port in &config.port {
ui.selectable_value(&mut config.selected_port, port.clone(), port.as_str());
}
});
// Recording toolbar (visible when connected)
if is_connected {
draw_recording_toolbar(ui, recorder, export_path);
if ui
.add(style::icon_button(
egui::RichText::new("").color(ONE_DARK_PRO.text).size(16.0),
METRICS.icon_button,
))
.on_hover_text("刷新串口")
.clicked()
{
if let Ok(ports) = serial_enum() {
if !ports.contains(&config.selected_port) {
config.selected_port = ports.first().cloned().unwrap_or_default();
}
config.port = ports;
}
}
ui.add_space(METRICS.item_gap);
ui.label(style::field_label("频率"));
ui.add_sized(
egui::vec2(72.0, METRICS.field_height),
egui::DragValue::new(&mut config.duration).range(1..=120),
);
});
}
fn draw_connect_matrix_row(ui: &mut egui::Ui, config: &mut ConnectPanelState) {
ui.horizontal(|ui| {
ui.checkbox(&mut config.manual, "手动矩阵");
ui.add_enabled_ui(config.manual, |ui| {
ui.add_space(METRICS.item_gap);
ui.label(style::field_label(""));
ui.add_sized(
egui::vec2(56.0, METRICS.field_height),
egui::DragValue::new(&mut config.rows).range(1..=64),
);
ui.label(style::field_label(""));
ui.add_sized(
egui::vec2(56.0, METRICS.field_height),
egui::DragValue::new(&mut config.cols).range(1..=64),
);
});
});
}
fn draw_connect_action_row(
ui: &mut egui::Ui,
conn_state: ConnectionState,
is_connected: bool,
config: &ConnectPanelState,
connection: &ConnectionManager,
) {
ui.horizontal(|ui| {
let status_text = match conn_state {
ConnectionState::Disconnected => "未连接",
ConnectionState::Connecting => "连接中...",
ConnectionState::Connected => "已连接",
ConnectionState::Streaming => "采集中",
ConnectionState::Error => "连接错误",
};
let status_color = match conn_state {
ConnectionState::Disconnected => ONE_DARK_PRO.text_dim,
ConnectionState::Connecting => style::status_color_warn(),
ConnectionState::Connected => style::status_color_ok(),
ConnectionState::Streaming => style::status_color_info(),
ConnectionState::Error => style::status_color_error(),
};
ui.colored_label(status_color, status_text);
ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
let button = if is_connected {
style::danger_button("断开")
} else {
style::primary_button("连接")
};
if ui.add(button).clicked() {
if is_connected {
connection.disconnect();
} else if !config.selected_port.is_empty() {
connection.connect(
&config.selected_port,
config.rows as u32,
config.cols as u32,
);
}
}
});
});
@@ -311,11 +307,16 @@ pub fn draw_config_panel(
ctx: &egui::Context,
panel: &mut FloatingPanelState,
config: &mut ConfigPanelState,
) {
) -> Option<SerialMode> {
let mut changed_mode = None;
draw_floating_panel(ctx, panel, "配置", "config_panel", |ui| {
ui.set_min_width(560.0);
draw_mode_row(ui, config);
if let Some(mode) = draw_mode_row(ui, config) {
changed_mode = Some(mode);
}
// draw_mode_row(ui, config);
ui.separator();
draw_connection_row(ui, config);
ui.add_space(8.0);
@@ -323,22 +324,30 @@ pub fn draw_config_panel(
ui.add_space(8.0);
draw_mode_body(ui, config);
});
changed_mode
}
fn draw_mode_row(ui: &mut egui::Ui, config: &mut ConfigPanelState) {
fn draw_mode_row(ui: &mut egui::Ui, config: &mut ConfigPanelState) -> Option<SerialMode> {
let mut changed_to = None;
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, "模型");
if mode_button(ui, &mut config.mode, SerialMode::Finger, "指尖模块") {
changed_to = Some(SerialMode::Finger)
}
if mode_button(ui, &mut config.mode, SerialMode::Hand, "手掌模块") {
changed_to = Some(SerialMode::Hand)
}
// 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(), "重连");
});
});
changed_to
}
fn draw_connection_row(ui: &mut egui::Ui, config: &mut ConfigPanelState) {
@@ -348,20 +357,20 @@ fn draw_connection_row(ui: &mut egui::Ui, config: &mut ConfigPanelState) {
.fit_to_exact_size(egui::vec2(72.0, 72.0)),
);
ui.add_space(10.0);
ui.add_space(METRICS.item_gap);
ui.vertical(|ui| {
ui.label(format!("端口 {}", config.port));
ui.label(format!("波特率 {}", config.baud_rate));
ui.label(style::value_text(format!("端口 {}", config.port)));
ui.label(style::value_text(format!("波特率 {}", config.baud_rate)));
let status = if config.connected {
"已连接"
} else {
"未连接"
};
let status_color = if config.connected {
egui::Color32::from_rgb(158, 184, 101)
style::status_color_ok()
} else {
egui::Color32::from_rgb(255, 98, 82)
style::status_color_error()
};
ui.colored_label(status_color, status);
});
@@ -370,12 +379,11 @@ fn draw_connection_row(ui: &mut egui::Ui, config: &mut ConfigPanelState) {
let button_text = if config.connected { "断开" } else { "连接" };
if ui
.add(
egui::Button::new(button_text)
.fill(ONE_DARK_PRO.accent)
.stroke(egui::Stroke::new(1.0, ONE_DARK_PRO.accent_hot))
.min_size(egui::vec2(120.0, 30.0)),
)
.add(if config.connected {
style::danger_button(button_text)
} else {
style::primary_button(button_text)
})
.clicked()
{
config.connected = !config.connected;
@@ -383,7 +391,11 @@ fn draw_connection_row(ui: &mut egui::Ui, config: &mut ConfigPanelState) {
ui.add_space(18.0);
ui.colored_label(
egui::Color32::from_rgb(158, 184, 101),
if config.auto_reconnect {
style::status_color_ok()
} else {
ONE_DARK_PRO.text_dim
},
if config.auto_reconnect {
"链路保护 开"
} else {
@@ -397,39 +409,39 @@ 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))
.spacing(egui::vec2(14.0, 7.0))
.striped(true)
.show(ui, |ui| {
ui.label("端口");
ui.label(style::field_label("端口"));
ui.add_sized(
egui::vec2(110.0, 20.0),
egui::vec2(110.0, METRICS.field_height),
egui::TextEdit::singleline(&mut config.port),
);
ui.label("波特率");
ui.label(style::field_label("波特率"));
baud_combo(ui, config);
ui.end_row();
ui.label("数据位");
ui.label(style::field_label("数据位"));
ui.add_sized(
egui::vec2(70.0, 20.0),
egui::vec2(70.0, METRICS.field_height),
egui::DragValue::new(&mut config.data_bits).range(5..=8),
);
ui.label("校验");
ui.label(style::field_label("校验"));
parity_combo(ui, config);
ui.end_row();
ui.label("停止位");
ui.label(style::field_label("停止位"));
ui.add_sized(
egui::vec2(70.0, 20.0),
egui::vec2(70.0, METRICS.field_height),
egui::DragValue::new(&mut config.stop_bits).range(1..=2),
);
ui.label("超时");
ui.label(style::field_label("超时"));
ui.horizontal(|ui| {
ui.add_sized(
egui::vec2(84.0, 20.0),
egui::vec2(84.0, METRICS.field_height),
egui::DragValue::new(&mut config.timeout_ms)
.range(50..=30_000)
.speed(50),
@@ -443,11 +455,11 @@ fn draw_serial_grid(ui: &mut egui::Ui, config: &mut ConfigPanelState) {
fn draw_mode_body(ui: &mut egui::Ui, config: &mut ConfigPanelState) {
group_frame().show(ui, |ui| match config.mode {
SerialMode::SingleModule => {
SerialMode::Finger => {
ui.horizontal(|ui| {
ui.label("模块地址");
ui.label(style::field_label("模块地址"));
ui.add_sized(
egui::vec2(80.0, 20.0),
egui::vec2(80.0, METRICS.field_height),
egui::DragValue::new(&mut config.module_addr).range(1..=247),
);
ui.add_space(16.0);
@@ -464,11 +476,11 @@ fn draw_mode_body(ui: &mut egui::Ui, config: &mut ConfigPanelState) {
ui.label("0 字节");
});
}
SerialMode::Manual => {
SerialMode::Hand => {
ui.horizontal(|ui| {
ui.label("发送");
ui.label(style::field_label("发送"));
ui.add_sized(
egui::vec2(300.0, 20.0),
egui::vec2(300.0, METRICS.field_height),
egui::TextEdit::singleline(&mut config.manual_tx),
);
if ui.add(tag_button("发送")).clicked() {}
@@ -476,18 +488,17 @@ fn draw_mode_body(ui: &mut egui::Ui, config: &mut ConfigPanelState) {
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() {}
});
}
} // SerialMode::Model => {
// ui.horizontal(|ui| {
// ui.label(style::field_label("模型"));
// ui.add_sized(
// egui::vec2(300.0, METRICS.field_height),
// egui::TextEdit::singleline(&mut config.model_path),
// );
// if ui.add(tag_button("加载")).clicked() {}
// if ui.add(tag_button("运行")).clicked() {}
// });
// }
});
}
@@ -517,35 +528,25 @@ pub fn icon_button_sized<'a>(
}
.fill(ONE_DARK_PRO.panel_strong)
.stroke(egui::Stroke::new(1.0, ONE_DARK_PRO.border))
.corner_radius(egui::CornerRadius::same(2))
.corner_radius(egui::CornerRadius::same(ONE_DARK_PRO.radius))
.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 {
ONE_DARK_PRO.accent
} else {
ONE_DARK_PRO.panel_strong
};
let stroke = if selected {
ONE_DARK_PRO.accent_hot
} else {
ONE_DARK_PRO.border_soft
};
fn mode_button(
ui: &mut egui::Ui,
mode: &mut SerialMode,
value: SerialMode,
label: &'static str,
) -> bool {
let clicked = ui.add(style::mode_button(label, *mode == value)).clicked();
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()
{
if clicked && *mode != value {
*mode = value;
true
} else {
false
}
}
@@ -581,13 +582,13 @@ 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.label(style::subtle_text("81m:51s"));
});
ui.separator();
group_frame().show(ui, |ui| {
ui.label("帧率 / GPU 信息");
ui.label("边界 589.0us");
ui.label("簇 12.8ms");
ui.label(style::field_label("帧率 / GPU 信息"));
ui.label(style::value_text("边界 589.0us"));
ui.label(style::value_text("簇 12.8ms"));
});
});
}
@@ -612,12 +613,13 @@ fn draw_floating_panel(
.resizable(true)
.frame(panel_frame(ctx))
.show(ctx, |ui| {
ui.spacing_mut().item_spacing = egui::vec2(METRICS.item_gap, 6.0);
ui.horizontal(|ui| {
if ui.add(tag_button("隐藏")).clicked() {
hide_requested = true;
}
ui.add_space(6.0);
ui.colored_label(dim_text(), title);
ui.label(style::panel_title(title));
});
ui.separator();
add_contents(ui);
@@ -647,23 +649,24 @@ fn draw_floating_panel(
}
}
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;
}
// 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(
@@ -674,7 +677,7 @@ fn draw_center_floating_panel(
collapsed_label: &'static str,
add_contents: impl FnOnce(&mut egui::Ui),
) {
const PANEL_WIDTH: f32 = 430.0;
const PANEL_WIDTH: f32 = 520.0;
const SLIDE_DISTANCE: f32 = 18.0;
let anim = advance_center_panel_anim(ctx, panel);
@@ -759,14 +762,10 @@ fn center_panel_shell<R>(
width: f32,
add_contents: impl FnOnce(&mut egui::Ui) -> R,
) -> egui::InnerResponse<R> {
egui::Frame::new()
.fill(ONE_DARK_PRO.panel)
.corner_radius(egui::CornerRadius::same(ONE_DARK_PRO.radius))
.inner_margin(egui::Margin::symmetric(10, 8))
.show(ui, |ui| {
ui.set_width(width);
add_contents(ui)
})
style::center_panel_frame().show(ui, |ui| {
ui.set_width(width);
add_contents(ui)
})
}
fn allocate_center_panel_handle(ui: &mut egui::Ui) -> egui::Rect {
@@ -1059,10 +1058,10 @@ pub fn draw_recording_toolbar(ui: &mut egui::Ui, recorder: &Recorder, export_pat
let rec_btn = if is_recording {
tag_button("⏹ 停止")
} else {
egui::Button::new(egui::RichText::new("● 全量录制").color(egui::Color32::WHITE))
.fill(ACCENT_RED)
.stroke(egui::Stroke::new(1.0, ONE_DARK_PRO.border))
.corner_radius(egui::CornerRadius::same(2))
style::accent_button(
egui::RichText::new("● 全量录制").color(egui::Color32::WHITE),
ACCENT_RED,
)
};
if ui.add(rec_btn).clicked() {
if is_recording {
@@ -1078,10 +1077,10 @@ pub fn draw_recording_toolbar(ui: &mut egui::Ui, recorder: &Recorder, export_pat
let snap_btn = if is_recording {
tag_button("⏹ 快照停止")
} else {
egui::Button::new(egui::RichText::new("✂ 快照录制").color(egui::Color32::WHITE))
.fill(ACCENT_ORANGE)
.stroke(egui::Stroke::new(1.0, ONE_DARK_PRO.border))
.corner_radius(egui::CornerRadius::same(2))
style::accent_button(
egui::RichText::new("✂ 快照录制").color(egui::Color32::WHITE),
ACCENT_ORANGE,
)
};
if ui.add(snap_btn).clicked() {
if is_recording {
@@ -1104,12 +1103,10 @@ pub fn draw_recording_toolbar(ui: &mut egui::Ui, recorder: &Recorder, export_pat
// Export
if ui
.add(
egui::Button::new(egui::RichText::new("📤 导出CSV").color(egui::Color32::WHITE))
.fill(ACCENT_BLUE)
.stroke(egui::Stroke::new(1.0, ONE_DARK_PRO.border))
.corner_radius(egui::CornerRadius::same(2)),
)
.add(style::accent_button(
egui::RichText::new("📤 导出CSV").color(egui::Color32::WHITE),
ACCENT_BLUE,
))
.clicked()
{
let path = if export_path.is_empty() {
@@ -1128,12 +1125,10 @@ pub fn draw_recording_toolbar(ui: &mut egui::Ui, recorder: &Recorder, export_pat
// Import
if ui
.add(
egui::Button::new(egui::RichText::new("📥 导入CSV").color(egui::Color32::WHITE))
.fill(ACCENT_GREEN)
.stroke(egui::Stroke::new(1.0, ONE_DARK_PRO.border))
.corner_radius(egui::CornerRadius::same(2)),
)
.add(style::accent_button(
egui::RichText::new("📥 导入CSV").color(egui::Color32::WHITE),
ACCENT_GREEN,
))
.clicked()
{
let import_path = export_path.clone();
@@ -1147,7 +1142,10 @@ pub fn draw_recording_toolbar(ui: &mut egui::Ui, recorder: &Recorder, export_pat
ui.horizontal(|ui| {
ui.colored_label(dim_text(), "导出路径");
ui.add_sized(
egui::vec2(ui.available_width() - 80.0, 20.0),
egui::vec2(
(ui.available_width() - 80.0).max(160.0),
METRICS.field_height,
),
egui::TextEdit::singleline(export_path).hint_text("eskin_export_*.csv"),
);
});
@@ -1198,25 +1196,24 @@ pub fn draw_matrix_config_panel(
ui.set_min_width(280.0);
ui.horizontal(|ui| {
ui.colored_label(dim_text(), "矩阵尺寸");
ui.add_space(8.0);
ui.colored_label(ONE_DARK_PRO.text, "");
ui.label(style::field_label("矩阵尺寸"));
ui.add_space(METRICS.item_gap);
ui.label(style::value_text(""));
ui.add_sized(
egui::vec2(56.0, 20.0),
egui::vec2(56.0, METRICS.field_height),
egui::DragValue::new(&mut config.rows).range(1..=128),
);
ui.colored_label(ONE_DARK_PRO.text, "");
ui.label(style::value_text(""));
ui.add_sized(
egui::vec2(56.0, 20.0),
egui::vec2(56.0, METRICS.field_height),
egui::DragValue::new(&mut config.cols).range(1..=128),
);
});
ui.add_space(4.0);
ui.add_space(METRICS.item_gap);
// Presets
ui.horizontal(|ui| {
ui.colored_label(dim_text(), "预设");
ui.label(style::field_label("预设"));
for (r, c, name) in &[
(12, 7, "12×7"),
(24, 12, "24×12"),
@@ -1231,31 +1228,40 @@ pub fn draw_matrix_config_panel(
}
});
ui.add_space(4.0);
ui.add_space(METRICS.item_gap);
ui.horizontal(|ui| {
ui.colored_label(dim_text(), "色域范围");
ui.add_space(8.0);
ui.colored_label(ONE_DARK_PRO.text, "最小");
ui.label(style::field_label("色域范围"));
ui.add_space(METRICS.item_gap);
ui.label(style::value_text("最小"));
ui.add_sized(
egui::vec2(72.0, 20.0),
egui::vec2(72.0, METRICS.field_height),
egui::DragValue::new(&mut config.color_min)
.range(0.0..=10000.0)
.speed(100.0),
);
ui.colored_label(ONE_DARK_PRO.text, "最大");
ui.label(style::value_text("最大"));
ui.add_sized(
egui::vec2(72.0, 20.0),
egui::vec2(72.0, METRICS.field_height),
egui::DragValue::new(&mut config.color_max)
.range(1.0..=10000.0)
.speed(100.0),
);
});
ui.add_space(8.0);
ui.add_space(METRICS.row_gap);
if ui.add(tag_button("重置默认")).clicked() {
*config = MatrixConfigState::default();
}
});
}
pub fn panel_restore_item(ui: &mut egui::Ui, title: &'static str, panel: &mut FloatingPanelState) {
if panel.visible {
ui.add_enabled(false, egui::Button::new(format!("{title} 已显示")));
} else if ui.button(format!("显示 {title}")).clicked() {
panel.visible = true;
ui.close();
}
}