ek-ui 0.9.43

ek 生态常驻主界面(egui/eframe):插件即界面——观测页 + 日志页,车头与 UI 同插件
//! UiExt(0.2.30,BD-UI-47):语义排版助手。
//!
//! 为什么要有这一层:毛病 7「无视觉层级」的根因不是缺标题,是**同一档文字被用在
//! 不同语义上**——口径说明句(「代码回车 = 订阅」)与正文同字号同色,空态提示与
//! 表内数据同字号同色,于是屏幕上没有主次。Python 侧的层级来自 Qt 控件类型
//! (QTabBar / QHeaderView / QLabel / QLineEdit 各有 qss 规格),egui 里控件类型少,
//! 层级必须由这一层显式给。
//!
//! 纪律:这一层只**派生**自 [`crate::tokens`],不新造颜色、不新造字号。
//! 裁定边界(BD-UI-31「表题文字撤除,带位即身份」)仍然有效:主界面四张监控格
//! **不加**大标题,`section_title` 只用在有 windowTitle 的浮动窗里(Python 侧那些窗
//! 正是有标题的:`widget.py:1659 / 2407 / 2605`)。

use crate::tokens;
use egui::{Response, RichText, TextStyle, Ui};

/// 区块标题(Heading 档)。只用在浮动窗:运维窗 / 账户窗 / 合约查询窗。
/// 0.8.2:标题吃 **ink**(`strong_text_color()` = `widgets.active.fg_stroke` = `text1`),
/// 不吃 `noninteractive.fg_stroke` —— 那一支自 0.8.2 起是**表体口径**(`text2`,参考站
/// `td{color:var(--st-ink-soft)}`),标题跟着它走就等于三层塌成一层。
pub fn section_title(ui: &mut Ui, text: impl Into<String>) -> Response {
    ui.label(
        RichText::new(text.into())
            .font(ui.style().text_styles[&TextStyle::Heading].clone())
            .color(ui.visuals().strong_text_color()),
    )
}

/// 次级说明:口径句、空态提示、字段旁的一次性提示。Small 档 + 弱化色。
/// 与正文的差别必须**肉眼可分**(字号 + 色两档都降),否则等于没层级。
/// 0.8.0 双档:弱化色走**当前档**的 `text3` —— 旧写法直写深档那支常量,浅档上就是一块灰不溜秋。
pub fn muted_label(ui: &mut Ui, text: impl Into<String>) -> Response {
    ui.label(
        RichText::new(text.into())
            .font(ui.style().text_styles[&TextStyle::Small].clone())
            .color(tokens::palette(ui.ctx()).text3),
    )
}

/// 区块之间的呼吸位(比缺省行距大一档,`tokens::GROUP_GAP`)。
pub fn space_group(ui: &mut Ui) {
    ui.add_space(tokens::GROUP_GAP - ui.spacing().item_spacing.y);
}

/// 主操作钮(委托 / 全撤这类)。取色板里 pressed/checked 那一档(`widgets.active`,
/// Nova 下 = `#525252`)—— 色板内合法,不新造颜色;文字加粗。Python 侧下单钮是普通
/// QPushButton,这里是**有意**的层级加强(毛病 7),加强只用色板既有档。
pub fn primary_button(ui: &mut Ui, text: impl Into<String>) -> Response {
    ui.add(
        egui::Button::new(RichText::new(text.into()).strong())
            .fill(ui.visuals().widgets.active.bg_fill)
            .stroke(ui.visuals().widgets.active.bg_stroke),
    )
}

// 0.8.0 对齐统一(裁定 D「全部左对齐」):**`num_cell` 已退役**(旧口径「数字贴右」的
// 唯一实现,六张表 35 个右对齐格位都从它出;表头 100% 左 → 表头与表体对不齐)。
// 「数字成列」改由 `tokens::num_text` 的**等宽数字**守(代价 = 左对齐下小数点参差,
// 明码写在 `tokens::num_text` 注释里)。判据 `u4` 的助手清单随之摘掉这一项。

/// Grid 短表的列宽下限(0.4.1 七期 BD-UI-60,用户裁定「表宽默认就是你所在区域的 100%,
/// 字段什么都应该会压缩」)。
///
/// `min_col_width` 是**下限**不是固定值:内容更宽时列照样往上长(grid.rs:231
/// `max(内容宽, min_cell_size.x)`)→ 填不满时铺满、装不下时出横向滚动,与 Python
/// `ResizeToContents` + `stretchLastSection` 同一口径,不截断任何文字。
///
/// 目标宽**第一帧钉死**,之后只跟着容器缩、不跟着容器长:浮动窗是自动尺寸的,列宽若取
/// 「当帧可用宽」,窗就会跟着变宽的内容再长 → 下一帧可用宽更大 → 列再长,`grid.rs:276-278`
/// 每帧 `curr_state != prev_state` → `request_repaint` 死循环(0.4.1 七期实撞:15 条判据
/// 全撞在 `Harness::run exceeded max_steps`,Repaint causes 全是 grid.rs:278)。
pub fn grid_col_width(ui: &Ui, salt: &str, n_cols: usize) -> f32 {
    let id = egui::Id::new(("grid_fill", salt));
    let avail = ui.available_width();
    let stored = ui.ctx().data(|d| d.get_temp::<f32>(id)).unwrap_or(0.0);
    let target = if stored > 0.0 {
        stored.min(avail)
    } else {
        avail
    };
    ui.ctx().data_mut(|d| d.insert_temp(id, target.max(1.0)));
    let gaps = ui.spacing().item_spacing.x * (n_cols as f32 - 1.0);
    ((target - gaps) / n_cols as f32).max(24.0)
}