ek-ui 0.3.3

ek 生态常驻主界面(egui/eframe):插件即界面——观测页 + 日志页,车头与 UI 同插件
Documentation
//! 0.3.0 Wave C(BD-UI-48):下带四组监控格换 `egui_dock` 树。
//!
//! 框架形状按交接报告 §6 的定案:**菜单条 / 行情带 / 下单侧保持 egui 原生面板,不进 dock**;
//! 只有下带右侧数据区(委托|未成交 / 成交 / 持仓 / 资金|日志)换成 dock 树,
//! 在 `CentralPanel` 里用 `DockArea::show_inside`(`DockArea::show` 与 `CentralPanel::show` 互斥)。
//!
//! spike 验收(`.dsh/plans/ek-ui-WaveC-测试方案.md` §1,commit `52104cd`)留下的三条硬口径:
//! 1. dock 的 tab 节点在 AccessKit 里**查得到但精确标签查不到**(标签比标题长)——判据一律 `contains`;
//! 2. 默认树必须显式 `split`(`DockState::new` 只会把所有 tab 塞进一个叶子),
//!    `Right 0.5` + 两半各 `Below 0.5` 才是 Python `right_splitter` 的 50:50;
//! 3. 核心区不能被关:`show_close_buttons(false)` + `is_closeable=false` + `on_close=Ignore`。

use egui_dock::tab_viewer::OnCloseResponse;
use egui_dock::{
    AllowedSplits, DockArea, DockState, Node, NodeIndex, Split, Style, SurfaceIndex, TabViewer,
};

use crate::state::UiShared;

/// 下带四组(对应 Python `mainwindow.py:253-254` `right_splitter` 里的四个 widget)。
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum DataGroup {
    /// 左上:委托 / 未成交两 tab。
    Orders,
    /// 右上:成交。
    Trades,
    /// 左下:持仓。
    Positions,
    /// 右下:资金 / 日志两 tab。
    Funds,
}

impl DataGroup {
    /// 固定顺序 = 默认 2×2 的读序(左上 → 右上 → 左下 → 右下)。
    pub const ALL: [DataGroup; 4] = [
        DataGroup::Orders,
        DataGroup::Trades,
        DataGroup::Positions,
        DataGroup::Funds,
    ];

    /// tab 标题 = Python 四块的表题语义(BD-UI-31「表题文字撤除,带位即身份」仍然成立:
    /// 标题只出现在 tab 上,格内不再重复大字表题)。
    pub fn title(&self) -> &'static str {
        match self {
            DataGroup::Orders => "委托",
            DataGroup::Trades => "成交",
            DataGroup::Positions => "持仓",
            DataGroup::Funds => "资金",
        }
    }
}

/// 默认布局:先左右 50:50 一刀,再左右两半各自上下 50:50 一刀 = 2×2。
///
/// 必须显式 `split`:`DockState::new(vec![..])` 只会把所有 tab 塞进**同一个叶子**
/// (spike 实测:不 split 时四组挤成一列,S4 直接红)。
pub fn default_tree() -> DockState<DataGroup> {
    let mut tree = DockState::new(vec![DataGroup::Orders]);
    let main = SurfaceIndex::main();
    let [left, right] = tree.split(
        (main, NodeIndex::from(0usize)),
        Split::Right,
        0.5,
        Node::leaf(DataGroup::Trades),
    );
    tree.split(
        (main, left),
        Split::Below,
        0.5,
        Node::leaf(DataGroup::Positions),
    );
    tree.split(
        (main, right),
        Split::Below,
        0.5,
        Node::leaf(DataGroup::Funds),
    );
    tree
}

/// `TabViewer`:格内容直接复用 0.2.29 的四个 group 渲染函数,**不改一行业务渲染代码**。
pub struct EkTabViewer<'a> {
    shared: &'a UiShared,
}

impl<'a> EkTabViewer<'a> {
    pub fn new(shared: &'a UiShared) -> Self {
        Self { shared }
    }
}

impl TabViewer for EkTabViewer<'_> {
    type Tab = DataGroup;

    fn title(&mut self, tab: &mut Self::Tab) -> egui::WidgetText {
        tab.title().to_string().into()
    }

    fn ui(&mut self, ui: &mut egui::Ui, tab: &mut Self::Tab) {
        match tab {
            DataGroup::Orders => crate::render::render_topleft_group(ui, self.shared),
            DataGroup::Trades => crate::render::render_trades_group(ui, self.shared),
            DataGroup::Positions => crate::render::render_positions_group(ui, self.shared),
            DataGroup::Funds => crate::render::render_bottomright_group(ui, self.shared),
        }
    }

    fn id(&mut self, tab: &mut Self::Tab) -> egui::Id {
        egui::Id::new(*tab)
    }

    // 核心区不能被关(S5):三处一起关死,不留「关掉就没了」的入口。
    fn is_closeable(&self, _tab: &Self::Tab) -> bool {
        false
    }

    fn closeable(&mut self, _tab: &mut Self::Tab) -> bool {
        false
    }

    fn on_close(&mut self, _tab: &mut Self::Tab) -> OnCloseResponse {
        OnCloseResponse::Ignore
    }

    // 四组都是数据区常驻件,允许被 detach 成独立窗(用户裁定:tab 可见 + 允许 detach)。
    fn allowed_in_windows(&self, _tab: &mut Self::Tab) -> bool {
        true
    }
}

/// 画下带数据区(`CentralPanel` 内)。样式**只从 egui `Style` 派生**(egui `Style` 已由
/// `tokens::apply` 填好)——这里不出现任何字面量颜色(V5 机械钉扫的就是这一点)。
pub fn show_monitor_dock(ui: &mut egui::Ui, shared: &UiShared) {
    let mut guard = shared.dock_tree();
    if guard.is_none() {
        *guard = Some(default_tree());
    }
    let tree = guard.as_mut().expect("dock 树已初始化");
    let style = Style::from_egui(ui.style().as_ref());
    DockArea::new(tree)
        .id(egui::Id::new("ek_monitor_dock"))
        .style(style)
        .show_close_buttons(false)
        .show_add_buttons(false)
        // 实跑截图(0.3.0 尾巴)实锤:`DockArea::new` 的这两个开关默认 true,每个叶子里会画
        // 「×」(全关)与「▼」(折叠)两颗钮 —— 数据区四组不能被关,这两颗钮就是漏口。
        .show_leaf_close_all_buttons(false)
        .show_leaf_collapse_buttons(false)
        .draggable_tabs(true)
        .allowed_splits(AllowedSplits::All)
        .show_inside(ui, &mut EkTabViewer::new(shared));
}