fix(terminal): 左侧/底部侧栏拖动改用工作区根元素的真实 bounds

侧栏拖动的宽度换算依赖 workspace_bounds,而它此前由
WorkspaceResizeEventHandler::prepaint 用元素自身 bounds 写入。该元素是
h_flex 里 0×0 的弹性子项,落在工作区右缘、垂直居中,于是:

- 左侧:`鼠标 x - 工作区右缘` 恒为负,每次拖动都被夹到最小宽度
  (280),表现为「移到左边后拖不动」。
- 底部:`工作区中线 - 鼠标 y` 同样为负,也被夹到最小宽度。
- 右侧:元素右缘恰好等于工作区右缘,才碰巧正常。

改为在工作区根 h_flex 上用 on_prepaint 捕获真实 bounds(与 db_view 的
写法一致),事件处理元素只保留鼠标监听。内嵌 dock(TerminalView 嵌入
资源工作台)的 view_bounds 同样从 window.bounds() 改为 dock 根元素的
on_prepaint,避免视图不在窗口原点时换算偏移。

新增两条契约测试分别锁定工作区与内嵌 dock 的 bounds 来源。

验证:cargo test -p terminal_view --lib 572 passed。
This commit is contained in:
胡飞
2026-11-02 11:09:57 +08:00
parent 5dcd1a07d0
commit ba1ebd0df8
6 changed files with 69 additions and 15 deletions
@@ -88,6 +88,7 @@ impl TerminalView {
) -> AnyElement {
let state = self.tool_dock_render_state(cx);
let view = cx.entity().clone();
let bounds_view = view.clone();
let bg_color = self.current_theme.background;
let center = CenterRegionState {
font_family,
@@ -106,6 +107,13 @@ impl TerminalView {
.min_w_0()
.min_h_0()
.overflow_hidden()
// 拖动换算要用 dock 根元素的真实 bounds;事件监听元素自身是 0×0,
// 而 window.bounds() 在嵌入面板里也不等于 dock 的位置。
.on_prepaint(move |bounds, _, cx| {
bounds_view.update(cx, |view, _| {
view.view_bounds = bounds;
});
})
.bg(bg_color)
.key_context(TERMINAL_CONTEXT)
.on_action(cx.listener(Self::start_recording_action))
@@ -40,16 +40,11 @@ impl Element for ResizeEventHandler {
_: Option<&InspectorElementId>,
_: Bounds<Pixels>,
_: &mut Self::RequestLayoutState,
window: &mut Window,
cx: &mut App,
_window: &mut Window,
_cx: &mut App,
) -> Self::PrepaintState {
let bounds = window.bounds();
self.view.update(cx, |view, _| {
view.view_bounds = Bounds {
origin: Point::default(),
size: bounds.size,
};
});
// view_bounds 由 dock 根元素的 on_prepaint 写入:本元素是 0×0 的弹性
// 子项,而窗口 bounds 在嵌入面板里也不等于 dock 的位置。
}
fn paint(
@@ -945,3 +945,24 @@ fn terminal_render_uses_cached_font_metrics() {
assert!(metric_source.contains("fn refresh_terminal_font_metrics("));
assert!(!render_setup.contains("cx.text_system().all_font_names()"));
}
/// 内嵌 dock 的拖动换算同样必须使用 dock 根元素的真实 bounds,
/// 不能用 `window.bounds()`(嵌入面板时原点/尺寸都不是 dock 的)。
#[test]
fn embedded_sidebar_resize_uses_dock_bounds_not_window_bounds() {
let render_layout = include_str!("../render_layout.rs");
let resize_event_handler = include_str!("../resize_event_handler.rs");
assert!(
render_layout.contains(".on_prepaint(move |bounds, _, cx|"),
"tool dock root must capture its own bounds via on_prepaint"
);
assert!(
render_layout.contains("view.view_bounds = bounds"),
"tool dock root prepaint must write view_bounds"
);
assert!(
!resize_event_handler.contains("window.bounds()"),
"resize event handler must not derive view bounds from the window"
);
}
+10 -1
View File
@@ -3,7 +3,7 @@ use gpui::{
AnyElement, Context, InteractiveElement as _, IntoElement as _, ParentElement as _,
Styled as _, div,
};
use gpui_component::{h_flex, v_flex};
use gpui_component::{ElementExt as _, h_flex, v_flex};
use one_core::layout::TOOLBAR_WIDTH;
use one_core::sidebar_contribution::SidebarPlacement;
@@ -76,12 +76,21 @@ impl TerminalWorkspace {
)
});
let workspace = cx.entity();
h_flex()
.id("terminal-workspace-root")
.size_full()
.min_w_0()
.min_h_0()
.overflow_hidden()
// 侧栏拖动换算要用工作区根元素的真实 bounds。事件监听元素本身是
// 0×0 的弹性子项,拿它自己的 bounds 当工作区边界会让左/下拖动
// 永远被夹到最小宽度。
.on_prepaint(move |bounds, _, cx| {
workspace.update(cx, |workspace, _| {
workspace.workspace_bounds = bounds;
});
})
.when_some(left, |this, panel| {
this.child(
div()
+4 -5
View File
@@ -116,14 +116,13 @@ impl Element for WorkspaceResizeEventHandler {
&mut self,
_: Option<&GlobalElementId>,
_: Option<&InspectorElementId>,
bounds: Bounds<Pixels>,
_: Bounds<Pixels>,
_: &mut Self::RequestLayoutState,
_: &mut Window,
cx: &mut App,
_: &mut App,
) -> Self::PrepaintState {
self.workspace.update(cx, |workspace, _| {
workspace.workspace_bounds = bounds;
});
// 工作区 bounds 由根元素的 on_prepaint 写入:本元素是 0×0 的弹性
// 子项,自身 bounds 不能当作工作区边界。
}
fn paint(
@@ -198,3 +198,25 @@ fn closing_pane_preserves_surviving_ratio(cx: &mut TestAppContext) {
assert_width(pane_width(visual, PANE_SELECTORS[1]), px(630.));
assert_eq!(2, visual.read(|cx| harness.read(cx).tree.panes().len()));
}
/// 侧栏拖动换算依赖 `workspace_bounds`。它必须来自工作区根元素的真实
/// bounds:事件监听元素是 0×0 的弹性子项,拿它自己的 bounds 会让左侧
/// 与底部拖动永远被夹到最小宽度(左侧面板拖不动)。
#[test]
fn sidebar_resize_bounds_come_from_the_workspace_root() {
let layout_source = include_str!("layout.rs");
let resize_source = include_str!("resize.rs");
assert!(
layout_source.contains(".on_prepaint(move |bounds, _, cx|"),
"workspace root must capture its own bounds via on_prepaint"
);
assert!(
layout_source.contains("workspace.workspace_bounds = bounds"),
"workspace root prepaint must write workspace_bounds"
);
assert!(
!resize_source.contains("workspace_bounds = bounds"),
"resize event handler is zero-sized; its bounds must not be used as the workspace bounds"
);
}