Improve panel layout and per-user installer
This commit is contained in:
10
src/app.rs
10
src/app.rs
@@ -364,11 +364,11 @@ impl EskinDesktopApp {
|
||||
|
||||
fn draw_floating_panels(&mut self, ctx: &egui::Context) {
|
||||
// draw_scene_panel(ctx, &mut self.scene_panel);
|
||||
if self.config_state.mode == SerialMode::Hand && self.stats_panel.visible {
|
||||
self.config_panel.visible = false;
|
||||
self.export_panel.visible = false;
|
||||
self.matrix_config_panel.visible = false;
|
||||
}
|
||||
// if self.config_state.mode == SerialMode::Hand && self.stats_panel.visible {
|
||||
// self.config_panel.visible = false;
|
||||
// self.export_panel.visible = false;
|
||||
// self.matrix_config_panel.visible = false;
|
||||
// }
|
||||
|
||||
if let Some(next_mode) = draw_config_panel(
|
||||
ctx,
|
||||
|
||||
502
src/ui.rs
502
src/ui.rs
@@ -138,8 +138,60 @@ impl Default for ConnectPanelState {
|
||||
}
|
||||
}
|
||||
|
||||
const PANEL_VIEWPORT_MARGIN: f32 = 12.0;
|
||||
|
||||
fn viewport_panel_rect(ctx: &egui::Context) -> egui::Rect {
|
||||
ctx.content_rect().shrink(PANEL_VIEWPORT_MARGIN)
|
||||
}
|
||||
|
||||
fn responsive_panel_width(ctx: &egui::Context, preferred: f32, min_width: f32) -> f32 {
|
||||
let screen = ctx.content_rect();
|
||||
let available = (screen.width() - PANEL_VIEWPORT_MARGIN * 2.0).max(240.0);
|
||||
let side_cap = (screen.width() * 0.44).max(min_width.min(available));
|
||||
|
||||
preferred
|
||||
.min(side_cap)
|
||||
.min(available)
|
||||
.max(min_width.min(available))
|
||||
}
|
||||
|
||||
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)
|
||||
.max(min_height.min(screen.height()));
|
||||
let height_cap = (screen.height() * 0.72).max(min_height.min(available));
|
||||
|
||||
preferred
|
||||
.min(height_cap)
|
||||
.min(available)
|
||||
.max(min_height.min(available))
|
||||
}
|
||||
|
||||
fn responsive_panel_pos(
|
||||
ctx: &egui::Context,
|
||||
preferred: egui::Pos2,
|
||||
panel_width: f32,
|
||||
) -> egui::Pos2 {
|
||||
let rect = viewport_panel_rect(ctx);
|
||||
let min_y = rect.top() + layout::TITLE_BAR_HEIGHT + 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 {
|
||||
max_x
|
||||
} else {
|
||||
preferred.x.clamp(rect.left(), max_x)
|
||||
};
|
||||
let y = preferred.y.clamp(min_y, rect.bottom());
|
||||
|
||||
egui::pos2(x, y)
|
||||
}
|
||||
|
||||
fn narrow_panel(ui: &egui::Ui) -> bool {
|
||||
ui.available_width() < 420.0
|
||||
}
|
||||
|
||||
pub fn draw_scene_panel(ctx: &egui::Context, panel: &mut FloatingPanelState) {
|
||||
draw_floating_panel(ctx, panel, "场景", "scene_panel", |ui| {
|
||||
draw_floating_panel(ctx, panel, "场景", "scene_panel", 320.0, 260.0, |ui| {
|
||||
ui.horizontal(|ui| {
|
||||
ui.colored_label(dim_text(), "视图");
|
||||
let _ = ui.selectable_label(true, "簇");
|
||||
@@ -223,7 +275,7 @@ pub fn draw_connect_panel(
|
||||
// }
|
||||
|
||||
fn draw_connect_port_row(ui: &mut egui::Ui, config: &mut ConnectPanelState) {
|
||||
ui.horizontal(|ui| {
|
||||
ui.horizontal_wrapped(|ui| {
|
||||
ui.label(style::field_label("串口"));
|
||||
egui::ComboBox::from_id_salt("connect_ports")
|
||||
.width(150.0)
|
||||
@@ -264,7 +316,7 @@ fn draw_connect_port_row(ui: &mut egui::Ui, config: &mut ConnectPanelState) {
|
||||
}
|
||||
|
||||
fn draw_connect_matrix_row(ui: &mut egui::Ui, config: &mut ConnectPanelState) {
|
||||
ui.horizontal(|ui| {
|
||||
ui.horizontal_wrapped(|ui| {
|
||||
ui.checkbox(&mut config.manual, "手动矩阵");
|
||||
|
||||
ui.add_enabled_ui(config.manual, |ui| {
|
||||
@@ -291,7 +343,7 @@ fn draw_connect_action_row(
|
||||
connection: &ConnectionManager,
|
||||
recorder: &Recorder,
|
||||
) {
|
||||
ui.horizontal(|ui| {
|
||||
ui.horizontal_wrapped(|ui| {
|
||||
let status_text = match conn_state {
|
||||
ConnectionState::Disconnected => "未连接",
|
||||
ConnectionState::Connecting => "连接中...",
|
||||
@@ -343,13 +395,15 @@ pub fn draw_config_panel(
|
||||
let mut changed_mode = None;
|
||||
let conn_state = connection.state();
|
||||
let stats = connection.stats();
|
||||
let panel_width = responsive_panel_width(ctx, 560.0, 340.0);
|
||||
config.connected = matches!(
|
||||
conn_state,
|
||||
ConnectionState::Connected | ConnectionState::Streaming
|
||||
);
|
||||
|
||||
draw_floating_panel(ctx, panel, "配置", "config_panel", |ui| {
|
||||
ui.set_min_width(560.0);
|
||||
draw_floating_panel(ctx, panel, "配置", "config_panel", 560.0, 340.0, |ui| {
|
||||
ui.set_min_width(panel_width);
|
||||
ui.set_max_width(panel_width);
|
||||
|
||||
if let Some(mode) = draw_mode_row(ui, config) {
|
||||
changed_mode = Some(mode);
|
||||
@@ -369,7 +423,7 @@ pub fn draw_config_panel(
|
||||
|
||||
fn draw_mode_row(ui: &mut egui::Ui, config: &mut ConfigPanelState) -> Option<SerialMode> {
|
||||
let mut changed_to = None;
|
||||
ui.horizontal(|ui| {
|
||||
ui.horizontal_wrapped(|ui| {
|
||||
ui.colored_label(dim_text(), "模式");
|
||||
ui.add_space(12.0);
|
||||
|
||||
@@ -398,50 +452,42 @@ fn draw_connection_row(
|
||||
recorder: &Recorder,
|
||||
conn_state: ConnectionState,
|
||||
) {
|
||||
ui.horizontal(|ui| {
|
||||
ui.add(
|
||||
egui::Image::new(egui::include_image!("../static/cpu.png"))
|
||||
.fit_to_exact_size(egui::vec2(72.0, 72.0)),
|
||||
);
|
||||
let is_narrow = narrow_panel(ui);
|
||||
let draw_port_status = |ui: &mut egui::Ui, config: &mut ConfigPanelState| {
|
||||
ui.horizontal_wrapped(|ui| {
|
||||
ui.label(style::field_label("端口"));
|
||||
egui::ComboBox::from_id_salt("config_ports")
|
||||
.width(ui.available_width().min(150.0).max(104.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());
|
||||
}
|
||||
});
|
||||
|
||||
ui.add_space(METRICS.item_gap);
|
||||
|
||||
ui.vertical(|ui| {
|
||||
ui.horizontal(|ui| {
|
||||
ui.label(style::field_label("端口"));
|
||||
egui::ComboBox::from_id_salt("config_ports")
|
||||
.width(126.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),
|
||||
METRICS.icon_button,
|
||||
))
|
||||
.on_hover_text("刷新串口")
|
||||
.clicked()
|
||||
{
|
||||
refresh_config_ports(config);
|
||||
}
|
||||
});
|
||||
ui.label(style::value_text(format!("波特率 {}", config.baud_rate)));
|
||||
ui.colored_label(
|
||||
connection_status_color(conn_state),
|
||||
connection_status_text(conn_state),
|
||||
);
|
||||
if ui
|
||||
.add(style::icon_button(
|
||||
egui::RichText::new("⟳").color(ONE_DARK_PRO.text).size(16.0),
|
||||
METRICS.icon_button,
|
||||
))
|
||||
.on_hover_text("刷新串口")
|
||||
.clicked()
|
||||
{
|
||||
refresh_config_ports(config);
|
||||
}
|
||||
});
|
||||
ui.label(style::value_text(format!("波特率 {}", config.baud_rate)));
|
||||
ui.colored_label(
|
||||
connection_status_color(conn_state),
|
||||
connection_status_text(conn_state),
|
||||
);
|
||||
};
|
||||
|
||||
ui.add_space(22.0);
|
||||
|
||||
let draw_connect_button = |ui: &mut egui::Ui, config: &ConfigPanelState| {
|
||||
let is_connected = matches!(
|
||||
conn_state,
|
||||
ConnectionState::Connected | ConnectionState::Streaming
|
||||
@@ -468,22 +514,46 @@ fn draw_connection_row(
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Legacy reconnect/link-protection indicator:
|
||||
// ui.add_space(18.0);
|
||||
// ui.colored_label(
|
||||
// if config.auto_reconnect {
|
||||
// style::status_color_ok()
|
||||
// } else {
|
||||
// ONE_DARK_PRO.text_dim
|
||||
// },
|
||||
// if config.auto_reconnect {
|
||||
// "链路保护 开"
|
||||
// } else {
|
||||
// "链路保护 关"
|
||||
// },
|
||||
// );
|
||||
});
|
||||
if is_narrow {
|
||||
ui.vertical(|ui| {
|
||||
draw_port_status(ui, config);
|
||||
ui.add_space(METRICS.item_gap);
|
||||
draw_connect_button(ui, config);
|
||||
});
|
||||
} else {
|
||||
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(METRICS.item_gap);
|
||||
|
||||
ui.vertical(|ui| {
|
||||
draw_port_status(ui, config);
|
||||
});
|
||||
|
||||
ui.add_space(22.0);
|
||||
draw_connect_button(ui, config);
|
||||
});
|
||||
}
|
||||
|
||||
// Legacy reconnect/link-protection indicator:
|
||||
// ui.add_space(18.0);
|
||||
// ui.colored_label(
|
||||
// if config.auto_reconnect {
|
||||
// style::status_color_ok()
|
||||
// } else {
|
||||
// ONE_DARK_PRO.text_dim
|
||||
// },
|
||||
// if config.auto_reconnect {
|
||||
// "链路保护 开"
|
||||
// } else {
|
||||
// "链路保护 关"
|
||||
// },
|
||||
// );
|
||||
}
|
||||
|
||||
fn draw_serial_grid(ui: &mut egui::Ui, config: &mut ConfigPanelState) {
|
||||
@@ -584,7 +654,7 @@ fn draw_mode_body(
|
||||
}
|
||||
|
||||
fn draw_status_bytes_row(ui: &mut egui::Ui, conn_state: ConnectionState, stats: SerialIoStats) {
|
||||
ui.horizontal(|ui| {
|
||||
ui.horizontal_wrapped(|ui| {
|
||||
ui.colored_label(dim_text(), "状态");
|
||||
ui.label(connection_status_text(conn_state));
|
||||
ui.colored_label(dim_text(), "接收");
|
||||
@@ -704,8 +774,8 @@ pub fn draw_stats_panel(
|
||||
force_history: &[f32],
|
||||
_spatial_force: Option<HudSpatialForce>,
|
||||
) {
|
||||
const PANEL_WIDTH: f32 = 380.0;
|
||||
const PANEL_HEIGHT: f32 = 340.0;
|
||||
const PREFERRED_PANEL_WIDTH: f32 = 380.0;
|
||||
const PREFERRED_PANEL_HEIGHT: f32 = 340.0;
|
||||
const PANEL_OUTSIDE_GAP: f32 = 18.0;
|
||||
|
||||
let force_active = has_recent_resultant_force(force_history);
|
||||
@@ -723,24 +793,34 @@ pub fn draw_stats_panel(
|
||||
|
||||
let eased = ease_in_out(anim);
|
||||
let screen = ctx.content_rect();
|
||||
let target_x = panel.default_pos.x;
|
||||
let panel_width = responsive_panel_width(ctx, PREFERRED_PANEL_WIDTH, 260.0);
|
||||
let panel_height = responsive_panel_height(ctx, PREFERRED_PANEL_HEIGHT, 220.0);
|
||||
let viewport = viewport_panel_rect(ctx);
|
||||
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),
|
||||
);
|
||||
let target_x = target_pos.x;
|
||||
let hidden_x = if target_x < screen.center().x {
|
||||
screen.left() - PANEL_WIDTH - PANEL_OUTSIDE_GAP
|
||||
screen.left() - panel_width - PANEL_OUTSIDE_GAP
|
||||
} else {
|
||||
screen.right() + PANEL_OUTSIDE_GAP
|
||||
};
|
||||
let x = egui::lerp(hidden_x..=target_x, eased);
|
||||
let y = panel.default_pos.y;
|
||||
let y = target_pos.y;
|
||||
|
||||
egui::Area::new(egui::Id::new("stats_panel"))
|
||||
.fixed_pos(egui::pos2(x, y))
|
||||
.constrain_to(viewport_panel_rect(ctx))
|
||||
.order(egui::Order::Foreground)
|
||||
.show(ctx, |ui| {
|
||||
panel_frame(ctx).show(ui, |ui| {
|
||||
ui.spacing_mut().item_spacing = egui::vec2(METRICS.item_gap, 6.0);
|
||||
ui.set_min_width(PANEL_WIDTH);
|
||||
ui.set_min_height(PANEL_HEIGHT);
|
||||
draw_force_chart_panel_contents(ui, "RF", "合力", force_history, PANEL_HEIGHT);
|
||||
ui.set_min_width(panel_width);
|
||||
ui.set_max_width(panel_width);
|
||||
ui.set_min_height(panel_height);
|
||||
draw_force_chart_panel_contents(ui, "RF", "合力", force_history, panel_height);
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -749,9 +829,9 @@ const FORCE_CHART_MAX_N: f32 = 25.6;
|
||||
const FORCE_CHART_SAMPLE_SECONDS: f32 = 0.1;
|
||||
const FORCE_PANEL_ACTIVE_TAIL: usize = 8;
|
||||
|
||||
const HAND_FORCE_PANEL_MIN_WIDTH: f32 = 280.0;
|
||||
const HAND_FORCE_PANEL_MIN_WIDTH: f32 = 220.0;
|
||||
const HAND_FORCE_PANEL_MAX_WIDTH: f32 = 500.0;
|
||||
const HAND_FORCE_PANEL_MIN_HEIGHT: f32 = 140.0;
|
||||
const HAND_FORCE_PANEL_MIN_HEIGHT: f32 = 104.0;
|
||||
const HAND_FORCE_PANEL_MAX_HEIGHT: f32 = 190.0;
|
||||
const HAND_FORCE_PANEL_GAP: f32 = 12.0;
|
||||
const HAND_FORCE_PANEL_SIDE_MARGIN: f32 = 24.0;
|
||||
@@ -780,41 +860,41 @@ pub fn draw_hand_force_panels(ctx: &egui::Context, visible: bool, histories: &[V
|
||||
.any(|history| has_recent_resultant_force(history));
|
||||
let target_visible = visible && hand_active;
|
||||
let screen = ctx.content_rect();
|
||||
let side_width = ((screen.width() - HAND_FORCE_PANEL_SIDE_MARGIN * 4.0) * 0.25).max(0.0);
|
||||
let side_margin = HAND_FORCE_PANEL_SIDE_MARGIN.min((screen.width() * 0.025).max(10.0));
|
||||
let vertical_margin = HAND_FORCE_PANEL_VERTICAL_MARGIN.min((screen.height() * 0.04).max(10.0));
|
||||
let gap = HAND_FORCE_PANEL_GAP.min((screen.height() * 0.018).max(6.0));
|
||||
let side_width = ((screen.width() - side_margin * 4.0) * 0.25).max(0.0);
|
||||
let panel_width = side_width
|
||||
.min(HAND_FORCE_PANEL_MAX_WIDTH)
|
||||
.max(HAND_FORCE_PANEL_MIN_WIDTH.min(side_width));
|
||||
let left_x = screen.left() + HAND_FORCE_PANEL_SIDE_MARGIN;
|
||||
let right_x = screen.right() - HAND_FORCE_PANEL_SIDE_MARGIN - panel_width;
|
||||
let left_x = screen.left() + side_margin;
|
||||
let right_x = screen.right() - side_margin - panel_width;
|
||||
let left_count = 4usize;
|
||||
let right_count = HAND_FORCE_PANEL_TITLES.len() - left_count;
|
||||
let available_height = (screen.height() - HAND_FORCE_PANEL_VERTICAL_MARGIN * 2.0).max(0.0);
|
||||
let panel_height = ((available_height - (left_count - 1) as f32 * HAND_FORCE_PANEL_GAP)
|
||||
/ left_count as f32)
|
||||
let available_height =
|
||||
(screen.height() - layout::TITLE_BAR_HEIGHT - 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),
|
||||
HAND_FORCE_PANEL_MAX_HEIGHT,
|
||||
);
|
||||
let left_height =
|
||||
left_count as f32 * panel_height + (left_count - 1) as f32 * HAND_FORCE_PANEL_GAP;
|
||||
let right_height = right_count as f32 * panel_height
|
||||
+ (right_count.saturating_sub(1)) as f32 * HAND_FORCE_PANEL_GAP;
|
||||
let left_top = screen.center().y - left_height * 0.5;
|
||||
let right_top = screen.center().y - right_height * 0.5;
|
||||
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 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);
|
||||
|
||||
for index in 0..HAND_FORCE_PANEL_TITLES.len() {
|
||||
let history = histories.get(index).map(Vec::as_slice).unwrap_or(&[]);
|
||||
let (code, title) = HAND_FORCE_PANEL_TITLES[index];
|
||||
let (target_x, target_y) = if index < left_count {
|
||||
(
|
||||
left_x,
|
||||
left_top + index as f32 * (panel_height + HAND_FORCE_PANEL_GAP),
|
||||
)
|
||||
(left_x, left_top + index as f32 * (panel_height + gap))
|
||||
} else {
|
||||
let right_index = index - left_count;
|
||||
(
|
||||
right_x,
|
||||
right_top + right_index as f32 * (panel_height + HAND_FORCE_PANEL_GAP),
|
||||
right_top + right_index as f32 * (panel_height + gap),
|
||||
)
|
||||
};
|
||||
|
||||
@@ -858,11 +938,13 @@ fn draw_force_chart_area(
|
||||
|
||||
egui::Area::new(id)
|
||||
.fixed_pos(egui::pos2(target_pos.x, y))
|
||||
.constrain_to(viewport_panel_rect(ctx))
|
||||
.order(egui::Order::Foreground)
|
||||
.show(ctx, |ui| {
|
||||
panel_frame(ctx).show(ui, |ui| {
|
||||
ui.spacing_mut().item_spacing = egui::vec2(METRICS.item_gap, 6.0);
|
||||
ui.set_min_width(size.x);
|
||||
ui.set_max_width(size.x);
|
||||
ui.set_min_height(size.y);
|
||||
draw_force_chart_panel_contents(ui, code, title, values, size.y);
|
||||
});
|
||||
@@ -895,7 +977,7 @@ fn paint_resultant_force_chart(ui: &mut egui::Ui, values: &[f32], chart_height:
|
||||
const CHART_POINTS: usize = 42;
|
||||
const CHART_WINDOW_SECONDS: f32 = CHART_POINTS as f32 * FORCE_CHART_SAMPLE_SECONDS;
|
||||
|
||||
let width = ui.available_width().max(280.0);
|
||||
let width = ui.available_width().max(180.0);
|
||||
let (rect, _) = ui.allocate_exact_size(egui::vec2(width, chart_height), egui::Sense::hover());
|
||||
let painter = ui.painter_at(rect);
|
||||
let radius = egui::CornerRadius::same(5);
|
||||
@@ -949,7 +1031,6 @@ fn paint_resultant_force_chart(ui: &mut egui::Ui, values: &[f32], chart_height:
|
||||
}
|
||||
|
||||
let plot_painter = painter.with_clip_rect(chart_rect.expand2(egui::vec2(1.0, 1.0)));
|
||||
paint_force_sweep(&plot_painter, chart_rect, time);
|
||||
paint_force_area(&plot_painter, &points, plot_rect);
|
||||
|
||||
if points.len() >= 2 {
|
||||
@@ -1036,28 +1117,6 @@ fn paint_force_grid(
|
||||
}
|
||||
}
|
||||
|
||||
fn paint_force_sweep(painter: &egui::Painter, chart_rect: egui::Rect, time: f32) {
|
||||
let sweep = (time * 0.55).fract();
|
||||
let x = chart_rect.left() + sweep * chart_rect.width();
|
||||
let sweep_rect = egui::Rect::from_min_max(
|
||||
egui::pos2(x - 8.0, chart_rect.top()),
|
||||
egui::pos2(x + 8.0, chart_rect.bottom()),
|
||||
);
|
||||
|
||||
painter.rect_filled(
|
||||
sweep_rect,
|
||||
egui::CornerRadius::ZERO,
|
||||
color_alpha(ONE_DARK_PRO.accent, 20),
|
||||
);
|
||||
painter.line_segment(
|
||||
[
|
||||
egui::pos2(x, chart_rect.top()),
|
||||
egui::pos2(x, chart_rect.bottom()),
|
||||
],
|
||||
egui::Stroke::new(1.0, color_alpha(ACCENT_GREEN, 95)),
|
||||
);
|
||||
}
|
||||
|
||||
fn paint_force_area(painter: &egui::Painter, points: &[egui::Pos2], plot_rect: egui::Rect) {
|
||||
if points.len() < 2 {
|
||||
return;
|
||||
@@ -1090,22 +1149,32 @@ fn draw_floating_panel(
|
||||
panel: &mut FloatingPanelState,
|
||||
title: &'static str,
|
||||
id: &'static str,
|
||||
preferred_width: f32,
|
||||
min_width: f32,
|
||||
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 panel_width = responsive_panel_width(ctx, preferred_width, min_width);
|
||||
let max_height = responsive_panel_height(ctx, ctx.content_rect().height(), 180.0);
|
||||
let default_pos = responsive_panel_pos(ctx, panel.default_pos, panel_width);
|
||||
|
||||
let window_response = egui::Window::new(title)
|
||||
.id(egui::Id::new(id))
|
||||
.open(&mut open)
|
||||
.default_pos(panel.default_pos)
|
||||
.default_pos(default_pos)
|
||||
.max_width(panel_width)
|
||||
.max_height(max_height)
|
||||
.constrain_to(viewport_panel_rect(ctx))
|
||||
.title_bar(false)
|
||||
.resizable(true)
|
||||
.frame(panel_frame(ctx))
|
||||
.show(ctx, |ui| {
|
||||
ui.spacing_mut().item_spacing = egui::vec2(METRICS.item_gap, 6.0);
|
||||
ui.set_min_width(panel_width);
|
||||
ui.set_max_width(panel_width);
|
||||
ui.horizontal(|ui| {
|
||||
if ui
|
||||
.add(rich_tag_button("隐藏", style::ONE_DARK_PRO.text_dim))
|
||||
@@ -1117,7 +1186,15 @@ fn draw_floating_panel(
|
||||
ui.label(style::panel_title(title));
|
||||
});
|
||||
ui.separator();
|
||||
add_contents(ui);
|
||||
let content_max_height = (max_height - 60.0).max(120.0);
|
||||
egui::ScrollArea::vertical()
|
||||
.max_height(content_max_height)
|
||||
.auto_shrink([false, true])
|
||||
.show(ui, |ui| {
|
||||
ui.set_min_width(panel_width);
|
||||
ui.set_max_width(panel_width);
|
||||
add_contents(ui);
|
||||
});
|
||||
});
|
||||
|
||||
if let Some(response) = window_response {
|
||||
@@ -1172,9 +1249,10 @@ fn draw_center_floating_panel(
|
||||
collapsed_label: &'static str,
|
||||
add_contents: impl FnOnce(&mut egui::Ui),
|
||||
) {
|
||||
const PANEL_WIDTH: f32 = 520.0;
|
||||
const PREFERRED_PANEL_WIDTH: f32 = 520.0;
|
||||
const SLIDE_DISTANCE: f32 = 18.0;
|
||||
|
||||
let panel_width = responsive_panel_width(ctx, PREFERRED_PANEL_WIDTH, 320.0);
|
||||
let anim = advance_center_panel_anim(ctx, panel);
|
||||
let eased = ease_in_out(anim);
|
||||
|
||||
@@ -1206,8 +1284,8 @@ fn draw_center_floating_panel(
|
||||
)
|
||||
.order(egui::Order::Tooltip)
|
||||
.show(ctx, |ui| {
|
||||
let response = center_panel_shell(ui, PANEL_WIDTH, |ui| {
|
||||
ui.set_width(PANEL_WIDTH);
|
||||
let response = center_panel_shell(ui, panel_width, |ui| {
|
||||
ui.set_width(panel_width);
|
||||
add_contents(ui);
|
||||
ui.add_space(6.0);
|
||||
let handle_rect = allocate_center_panel_handle(ui);
|
||||
@@ -1540,7 +1618,7 @@ pub fn draw_recording_toolbar(ui: &mut egui::Ui, recorder: &Recorder, export_pat
|
||||
let duration_ms = recorder.duration_ms();
|
||||
|
||||
ui.separator();
|
||||
ui.horizontal(|ui| {
|
||||
ui.horizontal_wrapped(|ui| {
|
||||
ui.colored_label(ACCENT_RED, "● REC");
|
||||
ui.colored_label(
|
||||
ONE_DARK_PRO.text,
|
||||
@@ -1548,7 +1626,7 @@ pub fn draw_recording_toolbar(ui: &mut egui::Ui, recorder: &Recorder, export_pat
|
||||
);
|
||||
});
|
||||
|
||||
ui.horizontal(|ui| {
|
||||
ui.horizontal_wrapped(|ui| {
|
||||
// Full recording
|
||||
let rec_btn = if is_recording {
|
||||
tag_button("⏹ 停止")
|
||||
@@ -1634,16 +1712,26 @@ pub fn draw_recording_toolbar(ui: &mut egui::Ui, recorder: &Recorder, export_pat
|
||||
});
|
||||
|
||||
// Export path
|
||||
ui.horizontal(|ui| {
|
||||
ui.colored_label(dim_text(), "导出路径");
|
||||
ui.add_sized(
|
||||
egui::vec2(
|
||||
(ui.available_width() - 80.0).max(160.0),
|
||||
METRICS.field_height,
|
||||
),
|
||||
egui::TextEdit::singleline(export_path).hint_text("eskin_export_*.csv"),
|
||||
);
|
||||
});
|
||||
if narrow_panel(ui) {
|
||||
ui.vertical(|ui| {
|
||||
ui.colored_label(dim_text(), "导出路径");
|
||||
ui.add_sized(
|
||||
egui::vec2(ui.available_width().max(180.0), METRICS.field_height),
|
||||
egui::TextEdit::singleline(export_path).hint_text("eskin_export_*.csv"),
|
||||
);
|
||||
});
|
||||
} else {
|
||||
ui.horizontal(|ui| {
|
||||
ui.colored_label(dim_text(), "导出路径");
|
||||
ui.add_sized(
|
||||
egui::vec2(
|
||||
(ui.available_width() - 80.0).max(160.0),
|
||||
METRICS.field_height,
|
||||
),
|
||||
egui::TextEdit::singleline(export_path).hint_text("eskin_export_*.csv"),
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// ── Export Panel (floating) ────────────────────────────────────────────
|
||||
@@ -1655,10 +1743,20 @@ pub fn draw_export_panel(
|
||||
recorder: &Recorder,
|
||||
export_path: &mut String,
|
||||
) {
|
||||
draw_floating_panel(ctx, panel, "录制导出", "export_panel", |ui| {
|
||||
ui.set_min_width(300.0);
|
||||
draw_recording_toolbar(ui, recorder, export_path);
|
||||
});
|
||||
let panel_width = responsive_panel_width(ctx, 340.0, 280.0);
|
||||
draw_floating_panel(
|
||||
ctx,
|
||||
panel,
|
||||
"录制导出",
|
||||
"export_panel",
|
||||
340.0,
|
||||
280.0,
|
||||
|ui| {
|
||||
ui.set_min_width(panel_width);
|
||||
ui.set_max_width(panel_width);
|
||||
draw_recording_toolbar(ui, recorder, export_path);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
// ── Matrix Config Panel ────────────────────────────────────────────────
|
||||
@@ -1687,75 +1785,85 @@ pub fn draw_matrix_config_panel(
|
||||
panel: &mut FloatingPanelState,
|
||||
config: &mut MatrixConfigState,
|
||||
) {
|
||||
draw_floating_panel(ctx, panel, "矩阵配置", "matrix_config_panel", |ui| {
|
||||
ui.set_min_width(280.0);
|
||||
let panel_width = responsive_panel_width(ctx, 380.0, 280.0);
|
||||
draw_floating_panel(
|
||||
ctx,
|
||||
panel,
|
||||
"矩阵配置",
|
||||
"matrix_config_panel",
|
||||
380.0,
|
||||
280.0,
|
||||
|ui| {
|
||||
ui.set_min_width(panel_width);
|
||||
ui.set_max_width(panel_width);
|
||||
|
||||
ui.horizontal_wrapped(|ui| {
|
||||
ui.label(style::field_label("矩阵尺寸"));
|
||||
ui.add_space(METRICS.item_gap);
|
||||
ui.label(style::value_text("行"));
|
||||
ui.add_sized(
|
||||
egui::vec2(56.0, METRICS.field_height),
|
||||
egui::DragValue::new(&mut config.rows).range(1..=128),
|
||||
);
|
||||
ui.label(style::value_text("列"));
|
||||
ui.add_sized(
|
||||
egui::vec2(56.0, METRICS.field_height),
|
||||
egui::DragValue::new(&mut config.cols).range(1..=128),
|
||||
);
|
||||
});
|
||||
|
||||
ui.horizontal(|ui| {
|
||||
ui.label(style::field_label("矩阵尺寸"));
|
||||
ui.add_space(METRICS.item_gap);
|
||||
ui.label(style::value_text("行"));
|
||||
ui.add_sized(
|
||||
egui::vec2(56.0, METRICS.field_height),
|
||||
egui::DragValue::new(&mut config.rows).range(1..=128),
|
||||
);
|
||||
ui.label(style::value_text("列"));
|
||||
ui.add_sized(
|
||||
egui::vec2(56.0, METRICS.field_height),
|
||||
egui::DragValue::new(&mut config.cols).range(1..=128),
|
||||
);
|
||||
});
|
||||
|
||||
ui.add_space(METRICS.item_gap);
|
||||
|
||||
ui.horizontal(|ui| {
|
||||
ui.label(style::field_label("预设"));
|
||||
for (r, c, name) in &[
|
||||
(12, 7, "12×7"),
|
||||
(24, 12, "24×12"),
|
||||
(32, 16, "32×16"),
|
||||
(48, 24, "48×24"),
|
||||
(64, 32, "64×32"),
|
||||
] {
|
||||
if ui
|
||||
.add(rich_tag_button(*name, style::ONE_DARK_PRO.text_dim))
|
||||
.clicked()
|
||||
{
|
||||
config.rows = *r;
|
||||
config.cols = *c;
|
||||
ui.horizontal_wrapped(|ui| {
|
||||
ui.label(style::field_label("预设"));
|
||||
for (r, c, name) in &[
|
||||
(12, 7, "12×7"),
|
||||
(24, 12, "24×12"),
|
||||
(32, 16, "32×16"),
|
||||
(48, 24, "48×24"),
|
||||
(64, 32, "64×32"),
|
||||
] {
|
||||
if ui
|
||||
.add(rich_tag_button(*name, style::ONE_DARK_PRO.text_dim))
|
||||
.clicked()
|
||||
{
|
||||
config.rows = *r;
|
||||
config.cols = *c;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
ui.add_space(METRICS.item_gap);
|
||||
|
||||
ui.horizontal(|ui| {
|
||||
ui.label(style::field_label("色域范围"));
|
||||
ui.add_space(METRICS.item_gap);
|
||||
ui.label(style::value_text("最小"));
|
||||
ui.add_sized(
|
||||
egui::vec2(72.0, METRICS.field_height),
|
||||
egui::DragValue::new(&mut config.color_min)
|
||||
.range(0.0..=10000.0)
|
||||
.speed(100.0),
|
||||
);
|
||||
ui.label(style::value_text("最大"));
|
||||
ui.add_sized(
|
||||
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(METRICS.row_gap);
|
||||
ui.horizontal_wrapped(|ui| {
|
||||
ui.label(style::field_label("色域范围"));
|
||||
ui.add_space(METRICS.item_gap);
|
||||
ui.label(style::value_text("最小"));
|
||||
ui.add_sized(
|
||||
egui::vec2(72.0, METRICS.field_height),
|
||||
egui::DragValue::new(&mut config.color_min)
|
||||
.range(0.0..=10000.0)
|
||||
.speed(100.0),
|
||||
);
|
||||
ui.label(style::value_text("最大"));
|
||||
ui.add_sized(
|
||||
egui::vec2(72.0, METRICS.field_height),
|
||||
egui::DragValue::new(&mut config.color_max)
|
||||
.range(1.0..=10000.0)
|
||||
.speed(100.0),
|
||||
);
|
||||
});
|
||||
|
||||
if ui
|
||||
.add(rich_tag_button("重置默认", style::ONE_DARK_PRO.text_dim))
|
||||
.clicked()
|
||||
{
|
||||
*config = MatrixConfigState::default();
|
||||
}
|
||||
});
|
||||
ui.add_space(METRICS.row_gap);
|
||||
|
||||
if ui
|
||||
.add(rich_tag_button("重置默认", style::ONE_DARK_PRO.text_dim))
|
||||
.clicked()
|
||||
{
|
||||
*config = MatrixConfigState::default();
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
pub fn panel_restore_item(ui: &mut egui::Ui, title: &'static str, panel: &mut FloatingPanelState) {
|
||||
|
||||
Reference in New Issue
Block a user