Skip to main content

ek_ui/
launcher_ui.rs

1//! pick 窗体:纯渲染入口(L2/kittest 目标)+ `PickApp`(eframe App 状态机)。
2//!
3//! 渲染无副作用(回调注入);启动动作由 [`crate::launcher`] 主流程接线。
4//! kittest 策略:`Harness::new_ui_state(State = Vec<bool>)` —— 点击即翻位,
5//! 窗口自闭环。
6
7use std::path::PathBuf;
8use std::sync::Arc;
9use std::time::Duration;
10
11use ek_station::default_kinds;
12
13use crate::launcher::{POLL, RunShared, launch_worker};
14use crate::plugin::UI_NAME;
15
16/// 启动按钮标签(测试与 UI 同源)。
17pub const BTN_LAUNCH: &str = "启动";
18/// 取消按钮标签。
19pub const BTN_CANCEL: &str = "取消";
20/// 无选择时的禁用提示文案(摘要行)。
21const PICKED_NONE: &str = "已选: (无)";
22
23/// pick 表:ek-station 缺省工厂(0.1.12 起 = dm/oms/ctp 三项,sim 已退场)+ ui 工厂(名称,一行说明)。
24#[must_use]
25pub fn pick_kinds() -> Vec<(String, String)> {
26    let mut list = default_kinds()
27        .into_iter()
28        .map(|k| (k.name.to_string(), k.doc.to_string()))
29        .collect::<Vec<_>>();
30    list.push((UI_NAME.to_string(), "常驻观测界面(加载即开窗)".to_string()));
31    list
32}
33
34/// 画 pick 窗口体(不建窗;调用方自定容器)。
35///
36/// `selected.len()` 必须与 `kinds.len()` 同长(调用方初始化);空选启动钮禁用。
37pub fn render_pick(
38    ui: &mut egui::Ui,
39    selected: &mut [bool],
40    kinds: &[(String, String)],
41    on_launch: &mut dyn FnMut(),
42    on_cancel: &mut dyn FnMut(),
43) {
44    ui.heading("ek-station 启动选择");
45    ui.label("勾选要装载的插件;启动写入清单并拉起 ek-ui(ui 自动补上)。");
46    ui.separator();
47
48    egui::ScrollArea::vertical()
49        .max_height(240.0)
50        .show(ui, |ui| {
51            for (i, (name, doc)) in kinds.iter().enumerate() {
52                ui.horizontal(|ui| {
53                    ui.checkbox(&mut selected[i], name);
54                    let _ = ui.label(doc.clone());
55                });
56            }
57        });
58
59    ui.separator();
60    let picked: Vec<String> = kinds
61        .iter()
62        .zip(selected)
63        .filter(|(_, ok)| **ok)
64        .map(|(k, _)| k.0.clone())
65        .collect();
66    if picked.is_empty() {
67        ui.label(PICKED_NONE);
68    } else {
69        ui.label(format!("已选: {}", picked.join(", ")));
70    }
71    ui.horizontal(|ui| {
72        if ui
73            .add_enabled(!picked.is_empty(), egui::Button::new(BTN_LAUNCH))
74            .clicked()
75        {
76            on_launch();
77        }
78        if ui.button(BTN_CANCEL).clicked() {
79            on_cancel();
80        }
81    });
82}
83
84/// pick 窗 App(UI 线程):勾选 → 按钮/取消 → 触发 worker → 见果关窗。
85pub struct PickApp {
86    kinds: Vec<(String, String)>,
87    selected: Vec<bool>,
88    program: PathBuf,
89    shared: Arc<RunShared>,
90    spawned: bool,
91    cancel: bool,
92}
93
94impl PickApp {
95    /// 全量可勾选 + 全不勾初始态。
96    pub fn new(program: PathBuf, shared: Arc<RunShared>) -> Self {
97        let kinds = pick_kinds();
98        let selected = vec![false; kinds.len()];
99        Self {
100            kinds,
101            selected,
102            program,
103            shared,
104            spawned: false,
105            cancel: false,
106        }
107    }
108}
109
110impl eframe::App for PickApp {
111    fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) {
112        crate::render::apply_ui_style(ctx); // BD-UI-32:选单窗与主窗同字号(整体口径)
113        let kinds = self.kinds.clone();
114        let mut req_launch = false;
115        let mut req_cancel = false;
116        egui::CentralPanel::default().show(ctx, |ui| {
117            crate::launcher_ui::render_pick(
118                ui,
119                &mut self.selected,
120                &kinds,
121                &mut || req_launch = true,
122                &mut || req_cancel = true,
123            );
124        });
125
126        if req_cancel {
127            self.cancel = true;
128        }
129        if req_launch && !self.spawned {
130            let picked: Vec<String> = self
131                .kinds
132                .iter()
133                .zip(&self.selected)
134                .filter(|(_, ok)| **ok)
135                .map(|(k, _)| k.0.clone())
136                .collect();
137            if !picked.is_empty() {
138                self.spawned = true;
139                let shared = Arc::clone(&self.shared);
140                let program = self.program.clone();
141                let worker_names = picked;
142                let h = std::thread::spawn(move || {
143                    launch_worker(&shared, &worker_names, program);
144                });
145                *self.shared.handle.lock().unwrap() = Some(h);
146            }
147        }
148
149        if self.spawned {
150            let done = self.shared.outcome.lock().unwrap().is_some();
151            if done {
152                ctx.send_viewport_cmd(egui::ViewportCommand::Close); // 收场交给 run_pick_launcher
153            } else {
154                ctx.request_repaint_after(POLL);
155            }
156        }
157        if self.cancel {
158            ctx.send_viewport_cmd(egui::ViewportCommand::Close);
159        }
160        if !self.spawned {
161            ctx.request_repaint_after(Duration::from_millis(500));
162        }
163    }
164}