Resizable
Resizable 组件系统用于构建可拖拽调整大小的面板布局,支持横向与纵向分割、嵌套布局、尺寸限制和拖拽句柄,适合实现 IDE、仪表盘或分栏视图。
#导入
use gpui_kit::component::resizable::{
h_resizable, v_resizable, resizable_panel, resize_handle,
ResizablePanelGroup, ResizablePanel, ResizableState, ResizablePanelEvent, HandleEdge
};
#用法
使用 h_resizable 创建横向布局,使用 v_resizable 创建纵向布局。
第一个参数是 [ResizablePanelGroup] 的 id。
h_resizable("my-layout")
.on_resize(|state, window, cx| {
let state = state.read(cx);
let sizes = state.sizes();
})
.child(
resizable_panel()
.size(px(200.))
.child("Left Panel")
)
.child(
div()
.child("Right Panel")
.into_any_element()
)
纵向布局写法类似:
v_resizable("vertical-layout")
.child(
resizable_panel()
.size(px(100.))
.child("Top Panel")
)
.child(
div()
.child("Bottom Panel")
.into_any_element()
)
#分隔条外观
分隔条静止时是一条细线。指针接近后,线上会浮出一条指示条,并随 hover、按下、拖拽三级逐步变长、变实——拖拽开始一两个像素后指针就离开了手柄自身的命中带,这条指示条让「正在拖拽」仍然看得出来。
h_resizable、v_resizable 和 Dock 都已装上这套外观。resize_handle_appearance() 也已导出,供自建手柄使用,或显式传给 with_handle_appearance:
gpui_kit::base::h_resizable("my-layout")
.with_handle_appearance(resize_handle_appearance())
指示条的时长与缓动取自主题的 motion token;系统开启了减弱动态效果时,它直接到位,不播过渡。
#独立手柄
不属于任何面板组的边缘——比如应用自己的侧栏——可以单独使用 resize_handle。手柄负责命中带、光标和指示条;拖拽改变什么由你决定:用 on_drag 给它一个拖拽负载,用 on_drag_move 跟踪这个负载,其他监听也照样挂在手柄上:
use gpui_kit::component::resizable::{HandleEdge, resize_handle};
div()
.relative()
.w(self.sidebar_width)
.h_full()
.on_drag_move(cx.listener(|this, e: &DragMoveEvent<ResizeSidebar>, _, cx| {
this.sidebar_width = e.event.position.x - e.bounds.left();
cx.notify();
}))
.child(sidebar_content)
.child(
resize_handle("sidebar-edge", Axis::Horizontal)
.inside(HandleEdge::Trailing)
.on_drag(ResizeSidebar, |drag, _, _, cx| cx.new(|_| drag.clone()))
.on_double_click(cx.listener(|this, _, _, cx| {
this.sidebar_width = DEFAULT_SIDEBAR_WIDTH;
cx.notify();
})),
)
inside 让整条命中带留在容器内,贴住指定的边;不调用时,命中带骑在边界上,两侧各占一半。命中带会遮挡身后的元素:指针停在上面时,它身后的元素(包括所在容器)都不再算作悬停。容器如果需要知道指针还在自己上方,要在手柄上也挂一个监听。
#面板尺寸约束
resizable_panel()
.size(px(200.))
.size_range(px(150.)..px(400.))
.child("Constrained Panel")
#固定面板
固定面板在所在容器改变大小时保持自身尺寸,其余面板按各自尺寸的比例分摊差值;拖拽它自己的分隔条仍可调整它的大小。适合侧边栏、检查器这类应保持用户所设宽度的面板。固定面板需要设置 size。
resizable_panel()
.size(px(240.))
.size_range(px(160.)..px(480.))
.fixed()
.child("Sidebar")
#多面板布局
h_resizable("multi-panel", state)
.child(
resizable_panel()
.size(px(200.))
.size_range(px(150.)..px(300.))
.child("Left Panel")
)
.child(
resizable_panel()
.child("Center Panel")
)
.child(
resizable_panel()
.size(px(250.))
.child("Right Panel")
)
#嵌套布局
v_resizable("main-layout", window, cx)
.child(
resizable_panel()
.size(px(300.))
.child(
h_resizable("nested-layout", window, cx)
.child(
resizable_panel()
.size(px(200.))
.child("Top Left")
)
.child(
resizable_panel()
.child("Top Right")
)
)
)
.child(
resizable_panel()
.child("Bottom Panel")
)
#嵌套面板组
h_resizable("outer", window, cx)
.child(
resizable_panel()
.size(px(200.))
.child("Left Panel")
)
.group(
v_resizable("inner", window, cx)
.child(
resizable_panel()
.size(px(150.))
.child("Top Right")
)
.child(
resizable_panel()
.child("Bottom Right")
)
)
#条件显示面板
resizable_panel()
.visible(self.show_sidebar)
.size(px(250.))
.child("Sidebar Content")
#带尺寸上下限的面板
resizable_panel()
.size_range(px(100.)..Pixels::MAX)
.child("Flexible Panel")
resizable_panel()
.size_range(px(200.)..px(500.))
.child("Constrained Panel")
resizable_panel()
.size(px(300.))
.size_range(px(300.)..px(300.))
.child("Fixed Panel")
#示例
#文件浏览器布局
struct FileExplorer {
show_sidebar: bool,
}
impl Render for FileExplorer {
fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
h_resizable("file-explorer", window, cx)
.child(
resizable_panel()
.visible(self.show_sidebar)
.size(px(250.))
.size_range(px(200.)..px(400.))
.child(
v_flex()
.p_4()
.child("📁 Folders")
.child("• Documents")
.child("• Pictures")
.child("• Downloads")
)
)
.child(
v_flex()
.p_4()
.child("📄 Files")
.child("file1.txt")
.child("file2.pdf")
.child("image.png")
.into_any_element()
)
}
}
#IDE 布局
struct IDELayout {
main_state: Entity<ResizableState>,
sidebar_state: Entity<ResizableState>,
bottom_state: Entity<ResizableState>,
}
impl Render for IDELayout {
fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
h_resizable("ide-main", self.main_state.clone())
.child(
resizable_panel()
.size(px(300.))
.size_range(px(200.)..px(500.))
.child(
v_resizable("sidebar", self.sidebar_state.clone())
.child(
resizable_panel()
.size(px(200.))
.child("File Explorer")
)
.child(
resizable_panel()
.child("Outline")
)
)
)
.child(
resizable_panel()
.child(
v_resizable("editor-area", self.bottom_state.clone())
.child(
resizable_panel()
.child("Code Editor")
)
.child(
resizable_panel()
.size(px(150.))
.size_range(px(100.)..px(300.))
.child("Terminal / Output")
)
)
)
}
}
#仪表盘布局
struct Dashboard {
layout_state: Entity<ResizableState>,
widget_state: Entity<ResizableState>,
}
impl Render for Dashboard {
fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
v_resizable("dashboard", self.layout_state.clone())
.child(
resizable_panel()
.size(px(120.))
.child("Header / Navigation")
)
.child(
resizable_panel()
.child(
h_resizable("widgets", self.widget_state.clone())
.child(
resizable_panel()
.size(px(300.))
.child("Chart Widget")
)
.child(
resizable_panel()
.child("Data Table")
)
.child(
resizable_panel()
.size(px(250.))
.child("Stats Panel")
)
)
)
.child(
resizable_panel()
.size(px(60.))
.child("Footer")
)
}
}
#设置面板
struct SettingsPanel {
settings_state: Entity<ResizableState>,
}
impl SettingsPanel {
fn new(cx: &mut Context<Self>) -> Self {
let settings_state = ResizableState::new(cx);
cx.subscribe(&settings_state, |this, _, event: &ResizablePanelEvent, cx| {
match event {
ResizablePanelEvent::Resized => {
this.save_layout_preferences(cx);
}
}
});
Self { settings_state }
}
fn save_layout_preferences(&self, cx: &mut Context<Self>) {
let sizes = self.settings_state.read(cx).sizes();
println!("Saving layout: {:?}", sizes);
}
}
impl Render for SettingsPanel {
fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
h_resizable("settings", self.settings_state.clone())
.child(
resizable_panel()
.size(px(200.))
.size_range(px(150.)..px(300.))
.child(
v_flex()
.gap_2()
.p_4()
.child("Categories")
.child("• General")
.child("• Appearance")
.child("• Advanced")
)
)
.child(
resizable_panel()
.child(
div()
.p_6()
.child("Settings Content Area")
)
)
}
}
#最佳实践
- 为独立布局使用各自的
ResizableState。 - 始终为面板设置合理的最小和最大尺寸。
- 通过订阅
ResizablePanelEvent持久化用户布局。 - 使用
.group()构建清晰的嵌套结构。 - 避免无意义的深层嵌套以减少复杂度。
- 为拖拽句柄保留足够交互空间,提升体验。