1use std::sync::Arc;
15
16use ek_core::cfg_face::CfgHost;
17use ek_core::context::PluginContext;
18use ek_core::main_engine::MainEngine;
19use ek_core::section::SectionMap;
20
21use crate::state::UiShared;
22
23#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
26pub enum Shape {
27 #[default]
29 Single,
30 PerInstance,
32}
33
34pub enum Body {
37 Dm(ek_dm::ui_face::DmUiState),
39 Oms(ek_oms::ui_face::OmsUiState),
41 Tq(ek_tq::ui_face::TqUiState),
43 Gateway(crate::gateway::TableState),
45 Host(crate::hostwin::State),
47 Strategy(Box<crate::strategy_win::StrategyWinState>),
50}
51
52pub struct ModuleWin {
54 pub id: String,
56 pub kind: String,
58 pub title: String,
60 pub icon: &'static str,
62 pub order: i32,
64 pub shape: Shape,
66 pub instances: Vec<String>,
68 pub always: bool,
70 pub maximized: bool,
72 pub open: bool,
74 pub size: [f32; 2],
76 pub min_size: [f32; 2],
78 pub body: Body,
80}
81
82impl std::fmt::Debug for ModuleWin {
83 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
84 f.debug_struct("ModuleWin")
85 .field("id", &self.id)
86 .field("kind", &self.kind)
87 .field("instances", &self.instances)
88 .field("open", &self.open)
89 .finish()
90 }
91}
92
93impl ModuleWin {
94 #[must_use]
96 fn primary_instance(&self) -> &str {
97 self.instances.first().map(String::as_str).unwrap_or("")
98 }
99}
100
101pub struct Registry {
103 pub list: Vec<ModuleWin>,
105}
106
107impl Default for Registry {
108 fn default() -> Self {
111 Self { list: Vec::new() }
112 }
113}
114
115impl std::fmt::Debug for Registry {
116 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
117 f.debug_list()
118 .entries(self.list.iter().map(|m| (&m.id, &m.instances)))
119 .finish()
120 }
121}
122
123pub struct ModDesc {
127 pub kind: &'static str,
128 pub title: &'static str,
129 pub icon: &'static str,
130 pub order: i32,
131 pub size: [f32; 2],
132 pub min_size: [f32; 2],
133 pub always: bool,
134 pub maximized: bool,
136}
137
138pub const MODULES: &[ModDesc] = &[
140 ModDesc {
141 kind: "ctp",
142 title: "交易网关",
143 icon: "🔗",
144 order: 10,
145 size: [1100.0, 560.0],
146 min_size: [720.0, 360.0],
147 always: true,
148 maximized: false,
149 },
150 ModDesc {
151 kind: "dm",
152 title: "数据管理",
153 icon: "📊",
154 order: 20,
155 size: [560.0, 420.0],
156 min_size: [380.0, 260.0],
157 always: true,
160 maximized: false,
161 },
162 ModDesc {
163 kind: "oms",
164 title: "交易中枢",
165 icon: "⚙",
166 order: 30,
167 size: [560.0, 460.0],
168 min_size: [380.0, 280.0],
169 always: true,
170 maximized: false,
171 },
172 ModDesc {
173 kind: "tq",
174 title: "天勤桥",
175 icon: "🌉",
176 order: 40,
177 size: [520.0, 400.0],
178 min_size: [360.0, 260.0],
179 always: true,
180 maximized: false,
181 },
182 ModDesc {
183 kind: "ops",
184 title: "运维",
185 icon: "🛠",
186 order: 50,
187 size: [680.0, 420.0],
188 min_size: [420.0, 260.0],
189 always: true,
190 maximized: false,
191 },
192 ModDesc {
193 kind: "contracts",
194 title: "查询合约",
195 icon: "🔍",
196 order: 60,
197 size: [620.0, 460.0],
198 min_size: [420.0, 260.0],
199 always: true,
200 maximized: false,
201 },
202 ModDesc {
203 kind: "kline",
204 title: "K线",
205 icon: "📈",
206 order: 70,
207 size: [560.0, 360.0],
208 min_size: [360.0, 220.0],
209 always: true,
210 maximized: true,
211 },
212];
213
214#[must_use]
220pub fn scan_groups(shared: &UiShared) -> std::collections::BTreeMap<String, GroupInfo> {
221 let mut groups: std::collections::BTreeMap<String, GroupInfo> =
222 std::collections::BTreeMap::new();
223 if let Some(engine) = shared.engine_arc() {
224 for name in engine.plugin_names() {
225 if let Some(p) = engine.plugin_arc(&name) {
226 let kind = p.kind().to_string();
227 let e = groups.entry(kind).or_default();
228 e.instances.push(name);
229 if e.desc.is_none() {
231 e.desc = p.window();
232 }
233 }
234 }
235 }
236 groups
237}
238
239#[derive(Debug, Clone, Default)]
241pub struct GroupInfo {
242 pub instances: Vec<String>,
244 pub desc: Option<ek_core::WindowDesc>,
246}
247
248fn desc_of(kind: &str, group: Option<&GroupInfo>) -> Option<ModDesc> {
253 if let Some(d) = MODULES.iter().find(|d| d.kind == kind) {
254 return Some(ModDesc {
255 kind: d.kind,
256 title: d.title,
257 icon: d.icon,
258 order: d.order,
259 size: d.size,
260 min_size: d.min_size,
261 always: d.always,
262 maximized: d.maximized,
263 });
264 }
265 let d = group?.desc?;
266 if d.id != kind {
268 return None;
269 }
270 let (size, min_size) = if d.sizes_ok() {
272 (d.size, d.min_size)
273 } else {
274 ([640.0, 420.0], [360.0, 240.0])
275 };
276 Some(ModDesc {
277 kind: d.id,
278 title: d.title,
279 icon: d.icon,
280 order: d.order,
281 size,
282 min_size,
283 always: d.always,
284 maximized: d.maximized,
285 })
286}
287
288fn body_of(kind: &str) -> Option<Body> {
291 match kind {
292 "dm" => Some(Body::Dm(ek_dm::ui_face::DmUiState::new())),
293 "oms" => Some(Body::Oms(ek_oms::ui_face::OmsUiState::new())),
294 "tq" => Some(Body::Tq(ek_tq::ui_face::TqUiState::new())),
295 "ctp" => Some(Body::Gateway(crate::gateway::TableState::new())),
296 "ops" => Some(Body::Host(crate::hostwin::State::new(
297 crate::hostwin::HostKind::Ops,
298 ))),
299 "contracts" => Some(Body::Host(crate::hostwin::State::new(
300 crate::hostwin::HostKind::Contracts,
301 ))),
302 "kline" => Some(Body::Host(crate::hostwin::State::new(
305 crate::hostwin::HostKind::Kline {
306 symbol: String::new(),
307 },
308 ))),
309 "strategy" => Some(Body::Strategy(Box::new(
312 crate::strategy_win::StrategyWinState::new(),
313 ))),
314 _ => None,
315 }
316}
317
318impl Registry {
319 #[must_use]
324 pub fn with_always() -> Self {
325 let mut reg = Self::default();
326 reg.ensure_always();
327 reg
328 }
329
330 fn ensure_always(&mut self) {
331 for d in MODULES.iter().filter(|d| d.always) {
332 if self.list.iter().any(|w| w.kind == d.kind) {
333 continue;
334 }
335 if let Some(body) = body_of(d.kind) {
336 self.list.push(ModuleWin {
337 id: d.kind.to_string(),
338 kind: d.kind.to_string(),
339 title: d.title.to_string(),
340 icon: d.icon,
341 order: d.order,
342 shape: Shape::Single,
343 instances: Vec::new(),
344 always: true,
345 maximized: d.maximized,
346 open: false,
347 size: d.size,
348 min_size: d.min_size,
349 body,
350 });
351 }
352 }
353 self.list.sort_by_key(|w| (w.order, w.id.clone()));
354 }
355
356 #[must_use]
358 pub fn is_open(&self, kind: &str) -> bool {
359 self.list
360 .iter()
361 .find(|w| w.kind == kind)
362 .is_some_and(|w| w.open)
363 }
364
365 pub fn set_open(&mut self, kind: &str, open: bool) {
367 if self.list.iter().all(|w| w.kind != kind) {
368 self.ensure_always();
369 }
370 if let Some(w) = self.list.iter_mut().find(|w| w.kind == kind) {
371 w.open = open;
372 }
373 }
374
375 #[must_use]
377 pub fn mount(shared: &UiShared) -> Self {
378 let mut reg = Self::with_always();
379 reg.sync(shared);
380 reg
381 }
382
383 pub fn sync(&mut self, shared: &UiShared) {
390 let groups = scan_groups(shared);
391 self.sync_groups(&groups);
392 }
393
394 pub fn sync_groups(&mut self, groups: &std::collections::BTreeMap<String, GroupInfo>) {
402 for (kind, info) in groups {
404 if let Some(w) = self.list.iter_mut().find(|w| &w.kind == kind) {
405 w.instances = info.instances.clone();
406 continue;
407 }
408 let Some(d) = desc_of(kind, Some(info)) else {
409 continue;
410 };
411 let Some(body) = body_of(kind) else { continue };
412 self.list.push(ModuleWin {
413 id: d.kind.to_string(),
414 kind: d.kind.to_string(),
415 title: d.title.to_string(),
416 icon: d.icon,
417 order: d.order,
418 shape: Shape::Single,
419 instances: info.instances.clone(),
420 always: d.always,
421 maximized: d.maximized,
422 open: false,
423 size: d.size,
424 min_size: d.min_size,
425 body,
426 });
427 }
428 self.ensure_always();
430 for w in &mut self.list {
432 if !groups.contains_key(&w.kind) {
433 w.instances.clear();
434 }
435 }
436 self.list
438 .retain(|w| w.always || groups.contains_key(&w.kind));
439 self.list.sort_by_key(|w| (w.order, w.id.clone()));
441 }
442
443 pub fn menu(&mut self, ui: &mut egui::Ui, shared: &UiShared) {
448 for w in &mut self.list {
449 if w.id.starts_with("kline:") {
452 continue;
453 }
454 if !ui
455 .button(w.title.as_str())
456 .on_hover_text(format!(
457 "{} {}:{} 个实例(点开看它自己的窗)",
458 w.icon,
459 w.title,
460 w.instances.len()
461 ))
462 .clicked()
463 {
464 continue;
465 }
466 match w.id.as_str() {
468 "ops" => shared.set_ops_window_open(true),
469 "contracts" => shared.set_contracts_window_open(true),
470 _ => w.open = true,
471 }
472 }
473 }
474
475 pub fn windows(&mut self, ctx: &egui::Context, shared: &UiShared) {
482 self.sync_hosts(shared);
483 shared.begin_window_frame(); for w in &mut self.list {
485 if w.id == "kline" {
487 if w.open {
488 w.open = false;
489 let sym = shared.order_form().symbol.trim().to_string();
490 if sym.is_empty() {
491 shared.log_line("[kline] 先在行情表选一个合约再点「K线」".to_string());
492 } else {
493 shared.open_kline(sym);
494 }
495 }
496 continue;
497 }
498 if !w.open {
499 continue;
500 }
501 let closed = show_module_window(ctx, w, shared);
502 shared.note_window_visible(&w.id); if closed {
504 w.open = false;
505 match w.id.as_str() {
506 "ops" => shared.set_ops_window_open(false),
507 "contracts" => shared.set_contracts_window_open(false),
508 id if id.starts_with("kline:") => shared.close_kline(&id[6..]),
509 _ => {}
510 }
511 }
512 }
513 }
514
515 fn sync_hosts(&mut self, shared: &UiShared) {
517 for w in &mut self.list {
518 match w.id.as_str() {
519 "ops" => w.open = shared.ops_window_open(),
520 "contracts" => w.open = shared.contracts_window_open(),
521 _ => {}
522 }
523 }
524 let syms = shared.kline_symbols();
525 self.list.retain(|w| {
526 !w.id.starts_with("kline:") || syms.iter().any(|s| w.id == format!("kline:{s}"))
527 });
528 for s in syms {
529 let id = format!("kline:{s}");
530 if self.list.iter().any(|w| w.id == id) {
531 continue;
532 }
533 self.list.push(ModuleWin {
534 id,
535 kind: "kline".to_string(),
536 title: format!("K线 — {s}"),
537 icon: "📈",
538 order: 71,
539 shape: Shape::Single,
540 instances: Vec::new(),
541 always: true,
542 maximized: true,
543 open: true,
546 size: [900.0, 620.0],
547 min_size: [360.0, 220.0],
548 body: Body::Host(crate::hostwin::State::new(
549 crate::hostwin::HostKind::Kline { symbol: s.clone() },
550 )),
551 });
552 }
553 self.list.sort_by_key(|w| (w.order, w.id.clone()));
554 }
555}
556
557pub struct CfgBridge<'a> {
562 shared: &'a UiShared,
563 ctx: Option<PluginContext>,
564 store: Option<ek_cfg::Store>,
566}
567
568impl<'a> CfgBridge<'a> {
569 #[must_use]
571 pub fn new(shared: &'a UiShared) -> Self {
572 let ctx = shared.engine_arc().and_then(|engine| {
573 engine
574 .get_typed::<crate::plugin::UiPlugin>(crate::plugin::UI_NAME)
575 .and_then(|p| p.ctx_handle())
576 });
577 Self {
578 shared,
579 ctx,
580 store: None,
581 }
582 }
583
584 #[must_use]
586 pub fn with_store(shared: &'a UiShared, store: ek_cfg::Store) -> Self {
587 Self {
588 shared,
589 ctx: None,
590 store: Some(store),
591 }
592 }
593
594 fn store(&self) -> ek_cfg::Store {
595 match &self.store {
596 Some(s) => s.clone(),
597 None => crate::plugin::ui_cfg_store(),
598 }
599 }
600}
601
602impl CfgHost for CfgBridge<'_> {
603 fn cfg_save(&self, ns: &str, map: &SectionMap) -> Result<(), String> {
604 if let Some(ctx) = &self.ctx {
605 match ctx.ask_service::<ek_cfg::wire::SaveReq, ek_cfg::wire::SaveRes>(
606 "cfg.save",
607 ek_cfg::wire::SaveReq {
608 ns: ns.to_string(),
609 map: map.clone(),
610 },
611 std::time::Duration::from_millis(1500),
612 ) {
613 Ok(r) if r.ok => return Ok(()),
614 Ok(r) => return Err(r.msg),
615 Err(ek_core::error::AskError::UnknownService { .. }) => {
616 self.shared
617 .log_line(format!("[cfg] cfg 缺席,{ns} 直写托底(软集中负形)"));
618 }
619 Err(e) => return Err(format!("{e:?}")),
620 }
621 }
622 self.store().save(ns, map).map_err(|e| e.to_string())
623 }
624
625 fn cfg_load(&self, ns: &str) -> Result<Option<SectionMap>, String> {
626 match self.store().load(ns) {
627 Ok(ek_cfg::LoadOutcome::Loaded { map, .. }) => Ok(Some(map)),
628 Ok(ek_cfg::LoadOutcome::Fresh) => Ok(None),
629 Err(e) => Err(e.to_string()),
630 }
631 }
632
633 fn log(&self, line: &str) {
634 self.shared.log_line(line.to_string());
635 }
636}
637
638fn show_module_window(ctx: &egui::Context, w: &mut ModuleWin, shared: &UiShared) -> bool {
648 let id = w.id.clone();
649 let title = match w.kind.as_str() {
652 "ops" | "contracts" => format!("{}窗", w.title),
653 _ => w.title.clone(),
654 };
655 let size = w.size;
656 let min = w.min_size;
657 let inst = w.primary_instance().to_string();
658 let mut open = w.open;
659 let mut closed = false;
660 let body = &mut w.body;
661
662 let mut builder = egui::ViewportBuilder::default()
663 .with_title(&title)
664 .with_inner_size(size)
665 .with_min_inner_size(min)
666 .with_minimize_button(true)
667 .with_maximize_button(true)
668 .with_close_button(true);
669 if w.maximized {
670 builder = builder.with_maximized(true);
671 }
672 ctx.show_viewport_immediate(
673 egui::ViewportId::from_hash_of(("module_win", &id)),
674 builder,
675 |ctx2, class| {
676 if class == egui::ViewportClass::Embedded {
677 egui::Window::new(&title)
678 .id(egui::Id::new(("module_win", &id)))
679 .open(&mut open)
680 .default_size(size)
681 .show(ctx2, |ui| {
682 render_body(body, shared, &inst, ui);
683 });
684 if !open {
685 closed = true;
686 }
687 return;
688 }
689 if ctx2.input(|i| i.viewport().close_requested()) {
690 closed = true;
691 return;
692 }
693 egui::CentralPanel::default().show(ctx2, |ui| {
694 render_body(body, shared, &inst, ui);
695 });
696 },
697 );
698 closed
699}
700
701fn render_body(body: &mut Body, shared: &UiShared, inst: &str, ui: &mut egui::Ui) {
703 match body {
704 Body::Dm(st) => {
705 let engine = shared.engine_arc();
710 let p = engine
711 .as_ref()
712 .and_then(|e| e.get_typed::<ek_dm::plugin::DmPlugin>(inst));
713 let host = CfgBridge::new(shared);
714 if ek_dm::ui_face::render(ui, p.as_deref(), &host, st) {
715 ui.ctx().request_repaint();
716 }
717 }
718 Body::Oms(st) => {
719 let engine = shared.engine_arc();
720 let p = engine
721 .as_ref()
722 .and_then(|e| e.get_typed::<ek_oms::plugin::OmsPlugin>(inst));
723 let host = CfgBridge::new(shared);
724 if ek_oms::ui_face::render(ui, p.as_deref(), &host, st) {
725 ui.ctx().request_repaint();
726 }
727 }
728 Body::Tq(st) => {
729 let engine = shared.engine_arc();
730 let p = engine
731 .as_ref()
732 .and_then(|e| e.get_typed::<ek_tq::plugin::TqPlugin>(inst));
733 let host = CfgBridge::new(shared);
734 if ek_tq::ui_face::render(ui, p.as_deref(), &host, st) {
735 ui.ctx().request_repaint();
736 }
737 }
738 Body::Gateway(st) => crate::gateway::render(ui, st, shared),
739 Body::Host(st) => crate::hostwin::render(ui, st, shared),
740 Body::Strategy(st) => {
741 if crate::strategy_win::render(ui, st.as_mut(), shared) {
742 ui.ctx().request_repaint();
743 }
744 }
745 }
746}
747
748#[must_use]
750pub fn engine_of(shared: &UiShared) -> Option<Arc<MainEngine>> {
751 shared.engine_arc()
752}