ek_core/window.rs
1//! 插件自报的**模块窗描述**(0.5.6,「菜单自动生成」波)。
2//!
3//! 背景:界面层原先靠一张静态 `MODULES` 表决定"菜单里有哪些模块窗"
4//! (`ek-ui/src/mount.rs`)。新增一个带界面的插件就得手改那张表 ——
5//! 于是"表里有、插件没装"和"插件装了、表里忘了"两种静默故障各有一半概率。
6//!
7//! 本模块把描述权交给插件自己:插件覆写 [`Plugin::window`](crate::Plugin::window)
8//! 返回一份 [`WindowDesc`],界面层扫描到就建窗、越不到就不建。**零 egui 依赖**
9//! (纯数据),所以 ek-core 的依赖树不会被界面框架污染。
10//!
11//! 纪律(与 `kind()`/`services()` 同):本方法必须**纯/静态/无锁** —— 界面层会在
12//! 渲染线程上、且在插件起停的间隙调用它。
13
14/// 一个模块窗的自描述(纯数据,零 egui)。
15///
16/// 字段与 `ek-ui::mount::ModDesc` 一一对应;界面层拿到本类型后直接映射,
17/// 不做二次推断(避免"两处各有一套默认值"的老病)。
18///
19/// ```ignore
20/// fn window(&self) -> Option<WindowDesc> {
21/// Some(WindowDesc {
22/// id: "strategy",
23/// title: "策略交易",
24/// icon: "🎯",
25/// order: 50,
26/// size: [1180.0, 620.0],
27/// min_size: [820.0, 420.0],
28/// maximized: false,
29/// always: false,
30/// })
31/// }
32/// ```
33#[derive(Clone, Copy, Debug, PartialEq)]
34pub struct WindowDesc {
35 /// 模块名(**必须等于** [`Plugin::kind`](crate::Plugin::kind))。
36 ///
37 /// 界面层用它做去重键:同一 `kind` 出现多份描述时**以静态表为准**
38 /// (静态表是"复核过的老模块",自报是"新插件"),并记一次诊断。
39 pub id: &'static str,
40 /// 菜单项标题。
41 pub title: &'static str,
42 /// 菜单项图标(单个 emoji 或短字形;空串 = 无图标)。
43 pub icon: &'static str,
44 /// 菜单排序位(小的在前;与静态表共用同一坐标系)。
45 pub order: i32,
46 /// 首次打开的建议尺寸(像素)。
47 pub size: [f32; 2],
48 /// 最小尺寸(像素)。
49 pub min_size: [f32; 2],
50 /// 打开即最大化。
51 pub maximized: bool,
52 /// **未装配也常驻菜单**。
53 ///
54 /// `true` 只适用于"没有实例也能配置/仍有意义"的模块(全局面、连接档案等);
55 /// 依赖实例内容才有意义的模块必须给 `false` —— 因为 [`Self::always`] 为真时,
56 /// 界面层要在**没有任何插件实例**的场合建窗,那时**没有对象可供自报描述**,
57 /// 只能靠静态兜底表。两者是互斥的,别同时指望。
58 pub always: bool,
59}
60
61impl WindowDesc {
62 /// 尺寸合法性(宽高为正且 min ≤ size);不合法时界面层按静态表兜底。
63 ///
64 /// 放在契约侧而不是渲染侧,是为了让插件在单测里就能撞见自己的笔误。
65 #[must_use]
66 pub fn sizes_ok(&self) -> bool {
67 let positive =
68 |v: [f32; 2]| v[0].is_finite() && v[1].is_finite() && v[0] > 0.0 && v[1] > 0.0;
69 positive(self.size)
70 && positive(self.min_size)
71 && self.min_size[0] <= self.size[0]
72 && self.min_size[1] <= self.size[1]
73 }
74}
75
76#[cfg(test)]
77mod tests {
78 use super::*;
79
80 fn base() -> WindowDesc {
81 WindowDesc {
82 id: "strategy",
83 title: "策略交易",
84 icon: "🎯",
85 order: 50,
86 size: [1180.0, 620.0],
87 min_size: [820.0, 420.0],
88 maximized: false,
89 always: false,
90 }
91 }
92
93 #[test]
94 fn sizes_ok_accepts_well_formed() {
95 assert!(base().sizes_ok());
96 }
97
98 #[test]
99 fn sizes_ok_rejects_zero_and_inverted() {
100 let mut d = base();
101 d.size = [0.0, 100.0];
102 assert!(!d.sizes_ok(), "零宽必须拒");
103
104 let mut d = base();
105 d.min_size = [2000.0, 420.0];
106 assert!(!d.sizes_ok(), "min 宽 > size 宽必须拒");
107
108 let mut d = base();
109 d.size = [f32::NAN, 10.0];
110 assert!(!d.sizes_ok(), "NaN 必须拒");
111 }
112
113 #[test]
114 fn copy_semantics_are_cheap() {
115 // 纯数据 + Copy:界面层每帧可能要读,不该产生分配。
116 let a = base();
117 let b = a;
118 assert_eq!(a, b);
119 }
120}