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