diff --git a/src/app.rs b/src/app.rs index ce5661f..7de5f6f 100644 --- a/src/app.rs +++ b/src/app.rs @@ -223,19 +223,23 @@ impl EskinDesktopApp { } fn draw_workspace(&mut self, ui: &mut egui::Ui) { + let screen = ui.max_rect(); + let workspace = egui::Rect::from_min_max( + screen.left_top() + egui::vec2(0.0, layout::WORKSPACE_TOP), + screen.right_bottom(), + ); + if self.breakout_visible { - self.draw_split_workspace(ui); + self.draw_split_workspace(ui, workspace); } else { - self.draw_wgpu_background_rect(ui, ui.max_rect()); + ui.painter() + .rect_filled(screen, egui::CornerRadius::ZERO, ONE_DARK_PRO.bg); + self.draw_wgpu_background_rect(ui, workspace); } } - fn draw_split_workspace(&mut self, ui: &mut egui::Ui) { + fn draw_split_workspace(&mut self, ui: &mut egui::Ui, content: egui::Rect) { let screen = ui.max_rect(); - let content = egui::Rect::from_min_max( - screen.left_top() + egui::vec2(0.0, layout::TITLE_BAR_HEIGHT), - screen.right_bottom(), - ); let gutter = 10.0; let half_width = (content.width() - gutter).max(1.0) * 0.5; let left = egui::Rect::from_min_size(content.min, egui::vec2(half_width, content.height())); @@ -423,12 +427,6 @@ impl EskinDesktopApp { self.matrix_config_panel.visible = false; self.context_menu_open = false; - if self.breakout_visible { - self.config_panel.visible = false; - self.stats_panel.visible = false; - return; - } - if let Some(next_mode) = draw_config_panel( ctx, &mut self.config_panel, @@ -455,6 +453,11 @@ impl EskinDesktopApp { } } + if self.breakout_visible { + self.stats_panel.visible = false; + return; + } + if self.config_state.mode == SerialMode::Finger { self.stats_panel.visible = true; draw_stats_panel( diff --git a/src/style.rs b/src/style.rs index 16bd2a2..92124c2 100644 --- a/src/style.rs +++ b/src/style.rs @@ -61,6 +61,8 @@ pub const METRICS: DesignMetrics = DesignMetrics { pub mod layout { pub const TITLE_BAR_HEIGHT: f32 = 36.0; + pub const CONFIG_BAR_HEIGHT: f32 = 48.0; + pub const WORKSPACE_TOP: f32 = TITLE_BAR_HEIGHT + CONFIG_BAR_HEIGHT; pub const CENTER_PANEL_TOP: f32 = 48.0; pub const LEFT_X: f32 = 24.0; pub const RIGHT_X: f32 = 1328.0; diff --git a/src/ui.rs b/src/ui.rs index 4250187..89c8884 100644 --- a/src/ui.rs +++ b/src/ui.rs @@ -157,7 +157,7 @@ fn responsive_panel_width(ctx: &egui::Context, preferred: f32, min_width: f32) - fn responsive_panel_height(ctx: &egui::Context, preferred: f32, min_height: f32) -> f32 { let screen = ctx.content_rect(); - let available = (screen.height() - layout::TITLE_BAR_HEIGHT - PANEL_VIEWPORT_MARGIN * 2.0) + let available = (screen.height() - layout::WORKSPACE_TOP - PANEL_VIEWPORT_MARGIN * 2.0) .max(min_height.min(screen.height())); let height_cap = (screen.height() * 0.72).max(min_height.min(available)); @@ -173,7 +173,7 @@ fn responsive_panel_pos( panel_width: f32, ) -> egui::Pos2 { let rect = viewport_panel_rect(ctx); - let min_y = rect.top() + layout::TITLE_BAR_HEIGHT + PANEL_VIEWPORT_MARGIN; + let min_y = rect.top() + layout::WORKSPACE_TOP + PANEL_VIEWPORT_MARGIN; let max_x = (rect.right() - panel_width).max(rect.left()); let preferred_right_side = preferred.x > rect.center().x; let x = if preferred_right_side { @@ -397,40 +397,163 @@ pub fn draw_config_panel( ) -> Option { let mut changed_mode = None; let conn_state = connection.state(); - let panel_width = responsive_panel_width(ctx, 560.0, 340.0); + let screen = ctx.content_rect(); + let bar_rect = egui::Rect::from_min_size( + screen.left_top() + egui::vec2(0.0, layout::TITLE_BAR_HEIGHT), + egui::vec2(screen.width(), layout::CONFIG_BAR_HEIGHT), + ); + config.connected = matches!( conn_state, ConnectionState::Connected | ConnectionState::Streaming ); panel.visible = true; - draw_pinned_panel(ctx, panel, "配置", "config_panel", 560.0, 340.0, |ui| { - ui.set_min_width(panel_width); - ui.set_max_width(panel_width); + egui::Area::new(egui::Id::new("config_panel_bar")) + .fixed_pos(bar_rect.min) + .order(egui::Order::Foreground) + .show(ctx, |ui| { + ui.set_min_size(bar_rect.size()); + ui.set_max_size(bar_rect.size()); - if let Some(mode) = draw_mode_row(ui, config) { - changed_mode = Some(mode); - } + config_bar_frame().show(ui, |ui| { + ui.set_min_width(bar_rect.width()); + ui.set_max_width(bar_rect.width()); + ui.spacing_mut().item_spacing = egui::vec2(METRICS.item_gap, 6.0); + + ui.horizontal(|ui| { + ui.label(style::panel_title("配置面板")); + ui.add_space(12.0); + + if let Some(mode) = draw_config_bar_mode(ui, config) { + changed_mode = Some(mode); + } + + ui.add_space(12.0); + draw_config_bar_connection(ui, config, connection, recorder, conn_state); + + ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| { + draw_config_bar_rates(ui, stats, sample_rate_hz, render_rate_hz); + }); + }); + }); + }); - // draw_mode_row(ui, config); - ui.separator(); - draw_connection_row(ui, config, connection, recorder, conn_state); - ui.add_space(8.0); - // Legacy serial parameter grid is intentionally kept commented for future hardware: - // draw_serial_grid(ui, config); - // ui.add_space(8.0); - draw_mode_body( - ui, - config, - conn_state, - stats, - sample_rate_hz, - render_rate_hz, - ); - }); changed_mode } +fn config_bar_frame() -> egui::Frame { + egui::Frame::new() + .fill(ONE_DARK_PRO.panel_deep) + .inner_margin(egui::Margin::symmetric(8, 6)) +} + +fn draw_config_bar_mode(ui: &mut egui::Ui, config: &mut ConfigPanelState) -> Option { + let mut changed_to = None; + + ui.horizontal(|ui| { + ui.colored_label(dim_text(), "模式"); + + 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); + } + }); + + changed_to +} + +fn draw_config_bar_connection( + ui: &mut egui::Ui, + config: &mut ConfigPanelState, + connection: &ConnectionManager, + recorder: &Recorder, + conn_state: ConnectionState, +) { + ui.colored_label(dim_text(), "端口"); + egui::ComboBox::from_id_salt("config_bar_ports") + .width(170.0) + .selected_text(if config.port.is_empty() { + "无可用串口".to_owned() + } else { + config.port.clone() + }) + .show_ui(ui, |ui| { + for port in &config.available_ports { + ui.selectable_value(&mut config.port, port.clone(), port.as_str()); + } + }); + + if ui + .add(style::icon_button( + egui::RichText::new("⟳").color(ONE_DARK_PRO.text).size(16.0), + egui::vec2(30.0, METRICS.field_height), + )) + .on_hover_text("刷新串口") + .clicked() + { + refresh_config_ports(config); + } + + ui.add_space(4.0); + ui.colored_label(dim_text(), "波特率"); + ui.label(style::value_text(config.baud_rate.to_string())); + + ui.add_space(4.0); + ui.colored_label(dim_text(), "状态"); + ui.colored_label( + connection_status_color(conn_state), + connection_status_text(conn_state), + ); + + ui.add_space(6.0); + + let is_connected = matches!( + conn_state, + ConnectionState::Connected | ConnectionState::Streaming + ); + let button_text = if is_connected { "断开" } else { "连接" }; + if ui + .add_sized( + egui::vec2(88.0, METRICS.button_height), + if is_connected { + style::danger_button(button_text) + } else { + style::primary_button(button_text) + }, + ) + .clicked() + { + if is_connected { + connection.disconnect(); + } else if !config.port.is_empty() { + connection.connect( + &config.port, + 12, + 7, + config.mode.baud_rate(), + config.mode.protocol(), + recorder.clone(), + ); + } + } +} + +fn draw_config_bar_rates( + ui: &mut egui::Ui, + _stats: SerialIoStats, + sample_rate_hz: f32, + render_rate_hz: f32, +) { + ui.label(style::value_text(format!("{render_rate_hz:.1} Hz"))); + ui.colored_label(dim_text(), "画面刷新率"); + ui.add_space(14.0); + ui.label(style::value_text(format!("{sample_rate_hz:.1} Hz"))); + ui.colored_label(dim_text(), "采样率"); +} + pub fn draw_spatial_force_panel( ctx: &egui::Context, force: Option, @@ -851,7 +974,7 @@ pub fn draw_stats_panel( let target_pos = egui::pos2( viewport.left(), (viewport.bottom() - panel_height - PANEL_VIEWPORT_MARGIN * 2.0) - .max(viewport.top() + layout::TITLE_BAR_HEIGHT + PANEL_VIEWPORT_MARGIN), + .max(viewport.top() + layout::WORKSPACE_TOP + PANEL_VIEWPORT_MARGIN), ); let target_x = target_pos.x; let hidden_x = if target_x < screen.center().x { @@ -924,7 +1047,7 @@ pub fn draw_hand_force_panels(ctx: &egui::Context, visible: bool, histories: &[V let left_count = 4usize; let right_count = HAND_FORCE_PANEL_TITLES.len() - left_count; let available_height = - (screen.height() - layout::TITLE_BAR_HEIGHT - vertical_margin * 2.0).max(0.0); + (screen.height() - layout::WORKSPACE_TOP - vertical_margin * 2.0).max(0.0); let panel_height = ((available_height - (left_count - 1) as f32 * gap) / left_count as f32) .clamp( HAND_FORCE_PANEL_MIN_HEIGHT.min(available_height), @@ -933,7 +1056,7 @@ pub fn draw_hand_force_panels(ctx: &egui::Context, visible: bool, histories: &[V let left_height = left_count as f32 * panel_height + (left_count - 1) as f32 * gap; let right_height = right_count as f32 * panel_height + (right_count.saturating_sub(1)) as f32 * gap; - let min_top = screen.top() + layout::TITLE_BAR_HEIGHT + vertical_margin; + let min_top = screen.top() + layout::WORKSPACE_TOP + vertical_margin; let left_top = (screen.center().y - left_height * 0.5).max(min_top); let right_top = (screen.center().y - right_height * 0.5).max(min_top);