fix(gpui): keep outline rows single-line so long headings cannot overlap (#191)

A heading label in the reader's outline wrapped once it exceeded the panel
width. The row's height is fixed during layout measurement, and the
horizontal available width of that scroll layer is not reliable there, so
the second line was painted outside its row box and landed on the next
heading's title. It only appeared with a narrow details panel and long
numbered headings.

Labels now use `truncate()`, so an outline row is always exactly one line
tall and its height no longer depends on the heading text. The full label
stays reachable through the row tooltip and the existing "copy heading"
context menu.

`Tooltip` now comes from the GPUI shell prelude, keeping third-party entry
points in the controlled list instead of importing `gpui_component` at the
call site.

`focus_tests` covers the contract: with the details panel at its minimum
width, the longest heading's row must be as tall as the shortest one's.

Co-authored-by: tataraDM <2201325624@qq.com>
Co-authored-by: Kuddev <christianmazel230@gmail.com>
This commit is contained in:
authored and GitHub committed 2026-09-19 18:54:24 +08:00
1 parent 75dc0b9481
commit c3f79d6a0e
3 files changed
+54 -1

No files matched your search

@@ -99,3 +99,36 @@ fn details_panel_drag_updates_shared_width_and_releases_on_mouse_up(cx: &mut Tes
assert!(window.selected_text(cx).is_empty(), "resizing must not select reader text");
});
}
/// 目录行高只由「一行」决定,与标题长度无关。
///
/// 目录是导航列,行高必须统一:长标题只截断,不折行。折行之所以是缺陷,是因为
/// 行高在布局测量阶段按单行定型,文字却在最终宽度下折成两行画到行框之外,压住
/// 下一行的标题(用户 09-17 在数学笔记目录上报告的窄面板重叠)。这种「画到框
/// 外」本身在布局 bounds 里看不见,所以这里钉住的是让它不可能发生的前提:面板
/// 压到最小宽度时,最长标题行仍与最短标题行同高。
#[gpui::test]
fn long_outline_headings_stay_one_line_in_a_narrow_details_panel(cx: &mut TestAppContext) {
let directory = tempfile::tempdir().unwrap();
let path = directory.path().join("outline.md");
let long = "## 这是一个在窄目录面板里一定会超出可用宽度的很长很长的标题";
std::fs::write(&path, format!("# 短标题\n\n{long}\n")).unwrap();
let (file, mut cx) = open(path, cx);
cx.update(|window, cx| {
file.update(cx, |view, cx| {
view.show_details = true;
view.info = false;
view.details_width = reader_presentation::DETAILS_MIN_WIDTH;
cx.notify();
});
let _ = window.draw(cx);
});
let short_row = cx.debug_bounds("outline-row-0").expect("短标题的目录行");
let long_row = cx.debug_bounds("outline-row-1").expect("长标题的目录行");
assert_eq!(
long_row.size.height, short_row.size.height,
"长标题必须在一行内截断:折行会画到行框之外压住下一行"
);
}
@@ -92,9 +92,28 @@ impl TextFileView {
.flex_1()
.min_w_0()
.py_1()
.whitespace_normal()
// 目录行只在**一行**内截断,绝不折行。行高由父级
// 的布局测量定型,而这一层的横向可用宽度在测量阶段
// 并不可靠(`overflow_y_scroll` 内容层不给百分比宽度
// 兜底)。一旦允许折行,文字会按最终宽度折成两行,却
// 仍被画在按单行算出的行框里——第二行落到行框之外,
// 压在下一行标题上(用户 09-17 报的重叠,窄面板 + 长
// 标题才出现)。改成一个目录行固定一行高之后,行高与
// 标题长度无关,重叠在结构上不可能发生;完整标题走
// 悬停提示与右键“复制标题”。
.truncate()
.child(heading.display_label()),
)
.tooltip({
let label = heading.display_label();
move |window, cx| {
let label = label.clone();
Tooltip::element(move |_, _| {
div().max_w(px(560.0)).whitespace_normal().child(label.clone())
})
.build(window, cx)
}
})
.on_click(cx.listener(move |view, _, window, cx| {
view.jump_to_heading(index, window, cx)
}))
+1
View File
@@ -29,6 +29,7 @@ pub use gpui_component::{
spinner::Spinner,
switch::Switch,
tab::{Tab, TabBar},
tooltip::Tooltip,
v_flex,
};