Skip to main content

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}