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    /// 0.8.0 配色双档:选单窗的档(插件还没起来,故在建构时读一次存储根;
93    /// **建构时读一次,不是每帧读** —— 渲染路径里不许有文件 IO)。
94    theme: crate::tokens::Theme,
95}
96
97impl PickApp {
98    /// 全量可勾选 + 全不勾初始态。
99    pub fn new(program: PathBuf, shared: Arc<RunShared>) -> Self {
100        let kinds = pick_kinds();
101        let selected = vec![false; kinds.len()];
102        Self {
103            kinds,
104            selected,
105            program,
106            shared,
107            spawned: false,
108            cancel: false,
109            theme: crate::plugin::load_theme(),
110        }
111    }
112}
113
114impl eframe::App for PickApp {
115    fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) {
116        // BD-UI-32:选单窗与主窗同字号(整体口径);0.8.0 起连档也同源(建构时读一次)。
117        crate::render::apply_ui_style(ctx, self.theme);
118        let kinds = self.kinds.clone();
119        let mut req_launch = false;
120        let mut req_cancel = false;
121        egui::CentralPanel::default().show(ctx, |ui| {
122            crate::launcher_ui::render_pick(
123                ui,
124                &mut self.selected,
125                &kinds,
126                &mut || req_launch = true,
127                &mut || req_cancel = true,
128            );
129        });
130
131        if req_cancel {
132            self.cancel = true;
133        }
134        if req_launch && !self.spawned {
135            let picked: Vec<String> = self
136                .kinds
137                .iter()
138                .zip(&self.selected)
139                .filter(|(_, ok)| **ok)
140                .map(|(k, _)| k.0.clone())
141                .collect();
142            if !picked.is_empty() {
143                self.spawned = true;
144                let shared = Arc::clone(&self.shared);
145                let program = self.program.clone();
146                let worker_names = picked;
147                let h = std::thread::spawn(move || {
148                    launch_worker(&shared, &worker_names, program);
149                });
150                *self.shared.handle.lock().unwrap() = Some(h);
151            }
152        }
153
154        if self.spawned {
155            let done = self.shared.outcome.lock().unwrap().is_some();
156            if done {
157                ctx.send_viewport_cmd(egui::ViewportCommand::Close); // 收场交给 run_pick_launcher
158            } else {
159                ctx.request_repaint_after(POLL);
160            }
161        }
162        if self.cancel {
163            ctx.send_viewport_cmd(egui::ViewportCommand::Close);
164        }
165        if !self.spawned {
166            ctx.request_repaint_after(Duration::from_millis(500));
167        }
168    }
169}