ek-core 0.5.6

Business-agnostic plugin host and actor-over-bus runtime on top of ek-event: capability index with runtime service discovery, per-plugin actor workers, dynamic add/remove with lifecycle broadcasts, one-shot ask/ask_service with contract gates, panic quarantine, five-step close.
Documentation
//! 插件自报的**模块窗描述**(0.5.6,「菜单自动生成」波)。
//!
//! 背景:界面层原先靠一张静态 `MODULES` 表决定"菜单里有哪些模块窗"
//! (`ek-ui/src/mount.rs`)。新增一个带界面的插件就得手改那张表 ——
//! 于是"表里有、插件没装"和"插件装了、表里忘了"两种静默故障各有一半概率。
//!
//! 本模块把描述权交给插件自己:插件覆写 [`Plugin::window`](crate::Plugin::window)
//! 返回一份 [`WindowDesc`],界面层扫描到就建窗、越不到就不建。**零 egui 依赖**
//! (纯数据),所以 ek-core 的依赖树不会被界面框架污染。
//!
//! 纪律(与 `kind()`/`services()` 同):本方法必须**纯/静态/无锁** —— 界面层会在
//! 渲染线程上、且在插件起停的间隙调用它。

/// 一个模块窗的自描述(纯数据,零 egui)。
///
/// 字段与 `ek-ui::mount::ModDesc` 一一对应;界面层拿到本类型后直接映射,
/// 不做二次推断(避免"两处各有一套默认值"的老病)。
///
/// ```ignore
/// fn window(&self) -> Option<WindowDesc> {
///     Some(WindowDesc {
///         id: "strategy",
///         title: "策略交易",
///         icon: "🎯",
///         order: 50,
///         size: [1180.0, 620.0],
///         min_size: [820.0, 420.0],
///         maximized: false,
///         always: false,
///     })
/// }
/// ```
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct WindowDesc {
    /// 模块名(**必须等于** [`Plugin::kind`](crate::Plugin::kind))。
    ///
    /// 界面层用它做去重键:同一 `kind` 出现多份描述时**以静态表为准**
    /// (静态表是"复核过的老模块",自报是"新插件"),并记一次诊断。
    pub id: &'static str,
    /// 菜单项标题。
    pub title: &'static str,
    /// 菜单项图标(单个 emoji 或短字形;空串 = 无图标)。
    pub icon: &'static str,
    /// 菜单排序位(小的在前;与静态表共用同一坐标系)。
    pub order: i32,
    /// 首次打开的建议尺寸(像素)。
    pub size: [f32; 2],
    /// 最小尺寸(像素)。
    pub min_size: [f32; 2],
    /// 打开即最大化。
    pub maximized: bool,
    /// **未装配也常驻菜单**。
    ///
    /// `true` 只适用于"没有实例也能配置/仍有意义"的模块(全局面、连接档案等);
    /// 依赖实例内容才有意义的模块必须给 `false` —— 因为 [`Self::always`] 为真时,
    /// 界面层要在**没有任何插件实例**的场合建窗,那时**没有对象可供自报描述**,
    /// 只能靠静态兜底表。两者是互斥的,别同时指望。
    pub always: bool,
}

impl WindowDesc {
    /// 尺寸合法性(宽高为正且 min ≤ size);不合法时界面层按静态表兜底。
    ///
    /// 放在契约侧而不是渲染侧,是为了让插件在单测里就能撞见自己的笔误。
    #[must_use]
    pub fn sizes_ok(&self) -> bool {
        let positive =
            |v: [f32; 2]| v[0].is_finite() && v[1].is_finite() && v[0] > 0.0 && v[1] > 0.0;
        positive(self.size)
            && positive(self.min_size)
            && self.min_size[0] <= self.size[0]
            && self.min_size[1] <= self.size[1]
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    fn base() -> WindowDesc {
        WindowDesc {
            id: "strategy",
            title: "策略交易",
            icon: "🎯",
            order: 50,
            size: [1180.0, 620.0],
            min_size: [820.0, 420.0],
            maximized: false,
            always: false,
        }
    }

    #[test]
    fn sizes_ok_accepts_well_formed() {
        assert!(base().sizes_ok());
    }

    #[test]
    fn sizes_ok_rejects_zero_and_inverted() {
        let mut d = base();
        d.size = [0.0, 100.0];
        assert!(!d.sizes_ok(), "零宽必须拒");

        let mut d = base();
        d.min_size = [2000.0, 420.0];
        assert!(!d.sizes_ok(), "min 宽 > size 宽必须拒");

        let mut d = base();
        d.size = [f32::NAN, 10.0];
        assert!(!d.sizes_ok(), "NaN 必须拒");
    }

    #[test]
    fn copy_semantics_are_cheap() {
        // 纯数据 + Copy:界面层每帧可能要读,不该产生分配。
        let a = base();
        let b = a;
        assert_eq!(a, b);
    }
}