ek-ui 0.3.3

ek 生态常驻主界面(egui/eframe):插件即界面——观测页 + 日志页,车头与 UI 同插件
Documentation
//! 设计令牌层(0.2.30,BD-UI-46)。
//!
//! 为什么要有这一层:0.2.29 之前「颜色」散在两个地方 —— 主题函数里一份(五套
//! `Visuals`),渲染代码里另一份(`from_rgb` 直绘 50 多处:多红 / 空绿 / 警告橙 /
//! 错误深红 / 成功深绿 / 次级灰蓝 / 灰)。同一支语义色在不同表里各写各的,改一处
//! 不会传到另一处;而且「涨跌色」这种**必须恒定**的东西没有唯一源。
//!
//! 0.2.30 起规矩写死:**`src/` 里除本模块,`from_rgb(` 出现次数必须为 0**(机械钉
//! `u2_no_hardcoded_colors` 挡着)。所有色、字号、间距、控件尺寸、圆角从这里出。
//!
//! 主题只有一套(裁定 8):Qt深色 = QDarkStyle 3.2.3 复刻。依据 = 已安装的
//! QDarkStyle 3.2.3 `dark/palette.py`(色板)+ `dark/darkstyle.qss`(规格,2216 行
//! 实测:radius 4px 出现 48 次、描边 1px、padding 以 2px 为主)。源 =
//! `enkeep_trade/enkeep_trade/trader/ui/qt.py:102`
//! `qapp.setStyleSheet(qdarkstyle.load_stylesheet(qt_api="pyside6"))`。

use egui::{Color32, CornerRadius, FontId, Style, TextStyle, Vec2, Visuals};

/// qdarkstyle 3.2.3 色板(`colorsystem.py` 原值)。**全仓唯一允许写 `from_rgb` 的地方。**
pub mod qdark {
    use super::*;
    /// Gray.B10 —— 窗底 / 容器底 / 输入框底(qss 里所有容器同底,靠 1px 描边区分)。
    pub const BG1: Color32 = Color32::from_rgb(0x19, 0x23, 0x2D);
    /// Gray.B20 —— 「比背景差一点」那档(次级底、悬停前的弱填充)。
    pub const BG2: Color32 = Color32::from_rgb(0x29, 0x35, 0x44);
    /// Gray.B40 —— 按钮底 + 一切描边(表头带、分隔线、窗框、网格线)。
    pub const BG4: Color32 = Color32::from_rgb(0x45, 0x53, 0x64);
    /// Gray.B50 —— hover。
    pub const BG5: Color32 = Color32::from_rgb(0x54, 0x68, 0x7A);
    /// Gray.B60 —— pressed / checked。
    pub const BG6: Color32 = Color32::from_rgb(0x60, 0x79, 0x8B);
    /// Gray.B130 —— 正文 / 按钮文字。
    pub const TEXT1: Color32 = Color32::from_rgb(0xDF, 0xE1, 0xE2);
    /// Gray.B90 —— 次级文本。
    pub const TEXT3: Color32 = Color32::from_rgb(0xAC, 0xB1, 0xB6);
    /// Blue.B40 —— 选中行 / 选中文本 / 进度 chunk。
    pub const ACCENT2: Color32 = Color32::from_rgb(0x34, 0x67, 0x92);
    /// Blue.B70 —— 选中 tab 的 3px 下划线、链接色。
    pub const ACCENT4: Color32 = Color32::from_rgb(0x25, 0x9A, 0xE9);
}

/// 语义色:**跨界面恒定,不随主题漂移**(金融语义不玩配色)。0.2.29 之前这些散在
/// 渲染代码里,现在这里是唯一源。红涨绿跌 / 买红卖绿是同两支,不另造。
/// 0.3.2(Wave E,BD-UI-50)告警 toast 三级色 —— 值就是 Python `AlertToast._LEVEL_COLORS`
/// (alert_toast.py:45-49):ERROR `#FF4D4F` / WARNING `#FAAD14` / INFO `#1890FF`。
/// 色源只这一处(U2 单一色源钉:src/ 里 `Color32::from_rgb(` 只许出现在 tokens.rs)。
pub mod alert {
    use egui::Color32;

    /// Python INFO `#1890FF`。
    pub const INFO: Color32 = Color32::from_rgb(0x18, 0x90, 0xFF);
    /// Python WARNING `#FAAD14`。
    pub const WARNING: Color32 = Color32::from_rgb(0xFA, 0xAD, 0x14);
    /// Python ERROR `#FF4D4F`。
    pub const ERROR: Color32 = Color32::from_rgb(0xFF, 0x4D, 0x4F);
}

pub mod semantic {
    use super::*;
    /// 涨 / 多头 / 买 —— 红。
    pub const RISE: Color32 = Color32::from_rgb(0xE0, 0x50, 0x50);
    /// 跌 / 空头 / 卖 —— 绿。
    pub const FALL: Color32 = Color32::from_rgb(0x2E, 0x9E, 0x2E);
    /// 警告 / 待办 / 「先去做某事」提示 —— 橙。
    pub const WARN: Color32 = Color32::from_rgb(0xE0, 0x88, 0x10);
    /// 错误 / 失败 / 隔离 —— 深红(比 RISE 沉一档,免得把「跌」和「错」混成同一支)。
    pub const ERROR: Color32 = Color32::from_rgb(0xC0, 0x30, 0x30);
    /// 成功 / 已启动 / 已连接 —— 深绿(比 FALL 沉一档,同理)。
    pub const OK: Color32 = Color32::from_rgb(0x2E, 0x7D, 0x32);
    /// 状态类信息(总线状态、来源、中性提示)—— 淡蓝灰。
    pub const INFO: Color32 = Color32::from_rgb(0x9A, 0xA8, 0xC0);
    /// 次级 / 弱化 / 口径说明 —— 中性灰。
    pub const MUTED: Color32 = Color32::from_rgb(0x88, 0x88, 0x88);
}

/// 字号阶梯(BD-UI-32 R1 用户实看裁定「整体字偏小」,对照 Python 版;
/// egui 缺省 13 / 12 / 18 / 12)。三档层级是毛病 7「无视觉层级」的解法:
/// 标题 / 正文 / 次级必须**肉眼可分**,不是只换个字重。
pub const FONT_HEADING: f32 = 22.0;
pub const FONT_BODY: f32 = 16.0;
pub const FONT_SMALL: f32 = 14.0;
pub const FONT_MONO: f32 = 15.0;

/// 控件与间距规格(qss 口径:描边 1px、圆角 4px、padding 以 2px 为主)。
pub const CORNER: u8 = 4;
pub const STROKE_WIDTH: f32 = 1.0;
/// 行内水平间距 / 垂直行间距(egui 缺省 8 / 4;垂直 4 在 16px 字号下太挤)。
pub const SPACING_ITEM_X: f32 = 8.0;
pub const SPACING_ITEM_Y: f32 = 6.0;
/// 控件目标高度(16px 字号 + qss 的 2px padding 上下 + 描边)。
pub const INTERACT_HEIGHT: f32 = 26.0;
/// 分组内边距(`Frame::group` 用)。
pub const FRAME_INNER: f32 = 8.0;
/// 组与组之间、区块之间的呼吸位(`UiExt::space_group` 用)。
pub const GROUP_GAP: f32 = 12.0;

/// 布局比例(对照 Python `mainwindow.py`):
/// `v_splitter.setSizes([240, 560])` → 行情带 : 下带 = 30 : 70。
pub const MARKET_BAND_HEIGHT: f32 = 240.0;
/// Python `TradingWidget` 是 **`setFixedWidth(300)`**(`widget.py:1748`,定死不是建议宽度)。
/// 0.2.29 之前我们给的是 250 —— 毛病 1「比例失衡」的一条实账:下单侧比基准窄 50px,
/// 右侧 2×2 就跟着宽 50px。egui `SidePanel` 可拖,这里只定缺省值。
pub const ORDER_SIDE_WIDTH: f32 = 300.0;

/// 字号阶梯 + 间距 + 控件尺寸 + 主题视觉,一次装好。
/// 真实 App、独立子窗(immediate 共享同一 Context)、kittest 三处同源。
pub fn apply(style: &mut Style) {
    style
        .text_styles
        .insert(TextStyle::Body, FontId::proportional(FONT_BODY));
    style
        .text_styles
        .insert(TextStyle::Button, FontId::proportional(FONT_BODY));
    style
        .text_styles
        .insert(TextStyle::Small, FontId::proportional(FONT_SMALL));
    style
        .text_styles
        .insert(TextStyle::Heading, FontId::proportional(FONT_HEADING));
    style
        .text_styles
        .insert(TextStyle::Monospace, FontId::monospace(FONT_MONO));
    style.spacing.item_spacing = Vec2::new(SPACING_ITEM_X, SPACING_ITEM_Y);
    style.spacing.interact_size = Vec2::new(40.0, INTERACT_HEIGHT);
}

/// Qt深色视觉(唯一主题)。映射口径逐条对过 qss,不是凭印象:
/// - QWidget / QLabel / QLineEdit / 表格 底 = BG1 —— qdarkstyle 所有容器同底,
///   靠 1px BG4 描边区分,所以 `window_fill` 与 `panel_fill` 同值(不是偷懒,是照抄)。
/// - QPushButton #455364 → hover #54687A → pressed/checked #60798B,且 `border: none`
///   → `bg_stroke` 与 `bg_fill` 取同色(画出来就是无边)。
/// - expansion 一律 0.0:qdarkstyle hover 只换色不涨块,egui 缺省 hover/active 涨 1px。
/// - fg_stroke 宽度一律 1.0:Qt hover/pressed 不加重文字,egui 缺省 1.5 / 2.0。
/// - 选中行与选中文本 = ACCENT2(qss `QWidget::item:selected` 与 `QLineEdit:selected`
///   同值)。选中 tab 的「BG5 底 + 3px ACCENT4 下划线」egui `Visuals` 里没有对应字段
///   → 由渲染侧 `tab_label` 直绘补上,取色走 `Chrome`。
/// - `selection.stroke.color` 会被 egui 当**选中项文字色**用(style.rs:374)→ 取 TEXT1;
///   若照抄 qss 的 focus 边框 #1A72BB,选中 tab 的文字会变蓝。
pub fn visuals() -> Visuals {
    use qdark::*;
    let r4 = CornerRadius::same(CORNER);
    let mut v = Visuals::dark();
    v.dark_mode = true;
    v.window_fill = BG1;
    v.panel_fill = BG1;
    v.extreme_bg_color = BG1; // QLineEdit 底 = 窗底(qss 正是同值)
    v.faint_bg_color = BG2;
    v.hyperlink_color = ACCENT4;
    v.weak_text_color = Some(TEXT3);
    v.window_stroke = egui::Stroke::new(STROKE_WIDTH, BG4);
    v.window_corner_radius = r4;
    v.menu_corner_radius = r4;
    // 不设(有意):text_edit_bg_color(缺省跟随 extreme_bg_color,同值设两遍是重复)、
    // code_bg_color(ui.monospace 只换字体不带背景)、warn/error_fg_color(语义色在渲染侧
    // 直绘)、slider/progress 相关(本 UI 0 处 slider、0 处进度条)。

    let stroke1 = egui::Stroke::new(STROKE_WIDTH, TEXT1);
    // `WidgetVisuals` / `Selection` 不在 egui 的根 re-export 里(lib.rs:508 只出
    // Style / TextStyle / Visuals / Spacing)→ 走 `egui::style::` 全路径。
    v.widgets.noninteractive = egui::style::WidgetVisuals {
        bg_fill: BG1,
        weak_bg_fill: BG1,
        bg_stroke: egui::Stroke::new(STROKE_WIDTH, BG4), // 分隔线 / 窗框 = #455364
        corner_radius: r4,
        fg_stroke: stroke1,
        expansion: 0.0,
    };
    v.widgets.inactive = egui::style::WidgetVisuals {
        bg_fill: BG4,
        weak_bg_fill: BG4,
        bg_stroke: egui::Stroke::new(STROKE_WIDTH, BG4), // 与填充同色 = 视觉无边
        corner_radius: r4,
        fg_stroke: stroke1,
        expansion: 0.0,
    };
    v.widgets.hovered = egui::style::WidgetVisuals {
        bg_fill: BG5,
        weak_bg_fill: BG5,
        bg_stroke: egui::Stroke::new(STROKE_WIDTH, BG5),
        corner_radius: r4,
        fg_stroke: stroke1,
        expansion: 0.0,
    };
    v.widgets.active = egui::style::WidgetVisuals {
        bg_fill: BG6,
        weak_bg_fill: BG6,
        bg_stroke: egui::Stroke::new(STROKE_WIDTH, BG6),
        corner_radius: r4,
        fg_stroke: stroke1,
        expansion: 0.0,
    };
    v.widgets.open = egui::style::WidgetVisuals {
        bg_fill: BG6,
        weak_bg_fill: BG6,
        bg_stroke: egui::Stroke::new(STROKE_WIDTH, BG4),
        corner_radius: r4,
        fg_stroke: stroke1,
        expansion: 0.0,
    };
    v.selection = egui::style::Selection {
        bg_fill: ACCENT2,
        stroke: egui::Stroke::new(STROKE_WIDTH, TEXT1), // 「选中项文字色」,不是边框色
    };
    // Qt 的按钮 / 链接是手型光标;egui 缺省 None(浏览器口径)。qdarkstyle 走 Qt 原生。
    v.interact_cursor = Some(egui::CursorIcon::PointingHand);
    v
}

/// 涨跌色唯一入口(毛病 3 的语义半边:数字成列之外,颜色也必须按语义走,
/// 不许各处自己 `if v > 0 {红} else {绿}` 现编)。
/// `v > 0` → 涨(红);`v < 0` → 跌(绿);`v == 0` → 正文色(不染色)。
pub fn rise_fall(v: f64) -> Color32 {
    if v > 0.0 {
        semantic::RISE
    } else if v < 0.0 {
        semantic::FALL
    } else {
        qdark::TEXT1
    }
}