ek-ui 0.9.5

ek 生态常驻主界面(egui/eframe):插件即界面——观测页 + 日志页,车头与 UI 同插件
Documentation
//! faces —— 插件界面挂载表(「插件即 App」波 P4)。
//!
//! 挂载 = **装配名单驱动**(判据 U-W1:装配了谁挂谁的面,没装配的不出现):
//! 插件自带界面住在插件 crate(feature `ui` 门控,daemon 根本不编译),本模块
//! 只做**派发 + 宿主桥**:
//! - [`CfgBridge`] = [`CfgHost`] 的 ek-ui 实现(写档 = ek-cfg 托底根;显形 =
//!   UiShared 日志行)—— 插件面经它交回宿主,插件本体永不碰文件与凭据库;
//! - 派发按注册名走 `MainEngine::get_typed`(类型安全取壳;tq/ctp 的注册名是
//!   派生网关名,所以逐类型试取,命中即定面);dm/oms/tq 吃各自 `ui_face::render`,
//!   ctp(自带键位、无 schema)走 [`crate::pluginwin::render_window`] 视图驱动形态。

use std::sync::Arc;

use ek_cfg::LoadOutcome;
use ek_cfg::Store as CfgStore;
use ek_core::cfg_face::CfgHost;
use ek_core::main_engine::MainEngine;
use ek_core::plugin::Plugin;
use ek_core::section::SectionMap;

use crate::pluginwin::WinState;
use crate::state::UiShared;

/// 插件面出口的稳定注册名(0.3.19 惯例:消费方零字面量,提供方导出口)。
/// ek-tq / ek-ctp 的注册名是派生网关名,故此处只作缺省参考,派发按类型试取。
const NS_DM: &str = "dm";
const NS_OMS: &str = "oms";

/// 命中面 kinds(挂载表与派发共用同一张判定)。
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum FaceKind {
    /// dm 自带面。
    Dm,
    /// oms 自带面。
    Oms,
    /// tq 自带面。
    Tq,
    /// ctp 走通用窗口(视图驱动)。
    Ctp,
}

/// 宿主桥:插件面的 `cfg_save`/`cfg_load`/`log` 落点(写档 + 日志显形)。
pub struct CfgBridge {
    /// ek-cfg 托底根(`Store::eco()` 成功时才有;失败 = 界面只显形不落盘)。
    pub cfg: Option<CfgStore>,
    /// 宿主日志面(窗口右下日志区)。
    pub shared: Arc<UiShared>,
}

impl CfgBridge {
    /// 用当前托底根 + 宿主日志面建桥(`eco()` 失败 = 只显形不落盘)。
    #[must_use]
    pub fn new(shared: &Arc<UiShared>) -> Self {
        Self {
            cfg: CfgStore::eco().ok(),
            shared: Arc::clone(shared),
        }
    }
}

impl CfgHost for CfgBridge {
    fn cfg_save(&self, ns: &str, map: &SectionMap) -> Result<(), String> {
        match &self.cfg {
            Some(c) => c.store(ns, map).map_err(|e| e.to_string()),
            None => Err("档案托底根不可用(Store::eco 失败)".to_string()),
        }
    }

    fn cfg_load(&self, ns: &str) -> Result<Option<SectionMap>, String> {
        match &self.cfg {
            Some(c) => match c.load(ns) {
                Ok(LoadOutcome::Loaded { map, .. }) => Ok(Some(map)),
                Ok(LoadOutcome::Fresh) => Ok(None),
                Err(e) => Err(e.to_string()),
            },
            None => Err("档案托底根不可用(Store::eco 失败)".to_string()),
        }
    }

    fn log(&self, line: &str) {
        self.shared.log_line(line);
    }
}

/// 一帧宿主的状态包(各插件面自带态 + 通用窗口态表)。
#[derive(Default)]
pub struct FaceStates {
    /// dm 面态。
    pub dm: ek_dm::ui_face::DmUiState,
    /// oms 面态。
    pub oms: ek_oms::ui_face::OmsUiState,
    /// tq 面态。
    pub tq: ek_tq::ui_face::TqUiState,
    /// 通用窗口态表(注册名 → 窗口态)。
    pub wins: std::collections::HashMap<String, WinState>,
}

impl FaceStates {
    /// 空态表。
    #[must_use]
    pub fn new() -> Self {
        Self::default()
    }
}

/// 取壳并 downcast 成真类型(0.9.5 裁 B 路)。
///
/// 装配侧把插件包进壳(ek-station `DynPlugin`),注册表的 downcast 底座是壳类型,
/// `get_typed::<真插件>` 在**产品装配路线**上必然落空(上一波判据用手搭引擎测才没暴露)。
/// 本口走 `MainEngine::plugin_arc`(core 0.5.3)取 dyn 本体 + 壳转发的 `as_any`(station 0.4.5)
/// 做 downcast —— 手搭引擎(`get_typed` 直连)与装配壳两条路都通。
fn with_plugin<T: Plugin + 'static, R>(
    engine: &MainEngine,
    name: &str,
    f: impl FnOnce(&T) -> R,
) -> Option<R> {
    let arc = engine.plugin_arc(name)?;
    let any = arc.as_any()?;
    any.downcast_ref::<T>().map(f)
}

/// 有面判据:能 downcast 成真类型且有受理面。
fn face_probe<T: Plugin + 'static>(engine: &MainEngine, name: &str) -> bool {
    with_plugin(engine, name, |p: &T| p.as_configurable().is_some()).unwrap_or(false)
}

/// dm 采样档位读数(产品取壳路,判据用;装配壳路线也通)。
#[must_use]
pub fn dm_intervals(engine: &MainEngine) -> Option<Vec<i64>> {
    with_plugin(engine, ek_ui_ns_dm(), |p: &ek_dm::plugin::DmPlugin| {
        p.intervals_snapshot()
    })
}

/// 本模块认的 dm 注册名(与挂载表同源)。
fn ek_ui_ns_dm() -> &'static str {
    NS_DM
}

/// 按注册名定面(逐类型试取:dm/oms 是定名,tq/ctp 是派生网关名)。
#[must_use]
pub fn face_of(engine: &MainEngine, name: &str) -> Option<FaceKind> {
    if face_probe::<ek_dm::plugin::DmPlugin>(engine, name) {
        return Some(FaceKind::Dm);
    }
    if face_probe::<ek_oms::plugin::OmsPlugin>(engine, name) {
        return Some(FaceKind::Oms);
    }
    if face_probe::<ek_tq::plugin::TqPlugin>(engine, name) {
        return Some(FaceKind::Tq);
    }
    if face_probe::<ek_ctp::plugin::CtpPlugin>(engine, name) {
        return Some(FaceKind::Ctp);
    }
    None
}

/// 挂载表:在册且有面的插件(注册名, 窗口标题)。未装配的不出现(判据 U-W1)。
#[must_use]
pub fn mounted(engine: &MainEngine) -> Vec<(String, String)> {
    let mut out: Vec<(String, String)> = Vec::new();
    for name in engine.plugin_names() {
        let title = match face_of(engine, &name) {
            Some(FaceKind::Dm) => "数据管理器 dm",
            Some(FaceKind::Oms) => "统一受理 oms",
            Some(FaceKind::Tq) => "天勤桥 tq",
            Some(FaceKind::Ctp) => "行情网关 ctp",
            None => continue,
        };
        out.push((name, title.to_string()));
    }
    out
}

/// 按段派发受理(档案包导入用):段名 → 在册插件壳 → `apply_config` 回执。
/// 取不到壳 = `None`(只收编不受理,导入不炸)。
pub fn apply_section(engine: &MainEngine, ns: &str, section: &SectionMap) -> Option<String> {
    // 定名段直取;派生网关名段(ctp/tq)按 ns 扫在册壳的 config_ns 对上号。
    let rep = match ns {
        NS_DM => with_plugin(engine, ns, |p: &ek_dm::plugin::DmPlugin| {
            p.as_configurable()?.apply_config(section).ok()
        })??,
        NS_OMS => with_plugin(engine, ns, |p: &ek_oms::plugin::OmsPlugin| {
            p.as_configurable()?.apply_config(section).ok()
        })??,
        _ => {
            // 扫在册插件:谁的 config_ns == ns 就派谁(tq/ctp 的派生名段)
            let mut found = None;
            for name in engine.plugin_names() {
                if let Some(kind) = face_of(engine, &name) {
                    let hit = match kind {
                        FaceKind::Tq => {
                            with_plugin(engine, &name, |p: &ek_tq::plugin::TqPlugin| {
                                let c = p.as_configurable()?;
                                (c.config_ns() == ns).then(|| c.apply_config(section).ok())
                            })
                        }
                        FaceKind::Ctp => {
                            with_plugin(engine, &name, |p: &ek_ctp::plugin::CtpPlugin| {
                                let c = p.as_configurable()?;
                                (c.config_ns() == ns).then(|| c.apply_config(section).ok())
                            })
                        }
                        _ => None,
                    };
                    // 三层压平:取壳(Option) × 命中判断(Option) × 受理回执(Option)
                    if let Some(r) = hit.flatten().flatten() {
                        found = Some(r);
                        break;
                    }
                }
            }
            found?
        }
    };
    Some(format!(
        "受理 采纳{}/待重启{}",
        rep.adopted.len(),
        rep.deferred.len()
    ))
}

/// 派发一个插件的面。返回 true = 本帧有交互(宿主按需重绘)。
pub fn render_face(
    ui: &mut egui::Ui,
    engine: &MainEngine,
    name: &str,
    host: &dyn CfgHost,
    fs: &mut FaceStates,
) -> bool {
    match face_of(engine, name) {
        Some(FaceKind::Dm) => {
            let Some(hit) = with_plugin(engine, name, |p: &ek_dm::plugin::DmPlugin| {
                ek_dm::ui_face::render(ui, p, host, &mut fs.dm)
            }) else {
                ui.label("dm 壳取不到(未装配?)");
                return false;
            };
            hit
        }
        Some(FaceKind::Oms) => {
            let Some(hit) = with_plugin(engine, name, |p: &ek_oms::plugin::OmsPlugin| {
                ek_oms::ui_face::render(ui, p, host, &mut fs.oms)
            }) else {
                ui.label("oms 壳取不到(未装配?)");
                return false;
            };
            hit
        }
        Some(FaceKind::Tq) => {
            let Some(hit) = with_plugin(engine, name, |p: &ek_tq::plugin::TqPlugin| {
                ek_tq::ui_face::render(ui, p, host, &mut fs.tq)
            }) else {
                ui.label("tq 壳取不到(未装配?)");
                return false;
            };
            hit
        }
        Some(FaceKind::Ctp) => {
            // ctp 走通用视图驱动形貌(键位表 + 只读秘面引用),直取 dyn 本体即可。
            let Some(arc) = engine.plugin_arc(name) else {
                ui.label("ctp 壳取不到(未装配?)");
                return false;
            };
            let st = fs.wins.entry(name.to_string()).or_default();
            crate::pluginwin::render_window(ui, arc.as_ref(), host, st)
        }
        None => {
            ui.label(format!("{name}:无自带面(不在挂载表)"));
            false
        }
    }
}