Skip to main content

ek_ui/
splitters.rs

1//! 薄 QSplitter(0.4.1,BD-UI-54):拆掉 egui_dock 后,数据区 2×2 的两刀分栏。
2//!
3//! Python `right_splitter` 就是 QSplitter(`mainwindow.py:253`,vertical 50:50),
4//! 所以这里补的正是 Python 本来就有的自由度 —— **可拖的分隔条** —— 而不是 dock
5//! 那套「任意拖动重排」:Python 的 QDockWidget 全是 NoDockWidgetFeatures
6//! (`mainwindow.py:121/137/179/199/237/267`),连拖都不给,那一层是 dock 白给的。
7//!
8//! 规格对照 QSplitter:
9//! - 竖刀 = 左右分,横刀 = 上下分;份额是 0..1 浮点(左侧/上侧占比),拖完记在
10//!   `UiShared`(会话内;「恢复布局」回 0.5)。
11//! - 分隔条命中带 6px(qss splitter handle 形制),拖拽/悬停画 ACCENT4 高亮线;
12//!   平时画 `shadcn::BORDER` 1px 线(与 chrome 分隔线同源令牌)。**不改光标**
13//!   (0.4.1 七期 BD-UI-60,用户裁定「鼠标就是正常形状,不要随便变」:旧口径每帧
14//!   无条件 `set_cursor_icon(ResizeVertical/ResizeHorizontal)` —— 不看 hover 也不看
15//!   drag,整窗光标都被它拽成resize 箭头)。
16//! - 分隔条是**纯装饰命中带**:不进 AccessKit 名册(s39「面板与把手是装饰」同口径);
17//!   份额断言走 ui_state 状态钉,几何断言走渲染出的表头锚点(kittest)。
18
19use egui::{Id, Pos2, Rect, Sense, Ui, UiBuilder};
20
21use crate::tokens::shadcn;
22
23/// 缺省两刀各半(Python `right_splitter` 50:50)。
24pub const DATA_SPLIT_DEFAULT: f32 = 0.5;
25/// 份额夹取范围:再小/再大的格子装不下表头带。
26const FRAC_MIN: f32 = 0.15;
27const FRAC_MAX: f32 = 0.85;
28/// 分隔条命中带宽度(qss `::handle` 形制;内容区各留一半)。
29const HANDLE: f32 = 6.0;
30
31/// 份额夹取(拖出界就地停住 = QSplitter 的 minimumSize 语义)。
32pub fn clamp_frac(f: f32) -> f32 {
33    f.clamp(FRAC_MIN, FRAC_MAX)
34}
35
36/// 2×2 四格 + **两条贯穿的分隔条**(0.4.1 九期 BD-UI-62,用户实看:「竖线拉的时候只高跳了
37/// 上面委托和成交之间,持仓和资金之间那条竖着的白色跳线没有跳出来 —— 只跳了一半」)。
38///
39/// 旧口径 = 先 `split_v` 上下分,再在每一排里各来一刀 `split_h` → 所谓「竖刀」其实是
40/// **两条独立线段**(探针实测:上排那条 y 278..562、下排那条 y 568..852,同一个 x=798),
41/// 悬 / 拖哪条只有哪条亮、哪条动。横线没这个问题(它本来就只有一根,横跨整块)。
42///
43/// 现在份额仍是同一对(`row` / `col`,与 `DataSplit` 一致),但四格直接由同一套矩形切出来,
44/// 分隔条 = 一根贯穿的横线 + 一根贯穿的竖线,命中带也各是一条 → 拖竖线 = 整条一起亮、一起动。
45// 10 个参数是 2×2 的形状本身:两把刀(份额 + 回调)× 2 + 四格内容。拆成结构体反而更难读。
46#[allow(clippy::too_many_arguments)]
47pub fn quad(
48    ui: &mut Ui,
49    id: Id,
50    row_frac: f32,
51    col_frac: f32,
52    on_row: impl FnOnce(f32),
53    on_col: impl FnOnce(f32),
54    top_left: impl FnOnce(&mut Ui),
55    top_right: impl FnOnce(&mut Ui),
56    bottom_left: impl FnOnce(&mut Ui),
57    bottom_right: impl FnOnce(&mut Ui),
58) {
59    let total = ui.available_rect_before_wrap();
60    if total.width() <= 0.0 || total.height() <= 0.0 {
61        return; // 折叠/零面积:没得画,也没得拖
62    }
63    let row = clamp_frac(row_frac);
64    let col = clamp_frac(col_frac);
65    // 先把整块面积吃进父布局(否则父层随后会出现假滚动条/布局逃逸)。
66    ui.allocate_rect(total, Sense::hover());
67
68    let half = HANDLE * 0.5;
69    let y = total.top() + total.height() * row;
70    let x = total.left() + total.width() * col;
71    let quads = [
72        Rect::from_min_max(total.left_top(), Pos2::new(x - half, y - half)),
73        Rect::from_min_max(
74            Pos2::new(x + half, total.top()),
75            Pos2::new(total.right(), y - half),
76        ),
77        Rect::from_min_max(
78            Pos2::new(total.left(), y + half),
79            Pos2::new(x - half, total.bottom()),
80        ),
81        Rect::from_min_max(Pos2::new(x + half, y + half), total.right_bottom()),
82    ];
83    ui.scope_builder(UiBuilder::new().max_rect(quads[0]), top_left);
84    ui.scope_builder(UiBuilder::new().max_rect(quads[1]), top_right);
85    ui.scope_builder(UiBuilder::new().max_rect(quads[2]), bottom_left);
86    ui.scope_builder(UiBuilder::new().max_rect(quads[3]), bottom_right);
87
88    // 分隔条画在四块内容之上(epaint 无 z-order,后画在上)。
89    let p = ui.painter();
90    let h_band = Rect::from_min_max(
91        Pos2::new(total.left(), y - half),
92        Pos2::new(total.right(), y + half),
93    );
94    let v_band = Rect::from_min_max(
95        Pos2::new(x - half, total.top()),
96        Pos2::new(x + half, total.bottom()),
97    );
98    p.rect_filled(h_band, 0.0, shadcn::BG1);
99    p.rect_filled(v_band, 0.0, shadcn::BG1);
100
101    let h_resp = ui.interact(h_band, id.with("handle_h"), Sense::drag());
102    let v_resp = ui.interact(v_band, id.with("handle_v"), Sense::drag());
103    let mut h_color = shadcn::BORDER;
104    let mut v_color = shadcn::BORDER;
105    if h_resp.dragged() {
106        h_color = shadcn::ACCENT4;
107        if total.height() > 1.0 {
108            let moved = clamp_frac(row + h_resp.drag_delta().y / total.height());
109            if moved != row {
110                on_row(moved);
111            }
112        }
113    } else if h_resp.hovered() {
114        h_color = shadcn::ACCENT4;
115    }
116    if v_resp.dragged() {
117        v_color = shadcn::ACCENT4;
118        if total.width() > 1.0 {
119            let moved = clamp_frac(col + v_resp.drag_delta().x / total.width());
120            if moved != col {
121                on_col(moved);
122            }
123        }
124    } else if v_resp.hovered() {
125        v_color = shadcn::ACCENT4;
126    }
127    p.line_segment(
128        [Pos2::new(total.left(), y), Pos2::new(total.right(), y)],
129        egui::Stroke::new(1.0, h_color),
130    );
131    p.line_segment(
132        [Pos2::new(x, total.top()), Pos2::new(x, total.bottom())],
133        egui::Stroke::new(1.0, v_color),
134    );
135}