use std::sync::Arc;
use ek_core::cfg_face::CfgHost;
use ek_core::context::PluginContext;
use ek_core::main_engine::MainEngine;
use ek_core::section::SectionMap;
use crate::state::UiShared;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum Shape {
#[default]
Single,
PerInstance,
}
pub enum Body {
Dm(ek_dm::ui_face::DmUiState),
Oms(ek_oms::ui_face::OmsUiState),
Tq(ek_tq::ui_face::TqUiState),
Gateway(crate::gateway::TableState),
Host(crate::hostwin::State),
}
pub struct ModuleWin {
pub id: String,
pub kind: String,
pub title: String,
pub icon: &'static str,
pub order: i32,
pub shape: Shape,
pub instances: Vec<String>,
pub always: bool,
pub maximized: bool,
pub open: bool,
pub size: [f32; 2],
pub min_size: [f32; 2],
pub body: Body,
}
impl std::fmt::Debug for ModuleWin {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("ModuleWin")
.field("id", &self.id)
.field("kind", &self.kind)
.field("instances", &self.instances)
.field("open", &self.open)
.finish()
}
}
impl ModuleWin {
#[must_use]
fn primary_instance(&self) -> &str {
self.instances.first().map(String::as_str).unwrap_or("")
}
}
pub struct Registry {
pub list: Vec<ModuleWin>,
}
impl Default for Registry {
fn default() -> Self {
Self { list: Vec::new() }
}
}
impl std::fmt::Debug for Registry {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_list()
.entries(self.list.iter().map(|m| (&m.id, &m.instances)))
.finish()
}
}
pub struct ModDesc {
pub kind: &'static str,
pub title: &'static str,
pub icon: &'static str,
pub order: i32,
pub size: [f32; 2],
pub min_size: [f32; 2],
pub always: bool,
pub maximized: bool,
}
pub const MODULES: &[ModDesc] = &[
ModDesc {
kind: "ctp",
title: "交易网关",
icon: "🔗",
order: 10,
size: [1100.0, 560.0],
min_size: [720.0, 360.0],
always: true,
maximized: false,
},
ModDesc {
kind: "dm",
title: "数据管理",
icon: "📊",
order: 20,
size: [560.0, 420.0],
min_size: [380.0, 260.0],
always: true,
maximized: false,
},
ModDesc {
kind: "oms",
title: "交易中枢",
icon: "⚙",
order: 30,
size: [560.0, 460.0],
min_size: [380.0, 280.0],
always: true,
maximized: false,
},
ModDesc {
kind: "tq",
title: "天勤桥",
icon: "🌉",
order: 40,
size: [520.0, 400.0],
min_size: [360.0, 260.0],
always: true,
maximized: false,
},
ModDesc {
kind: "ops",
title: "运维",
icon: "🛠",
order: 50,
size: [680.0, 420.0],
min_size: [420.0, 260.0],
always: true,
maximized: false,
},
ModDesc {
kind: "contracts",
title: "查询合约",
icon: "🔍",
order: 60,
size: [620.0, 460.0],
min_size: [420.0, 260.0],
always: true,
maximized: false,
},
ModDesc {
kind: "kline",
title: "K线",
icon: "📈",
order: 70,
size: [560.0, 360.0],
min_size: [360.0, 220.0],
always: true,
maximized: true,
},
];
#[must_use]
pub fn scan_groups(shared: &UiShared) -> std::collections::BTreeMap<String, Vec<String>> {
let mut groups: std::collections::BTreeMap<String, Vec<String>> =
std::collections::BTreeMap::new();
if let Some(engine) = shared.engine_arc() {
for name in engine.plugin_names() {
if let Some(p) = engine.plugin_arc(&name) {
groups.entry(p.kind().to_string()).or_default().push(name);
}
}
}
groups
}
fn desc_of(kind: &str) -> Option<&'static ModDesc> {
MODULES.iter().find(|d| d.kind == kind)
}
fn body_of(kind: &str) -> Option<Body> {
match kind {
"dm" => Some(Body::Dm(ek_dm::ui_face::DmUiState::new())),
"oms" => Some(Body::Oms(ek_oms::ui_face::OmsUiState::new())),
"tq" => Some(Body::Tq(ek_tq::ui_face::TqUiState::new())),
"ctp" => Some(Body::Gateway(crate::gateway::TableState::new())),
"ops" => Some(Body::Host(crate::hostwin::State::new(
crate::hostwin::HostKind::Ops,
))),
"contracts" => Some(Body::Host(crate::hostwin::State::new(
crate::hostwin::HostKind::Contracts,
))),
"kline" => Some(Body::Host(crate::hostwin::State::new(
crate::hostwin::HostKind::Kline {
symbol: String::new(),
},
))),
_ => None,
}
}
impl Registry {
#[must_use]
pub fn with_always() -> Self {
let mut reg = Self::default();
reg.ensure_always();
reg
}
fn ensure_always(&mut self) {
for d in MODULES.iter().filter(|d| d.always) {
if self.list.iter().any(|w| w.kind == d.kind) {
continue;
}
if let Some(body) = body_of(d.kind) {
self.list.push(ModuleWin {
id: d.kind.to_string(),
kind: d.kind.to_string(),
title: d.title.to_string(),
icon: d.icon,
order: d.order,
shape: Shape::Single,
instances: Vec::new(),
always: true,
maximized: d.maximized,
open: false,
size: d.size,
min_size: d.min_size,
body,
});
}
}
self.list.sort_by_key(|w| (w.order, w.id.clone()));
}
#[must_use]
pub fn is_open(&self, kind: &str) -> bool {
self.list
.iter()
.find(|w| w.kind == kind)
.is_some_and(|w| w.open)
}
pub fn set_open(&mut self, kind: &str, open: bool) {
if self.list.iter().all(|w| w.kind != kind) {
self.ensure_always();
}
if let Some(w) = self.list.iter_mut().find(|w| w.kind == kind) {
w.open = open;
}
}
#[must_use]
pub fn mount(shared: &UiShared) -> Self {
let mut reg = Self::with_always();
reg.sync(shared);
reg
}
pub fn sync(&mut self, shared: &UiShared) {
let groups = scan_groups(shared);
self.sync_groups(&groups);
}
pub fn sync_groups(&mut self, groups: &std::collections::BTreeMap<String, Vec<String>>) {
for (kind, insts) in groups {
if let Some(w) = self.list.iter_mut().find(|w| &w.kind == kind) {
w.instances = insts.clone();
continue;
}
let Some(d) = desc_of(kind) else { continue };
let Some(body) = body_of(kind) else { continue };
self.list.push(ModuleWin {
id: d.kind.to_string(),
kind: d.kind.to_string(),
title: d.title.to_string(),
icon: d.icon,
order: d.order,
shape: Shape::Single,
instances: insts.clone(),
always: d.always,
maximized: d.maximized,
open: false,
size: d.size,
min_size: d.min_size,
body,
});
}
self.ensure_always();
for w in &mut self.list {
if !groups.contains_key(&w.kind) {
w.instances.clear();
}
}
self.list
.retain(|w| w.always || groups.contains_key(&w.kind));
self.list.sort_by_key(|w| (w.order, w.id.clone()));
}
pub fn menu(&mut self, ui: &mut egui::Ui, shared: &UiShared) {
for w in &mut self.list {
if w.id.starts_with("kline:") {
continue;
}
if !ui
.button(w.title.as_str())
.on_hover_text(format!(
"{} {}:{} 个实例(点开看它自己的窗)",
w.icon,
w.title,
w.instances.len()
))
.clicked()
{
continue;
}
match w.id.as_str() {
"ops" => shared.set_ops_window_open(true),
"contracts" => shared.set_contracts_window_open(true),
_ => w.open = true,
}
}
}
pub fn windows(&mut self, ctx: &egui::Context, shared: &UiShared) {
self.sync_hosts(shared);
for w in &mut self.list {
if w.id == "kline" {
if w.open {
w.open = false;
let sym = shared.order_form().symbol.trim().to_string();
if sym.is_empty() {
shared.log_line("[kline] 先在行情表选一个合约再点「K线」".to_string());
} else {
shared.open_kline(sym);
}
}
continue;
}
if !w.open {
continue;
}
if show_module_window(ctx, w, shared) {
w.open = false;
match w.id.as_str() {
"ops" => shared.set_ops_window_open(false),
"contracts" => shared.set_contracts_window_open(false),
id if id.starts_with("kline:") => shared.close_kline(&id[6..]),
_ => {}
}
}
}
}
fn sync_hosts(&mut self, shared: &UiShared) {
for w in &mut self.list {
match w.id.as_str() {
"ops" => w.open = shared.ops_window_open(),
"contracts" => w.open = shared.contracts_window_open(),
_ => {}
}
}
let syms = shared.kline_symbols();
self.list.retain(|w| {
!w.id.starts_with("kline:") || syms.iter().any(|s| w.id == format!("kline:{s}"))
});
for s in syms {
let id = format!("kline:{s}");
if self.list.iter().any(|w| w.id == id) {
continue;
}
self.list.push(ModuleWin {
id,
kind: "kline".to_string(),
title: format!("K线 — {s}"),
icon: "📈",
order: 71,
shape: Shape::Single,
instances: Vec::new(),
always: true,
maximized: true,
open: true,
size: [900.0, 620.0],
min_size: [360.0, 220.0],
body: Body::Host(crate::hostwin::State::new(
crate::hostwin::HostKind::Kline { symbol: s.clone() },
)),
});
}
self.list.sort_by_key(|w| (w.order, w.id.clone()));
}
}
pub struct CfgBridge<'a> {
shared: &'a UiShared,
ctx: Option<PluginContext>,
store: Option<ek_cfg::Store>,
}
impl<'a> CfgBridge<'a> {
#[must_use]
pub fn new(shared: &'a UiShared) -> Self {
let ctx = shared.engine_arc().and_then(|engine| {
engine
.get_typed::<crate::plugin::UiPlugin>(crate::plugin::UI_NAME)
.and_then(|p| p.ctx_handle())
});
Self {
shared,
ctx,
store: None,
}
}
#[must_use]
pub fn with_store(shared: &'a UiShared, store: ek_cfg::Store) -> Self {
Self {
shared,
ctx: None,
store: Some(store),
}
}
fn store(&self) -> ek_cfg::Store {
match &self.store {
Some(s) => s.clone(),
None => crate::plugin::ui_cfg_store(),
}
}
}
impl CfgHost for CfgBridge<'_> {
fn cfg_save(&self, ns: &str, map: &SectionMap) -> Result<(), String> {
if let Some(ctx) = &self.ctx {
match ctx.ask_service::<ek_cfg::wire::SaveReq, ek_cfg::wire::SaveRes>(
"cfg.save",
ek_cfg::wire::SaveReq {
ns: ns.to_string(),
map: map.clone(),
},
std::time::Duration::from_millis(1500),
) {
Ok(r) if r.ok => return Ok(()),
Ok(r) => return Err(r.msg),
Err(ek_core::error::AskError::UnknownService { .. }) => {
self.shared
.log_line(format!("[cfg] cfg 缺席,{ns} 直写托底(软集中负形)"));
}
Err(e) => return Err(format!("{e:?}")),
}
}
self.store().save(ns, map).map_err(|e| e.to_string())
}
fn cfg_load(&self, ns: &str) -> Result<Option<SectionMap>, String> {
match self.store().load(ns) {
Ok(ek_cfg::LoadOutcome::Loaded { map, .. }) => Ok(Some(map)),
Ok(ek_cfg::LoadOutcome::Fresh) => Ok(None),
Err(e) => Err(e.to_string()),
}
}
fn log(&self, line: &str) {
self.shared.log_line(line.to_string());
}
}
fn show_module_window(ctx: &egui::Context, w: &mut ModuleWin, shared: &UiShared) -> bool {
let id = w.id.clone();
let title = match w.kind.as_str() {
"ops" | "contracts" => format!("{}窗", w.title),
_ => w.title.clone(),
};
let size = w.size;
let min = w.min_size;
let inst = w.primary_instance().to_string();
let mut open = w.open;
let mut closed = false;
let body = &mut w.body;
let mut builder = egui::ViewportBuilder::default()
.with_title(&title)
.with_inner_size(size)
.with_min_inner_size(min)
.with_minimize_button(true)
.with_maximize_button(true)
.with_close_button(true);
if w.maximized {
builder = builder.with_maximized(true);
}
ctx.show_viewport_immediate(
egui::ViewportId::from_hash_of(("module_win", &id)),
builder,
|ctx2, class| {
if class == egui::ViewportClass::Embedded {
egui::Window::new(&title)
.id(egui::Id::new(("module_win", &id)))
.open(&mut open)
.default_size(size)
.show(ctx2, |ui| {
render_body(body, shared, &inst, ui);
});
if !open {
closed = true;
}
return;
}
if ctx2.input(|i| i.viewport().close_requested()) {
closed = true;
return;
}
egui::CentralPanel::default().show(ctx2, |ui| {
render_body(body, shared, &inst, ui);
});
},
);
closed
}
fn render_body(body: &mut Body, shared: &UiShared, inst: &str, ui: &mut egui::Ui) {
match body {
Body::Dm(st) => {
let Some(engine) = shared.engine_arc() else {
ui.label("引擎未就绪");
return;
};
let Some(p) = engine.get_typed::<ek_dm::plugin::DmPlugin>(inst) else {
ui.label(format!("数据管理器未装配({inst})"));
return;
};
let host = CfgBridge::new(shared);
if ek_dm::ui_face::render(ui, &p, &host, st) {
ui.ctx().request_repaint();
}
}
Body::Oms(st) => {
let Some(engine) = shared.engine_arc() else {
ui.label("引擎未就绪");
return;
};
let Some(p) = engine.get_typed::<ek_oms::plugin::OmsPlugin>(inst) else {
ui.label(format!("交易中枢未装配({inst})"));
return;
};
let host = CfgBridge::new(shared);
if ek_oms::ui_face::render(ui, &p, &host, st) {
ui.ctx().request_repaint();
}
}
Body::Tq(st) => {
let Some(engine) = shared.engine_arc() else {
ui.label("引擎未就绪");
return;
};
let Some(p) = engine.get_typed::<ek_tq::plugin::TqPlugin>(inst) else {
ui.label(format!("天勤桥未装配({inst})"));
return;
};
let host = CfgBridge::new(shared);
if ek_tq::ui_face::render(ui, &p, &host, st) {
ui.ctx().request_repaint();
}
}
Body::Gateway(st) => crate::gateway::render(ui, st, shared),
Body::Host(st) => crate::hostwin::render(ui, st, shared),
}
}
#[must_use]
pub fn engine_of(shared: &UiShared) -> Option<Arc<MainEngine>> {
shared.engine_arc()
}