ek-ui 0.9.9

ek 生态常驻主界面(egui/eframe):插件即界面——观测页 + 日志页,车头与 UI 同插件
Documentation
//! 薄 QSplitter(0.4.1,BD-UI-54):拆掉 egui_dock 后,数据区 2×2 的两刀分栏。
//!
//! Python `right_splitter` 就是 QSplitter(`mainwindow.py:253`,vertical 50:50),
//! 所以这里补的正是 Python 本来就有的自由度 —— **可拖的分隔条** —— 而不是 dock
//! 那套「任意拖动重排」:Python 的 QDockWidget 全是 NoDockWidgetFeatures
//! (`mainwindow.py:121/137/179/199/237/267`),连拖都不给,那一层是 dock 白给的。
//!
//! 规格对照 QSplitter:
//! - 竖刀 = 左右分,横刀 = 上下分;份额是 0..1 浮点(左侧/上侧占比),拖完记在
//!   `UiShared`(会话内;「恢复布局」回 0.5)。
//! - 分隔条命中带 6px(qss splitter handle 形制),拖拽/悬停画 ACCENT4 高亮线;
//!   平时画**当前档色板的描边那支** 1px 线(与 chrome 分隔线同源;0.8.0 双档前是深档常量),
//!   **不改光标**
//!   (0.4.1 七期 BD-UI-60,用户裁定「鼠标就是正常形状,不要随便变」:旧口径每帧
//!   无条件 `set_cursor_icon(ResizeVertical/ResizeHorizontal)` —— 不看 hover 也不看
//!   drag,整窗光标都被它拽成resize 箭头)。
//! - 分隔条是**纯装饰命中带**:不进 AccessKit 名册(s39「面板与把手是装饰」同口径);
//!   份额断言走 ui_state 状态钉,几何断言走渲染出的表头锚点(kittest)。

use egui::{Id, Pos2, Rect, Sense, Ui, UiBuilder};

/// 缺省两刀各半(Python `right_splitter` 50:50)。
pub const DATA_SPLIT_DEFAULT: f32 = 0.5;
/// 份额夹取范围:再小/再大的格子装不下表头带。
const FRAC_MIN: f32 = 0.15;
const FRAC_MAX: f32 = 0.85;
/// 分隔条命中带宽度(qss `::handle` 形制;内容区各留一半)。
const HANDLE: f32 = 6.0;

/// 份额夹取(拖出界就地停住 = QSplitter 的 minimumSize 语义)。
pub fn clamp_frac(f: f32) -> f32 {
    f.clamp(FRAC_MIN, FRAC_MAX)
}

/// 2×2 四格 + **两条贯穿的分隔条**(0.4.1 九期 BD-UI-62,用户实看:「竖线拉的时候只高跳了
/// 上面委托和成交之间,持仓和资金之间那条竖着的白色跳线没有跳出来 —— 只跳了一半」)。
///
/// 旧口径 = 先 `split_v` 上下分,再在每一排里各来一刀 `split_h` → 所谓「竖刀」其实是
/// **两条独立线段**(探针实测:上排那条 y 278..562、下排那条 y 568..852,同一个 x=798),
/// 悬 / 拖哪条只有哪条亮、哪条动。横线没这个问题(它本来就只有一根,横跨整块)。
///
/// 现在份额仍是同一对(`row` / `col`,与 `DataSplit` 一致),但四格直接由同一套矩形切出来,
/// 分隔条 = 一根贯穿的横线 + 一根贯穿的竖线,命中带也各是一条 → 拖竖线 = 整条一起亮、一起动。
// 10 个参数是 2×2 的形状本身:两把刀(份额 + 回调)× 2 + 四格内容。拆成结构体反而更难读。
#[allow(clippy::too_many_arguments)]
pub fn quad(
    ui: &mut Ui,
    id: Id,
    row_frac: f32,
    col_frac: f32,
    on_row: impl FnOnce(f32),
    on_col: impl FnOnce(f32),
    top_left: impl FnOnce(&mut Ui),
    top_right: impl FnOnce(&mut Ui),
    bottom_left: impl FnOnce(&mut Ui),
    bottom_right: impl FnOnce(&mut Ui),
) {
    let total = ui.available_rect_before_wrap();
    if total.width() <= 0.0 || total.height() <= 0.0 {
        return; // 折叠/零面积:没得画,也没得拖
    }
    let row = clamp_frac(row_frac);
    let col = clamp_frac(col_frac);
    // 先把整块面积吃进父布局(否则父层随后会出现假滚动条/布局逃逸)。
    ui.allocate_rect(total, Sense::hover());

    let half = HANDLE * 0.5;
    let y = total.top() + total.height() * row;
    let x = total.left() + total.width() * col;
    let quads = [
        Rect::from_min_max(total.left_top(), Pos2::new(x - half, y - half)),
        Rect::from_min_max(
            Pos2::new(x + half, total.top()),
            Pos2::new(total.right(), y - half),
        ),
        Rect::from_min_max(
            Pos2::new(total.left(), y + half),
            Pos2::new(x - half, total.bottom()),
        ),
        Rect::from_min_max(Pos2::new(x + half, y + half), total.right_bottom()),
    ];
    ui.scope_builder(UiBuilder::new().max_rect(quads[0]), top_left);
    ui.scope_builder(UiBuilder::new().max_rect(quads[1]), top_right);
    ui.scope_builder(UiBuilder::new().max_rect(quads[2]), bottom_left);
    ui.scope_builder(UiBuilder::new().max_rect(quads[3]), bottom_right);

    // 分隔条画在四块内容之上(epaint 无 z-order,后画在上)。
    // 0.8.0 双档(BD-UI-61):取色走**当前档**的色板 —— 旧写法直写深档那三支常量
    // (BG1 / BORDER / ACCENT4),浅档下这两条分隔条就是界面里唯一的深档块
    // (判据 s89 钉「浅档帧不许出现深档底」,V5 钉这里不许再直写常量)。
    let pal = crate::tokens::palette(ui.ctx());
    let p = ui.painter();
    let h_band = Rect::from_min_max(
        Pos2::new(total.left(), y - half),
        Pos2::new(total.right(), y + half),
    );
    let v_band = Rect::from_min_max(
        Pos2::new(x - half, total.top()),
        Pos2::new(x + half, total.bottom()),
    );
    p.rect_filled(h_band, 0.0, pal.bg1);
    p.rect_filled(v_band, 0.0, pal.bg1);

    let h_resp = ui.interact(h_band, id.with("handle_h"), Sense::drag());
    let v_resp = ui.interact(v_band, id.with("handle_v"), Sense::drag());
    let mut h_color = pal.border;
    let mut v_color = pal.border;
    if h_resp.dragged() {
        h_color = pal.accent4;
        if total.height() > 1.0 {
            let moved = clamp_frac(row + h_resp.drag_delta().y / total.height());
            if moved != row {
                on_row(moved);
            }
        }
    } else if h_resp.hovered() {
        h_color = pal.accent4;
    }
    if v_resp.dragged() {
        v_color = pal.accent4;
        if total.width() > 1.0 {
            let moved = clamp_frac(col + v_resp.drag_delta().x / total.width());
            if moved != col {
                on_col(moved);
            }
        }
    } else if v_resp.hovered() {
        v_color = pal.accent4;
    }
    p.line_segment(
        [Pos2::new(total.left(), y), Pos2::new(total.right(), y)],
        egui::Stroke::new(1.0_f32, h_color),
    );
    p.line_segment(
        [Pos2::new(x, total.top()), Pos2::new(x, total.bottom())],
        egui::Stroke::new(1.0_f32, v_color),
    );
}