ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

gpui-kit Dock 停靠布局完全指南:从 PaneTree 数据模型到自定义渲染器

gpui-kit Dock 停靠布局完全指南:从 PaneTree 数据模型到自定义渲染器 gpui-kit Dock 停靠布局完全指南从 PaneTree 数据模型到自定义渲染器【免费下载链接】gpui-kitRust GUI components for building fantastic cross-platform desktop application by using GPUI.项目地址: https://gitcode.com/GitHub_Trending/gp/gpui-kitDock 是 gpui-kit 中gpui-base提供的无样式停靠布局基础用于在 GPUI 桌面应用中构建可拖放、可拆分、可持久化的停靠工作区。本文以中文版 Dock 文档 为主体骨架结合 crates/base/src/dock 源码展开帮助你掌握从布局建模、面板生命周期管理、DockArea操作到三套渲染器接口的完整实践最终能基于纯数据布局树PaneTree搭建属于自己的 IDE 式工作区。模型布局是一棵纯数据树在gpui-base的 Dock 设计中布局不是视图树而是一个值。整个布局是一棵由分栏、标签组、面板和可选 tiles 画布组成的树可以用PaneTree表示能够在没有Window、没有App的情况下被构建、比较、序列化和编辑。三种容器与四个区域布局树只有三种容器节点没有“叶子”变体因此面板只能存在于Tabs或Tiles之中——即使一个区域的中心只有一个面板它仍然是一个只含一个面板的Tabs容器承载内容说明Split沿一条轴排列的其他容器每个子槽位有可选的固定尺寸Tabs面板同一时刻显示一个携带当前显示的下标active_ixTiles自由位置上的面板每个 tile 有边界 bounds 与 z-indexDockArea管理四个区域中心center以及可选的左、右、下三个边缘 dock。每个区域都是一棵独立的PaneTree见 dock/mod.rs 的模块文档。两个稳定身份NodeId 与 PanelIdNodeId寻址一个容器。它历经每一次编辑和每一条规范化规则而保持不变——一个拖放后仍然存在的容器携带的是拖放前那个 id。id 由进程内全局计数器分配见 layout/tree.rs 中的NEXT_NODE_ID因此节点 id 在四个区域之间全局唯一不会出现两个树 mint 出相同 id 而抢占DockAreaentity 缓存槽位的情况。PanelId寻址一个面板。它包装了面板实体的EntityId只要实体存活就有效——无论面板在标签组之间、区域之间移动多少次。树与任何节点都不存储 GPUI entity 句柄这是布局代数可以在没有App的情况下被测试的关键前提。关键类型一览类型角色PaneTree一个区域的布局纯数据PaneNode/PaneRef节点以及你match的借用投影NodeId/PanelId稳定的容器与面板身份DockArea持有树、将树调和为实体、路由拖放与持久化DockLayout不构造任何实体即可描述布局Panel可停靠视图实现的 trait——只含行为PanelView对象安全的面板句柄Arcdyn PanelViewTabGroup/TilesStateTabs节点与Tiles节点背后的实体DockAreaRenderer/TabGroupRenderer/TilesRenderer一切视觉决策的归属处DockContext/TabGroupContext/TileContext交给 renderer 的已解析状态与回调DockAreaState整个区域的序列化形式开始使用创建 DockArea 并安装初始布局先创建持久化的DockAreaentity注册面板工厂与 renderer再安装初始布局。核心要点渲染期间只读取模型添加、移动、关闭、缩放与锁定操作放在回调中执行并通知 GPUI。use std::rc::Rc; use gpui_kit::base::dock::{DockArea, DockLayout, DockPlacement}; let area cx.new(|cx| { DockArea::new(workspace, Some(1), window, cx).with_renderer(Rc::new(MySkin)) }); area.update(cx, |area, cx| { area.set_center( DockLayout::h_split() .child(DockLayout::tabs().panel(files.clone()), Some(px(240.))) .child(DockLayout::tabs().panel(editor.clone()), None), window, cx, ); area.set_dock( DockPlacement::Bottom, DockLayout::tabs().panel(terminal.clone()), window, cx, ); });DockArea::new接受一个 id属于你用于你自己的持久化和一个可选的 schema 版本号。不调用.with_renderer(...)的区域仍然可以停靠、拖放、缩放和持久化——它只是除面板本身之外什么都不绘制。DockArea是一个 entity应在应用构造时创建一次每帧重建会丢弃用户已经摆放好的布局见 showcase dock 示例 的注释。描述布局DockLayout 构建器DockLayout构建一棵树而不触碰window或cx因为构建树不会构造任何实体见 layout/builder.rs。下例描述了一个典型的编辑工作区左侧 240px 的文件浏览器右侧是上下分栏——上为含编辑器和 Diff 视图的标签组默认激活第 2 个标签下为 180px 的控制台DockLayout::h_split() .child(DockLayout::tabs().panel(explorer.clone()), Some(px(240.))) .child( DockLayout::v_split() .child( DockLayout::tabs() .panel(editor.clone()) .panel(diff.clone()) .active_index(1), None, ) .child(DockLayout::tabs().panel(console.clone()), Some(px(180.))), None, )构建器方法构建器产物h_split()/v_split()沿该轴的分栏child(layout, size)向分栏添加一个子容器tabs()一个标签组panel(entity)向标签组添加一个面板active_index(ix)初始显示哪个标签tiles()一个 tiles 画布tile(entity, bounds)在画布上放置一个面板此外还有两个面向已擦除句柄的变体panel_view(Arcdyn PanelView, cx)与tile_view(...)供持有Arcdyn PanelView的调用方使用见 layout/builder.rs。误用——例如向 split 添加面板、向标签组添加子容器——会触发debug_assert!并被忽略。构建器选择运行时断言而非类型状态机是为了避免四个标记类型泄漏进每个消费者签名见 layout/builder.rs 的设计说明。槽位尺寸沿分栏轴方向的约束child(layout, size)中的size是槽位沿分栏轴方向的长度h_split中为宽度v_split中为高度。Some(px(240.))固定该槽位None表示不约束——槽位与其余无约束兄弟均分剩余空间。所有槽位均为None时布局均分空间。当一个面板被拖到既有面板旁边而没有指定尺寸时它占走落点旁边空间的一半。规范化每一次编辑都收敛到规范形态每一次编辑在返回前都会执行一轮收敛到不动点的折叠。规则自底向上应用见 layout/normalize.rs空的Tabs、Tiles或Split从父节点移除只有一个子节点的Split被其子节点替换——子节点保留自己的NodeId并继承该 split 的槽位尺寸子节点为同轴Split时将该子节点的子节点拼接进自身并按比例缩放它们的尺寸以填满槽位见distribute_slotlayout/normalize.rsactive_ix被钳制到面板数量以内中心的根保持为Split即使为空保证空中心仍序列化为StackPaneldock 的根不受此约束RootKind::Split与RootKind::Any见 layout/tree.rs。规范化有MAX_NORMALIZE_PASSES 64的硬上限作为安全网在 release 构建中若未达到不动点会输出tracing::warn!而非崩溃见 layout/normalize.rs。测试证实真实对抗性树在 2 次 pass 内收敛normalize_converges_within_two_passes_on_an_adversarial_treelayout/normalize.rs。两个值得围绕它设计的结论你永远不必回避冗余嵌套——把节点包进同轴 split 是无害的因为规则 3 会把它拍平这也是split_at不需要复用父节点特判的原因调用方不存在观察到畸形树的窗口没有空容器、没有单子分栏、没有越界的活动下标。规范化是幂等的normalize(normalize(t)) normalize(t)有专门测试固定该性质layout/normalize.rs。从外部或旧版本载入布局后务必先规范化移除空节点、折叠无意义嵌套并修正非法比例同时保留稳定 ID。面板与生命周期Panel trait 的全部钩子面板对 base 的全部义务就是一个稳定的名字struct FilesPanel { focus_handle: FocusHandle } impl Panel for FilesPanel { fn panel_name(self) - static str { FilesPanel } } impl EventEmitterPanelEvent for FilesPanel {} impl Focusable for FilesPanel { fn focus_handle(self, _: App) - FocusHandle { self.focus_handle.clone() } } impl Render for FilesPanel { /* ... */ }panel_name在持久化布局中标识面板。选定后永远不要更改——它是已保存文件被读回的键。面板还必须实现EventEmitterPanelEvent、Focusable与RenderPanel的完整定义见 dock/panel.rs。PanelView则是其对象安全版本用于把异构面板实体放进同一个Arcdyn PanelView句柄EntityT有 blanket 实现dock/panel.rs。全部钩子方法默认值触发时机panel_name()必填任何时候面板被识别或写出visible(cx)true每次渲染 passclosable(cx)true关闭被提供或应用之前zoomable(cx)true缩放被应用之前on_added_to(group, ..)no-op面板加入标签组时附带其弱句柄set_active(active, ..)no-op每个真实的“是否为显示标签”边沿set_zoomed(zoomed, ..)no-op显示它的标签组放大或缩小时on_removed(..)no-op面板永久离开 dock 时dump(cx)仅名字DockArea::dump时生命周期契约这些契约精确且值得通读一遍set_active只在边沿触发。它以帧末净状态调用每个真实变化恰好一次通知在下一个 tick 投递。绝无同值重复绝无单帧内 false-then-true 翻转。占据活动槽位但被隐藏的面板仍会收到true——尽管渲染回退到第一个可见面板。被移除的面板不会收到false。on_removed才是去激活信号。如果你在set_active(false)中释放资源也必须在on_removed中释放。被移动的面板永远不会听到on_removed。把面板从一个标签组拖到另一个不会把它移出 dock因此它只收到新的on_added_to和其他通知。on_removed意味着真正离开被关闭或被整体性的set_center、set_dock、remove_dock或load替换。on_added_to先于任何set_active面板可以存下句柄并在首次激活时行动。set_zoomed只到达正在显示的面板。一个标签组只有一个缩放状态是可见面板填满 dock。组内其他标签的面板听不到任何消息缩放发生时未显示的面板永远不会被追溯通知。closable是许可而非保证。容器仍可拒绝——dock 的最后一个标签组就会拒绝因此 dock 不可能通过关闭被清空。隐藏的面板保留位置。visible返回false时面板仍在树中、仍在组内重新出现时回到原位。面板全部隐藏的容器会递归让出槽位——嵌套 split 中每个叶子都隐藏时不占空间。DockArea 操作DockArea拥有每棵PaneTree、以NodeId为键的容器实体缓存Tabs节点对应TabGroupTiles节点对应TilesStatesplit 对应缓存的ResizableState以及以PanelId为键的面板句柄表。任何编辑后它都会走一遍树为缓存中没有的 id 创建实体、为已消失的 id 丢弃条目并调用Panel::on_removed、把尺寸与活动下标推送给幸存者最后发出DockEvent::LayoutChanged见 dock/dock_area.rs 与 dock/mod.rs。安装布局area.set_center(layout, window, cx); area.set_dock(DockPlacement::Left, layout, window, cx); area.remove_dock(DockPlacement::Left, window, cx);每个调用都替换掉原有内容。被替换且不属于新布局的面板会收到on_removed。添加与移动面板area.add_panel(panel, DockPlacement::Left, Some(px(240.)), window, cx); area.add_tile(panel, DockPlacement::Center, bounds, window, cx); area.remove_panel(panel, window, cx); area.move_panel(panel_id, target, window, cx); area.split_at(node, panel_id, Placement::Right, window, cx);add_panel把面板放入该区域第一个标签组若区域不存在则创建。add_tile需要该区域已有 tiles 画布没有则成为 no-op。两者都有_view变体接收Arcdyn PanelView。边缘 dockarea.has_dock(DockPlacement::Left); area.is_dock_open(DockPlacement::Left); area.toggle_dock(DockPlacement::Left, window, cx); area.dock_size(DockPlacement::Left); area.set_dock_size(DockPlacement::Left, px(280.), window, cx); area.set_dock_collapsible(DockPlacement::Left, true, window, cx);关闭的 dock 保留其树与尺寸重新打开时两者都恢复。缩放缩放命名的是容器而非面板——标签组在其显示面板关闭后依然存活下一个标签接管时仍保持缩放状态。area.set_zoomed_in(node, window, cx); area.set_zoomed_out(window, cx); area.is_zoomed(); area.zoomed_group(); // OptionNodeId area.zoomed_tile(); // OptionPanelId通常的入口不是这些而是TabGroupContext::toggle_zoom/TileContext::toggle_zoom——皮肤绘制缩放控件的地方就有它。缩放结束于被缩放容器离开 dock或容器主动清除它——而不是某个无关面板被移除时。锁定与查询area.set_locked(true, window, cx)冻结重排无拖放、无关闭。读取与渲染不受影响。area.layout(DockPlacement::Center); // OptionPaneTree area.panel(panel_id); // OptionArcdyn PanelView area.is_empty(DockPlacement::Left, cx); area.is_locked(); area.bounds();修改前务必验证目标仍存在——事件发生后布局可能已经变化。直接编辑布局树以上操作最终都经由PaneTree。你可以亲自驱动它——例如为命令面板、键盘快捷键或恢复会话tree.insert_panel(panel, InsertTarget::Tabs { node, ix: None, activate: true }); tree.remove_panel(panel); tree.move_panel(panel, target); tree.split(node, panel, Placement::Right, Some(px(320.))); tree.set_active(node, 2); tree.set_sizes(node, vec![Some(px(200.)), None]); tree.set_tile_bounds(panel, bounds); tree.bring_to_front(panel);InsertTarget面板的落点变体含义Tabs { node, ix, activate }进入既有标签组可选地指定下标Split { node, placement, size }在某节点旁边新建标签组Tile { node, bounds }以给定边界放到 tiles 画布上每次编辑返回EditResultchanged()以及created_nodes()、removed_nodes()、removed_panels()、activated()、deactivated()。removed_panels排除移动——被移动面板的实体存活因此绝不能收到on_removed。读取一棵树tree.root(); // PaneNode tree.node_ids(); // VecNodeId, pre-order tree.panels(); // impl IteratorItem PanelId tree.find_node(node_id); // OptionPaneNode tree.find_panel_node(panel_id); // OptionNodeId match node.kind() { PaneRef::Split { axis, children, sizes } { /* ... */ } PaneRef::Tabs { panels, active_ix } { /* ... */ } PaneRef::Tiles { panels } { /* ... */ } }直接编辑适合批量迁移和恢复不适合普通交互。编辑后必须规范化并通过DockArea安装使索引、焦点、事件和渲染状态同步更新不要绕过宿主只修改一份外部副本。提供外观三个渲染器 traitgpui_kit::base::dock中没有任何东西绘制颜色、边框或尺寸——三个 trait 负责携带外观。renderer 接收只读上下文和明确回调不应拥有领域状态也不应在 render 中修改布局。DockAreaRenderer方法提供默认值frame区域最外层元素裸divcenter_frame容纳中心与底部 dock 的列裸divsplit_frame一个分栏的框架裸divrender_split_handle两槽位间的分隔条None→ base 的一像素线render_dock一个 dock 的 chrome标题条、折叠入口、缩放把手内容不加包裹build_placeholder本构建无法构造的面板的占位None→ 什么都不画tab_group_renderer必填—tiles_renderer必填—TabGroupRenderer方法提供默认值frame标签组外层元素裸divcontent_frame显示面板所在的元素裸divrender_tab_bar标签条无render_active_panel显示面板如何摆放面板填满框架render_drop_indicator显示落点的高亮无render_empty空标签组显示什么无TilesRenderer方法提供默认值frame画布裸divtile_frame单个 tile 的外层元素裸divrender_drag_bar必填——移动 tile 的条—render_resize_handlestile 的缩放入口无panel_frame面板所在元素裸divrender_overlay绘制在一切 tile 之上无grid_size吸附粒度不吸附Contextsrenderer 能拿到什么renderer永远不会看到拖放事件或鼠标位置。base 把拖放源、落点命中测试、焦点与键盘处理挂接到 renderer 返回的元素上并交给它已解析的状态与回调TabGroupContext—node()、panels()、active_ix()、active_panel()、drop_indicator()、is_zoomed()、is_collapsed()、can_close()、is_locked()、is_draggable()、is_droppable()动作有select_tab()、close()、toggle_zoom()、drag_panel()、drop_panel()、drop_item()。TileContext—node()、panel()、panel_id()、bounds()、z_index()、is_moving()、is_resizing()、can_close()、is_zoomed()、can_zoom()以及begin_move()/move_to()/end_move()、begin_resize()/resize_to()/end_resize()、bring_to_front()、toggle_zoom()、close()。DockContext—placement()、size()、is_open()、is_collapsible()以及toggle()、resize_to()。一个最小实现示例impl TabGroupRenderer for MySkin { fn render_tab_bar(self, group: TabGroupContext, _: mut Window, cx: mut App) - AnyElement { h_flex() .children(group.panels().iter().enumerate().map(|(ix, panel)| { div() .child(my_title(panel, cx)) .when(ix group.active_ix(), |this| this.font_semibold()) .on_click({ let group group.clone(); move |_, window, cx| group.select_tab(ix, window, cx) }) })) .into_any_element() } }每个钩子都以同样的方式可选拒绝一个就得到 base 为它提供的最小实现。render_split_handle是最清晰的例子——返回None时分隔条回退为一像素线颜色取自Theme::resizable。因此对分隔条没有意见的皮肤什么都不用实现有意见的皮肤替换绘制却不用触碰命中区域、光标或拖放逻辑。注意展示示例中的content_frame设置了.relative()因为落点指示器是相对它定位的showcase dock 示例。拖放与 Tiles 画布一次标签拖放由三部分组成皮肤只提供中间一部分。开始TabGroupContext::drag_panel(ix, cx)在标签可拖时返回一个DragPanel组被锁定、或面板是撑住某个 dock 开启的最后一个面板时它会拒绝。把它交给 GPUI 的on_drag。你返回的预览视图完全属于你base 自己的DragPanel什么都不渲染因为预览是外观示例中的DragPreview见 showcase dock 示例。落点拖放悬停期间base 解析它将要落的位置暴露为TabGroupContext::drop_indicator()——一个携带面板将占据矩形的DropIndicator。在render_drop_indicator中绘制它坐标相对 content frame因此该 frame 必须被定位。应用释放时drop_panel()把悬停变成TabGroupEvent::Drop { panel, source, target }区域以一次PaneTree::move_panel应用之。落到中间并入组朝向边缘则在那里拆分落到标签条则在该下标插入。宿主自有拖放你自己的任何东西都可以拖入 dock。把它包进AnyDragdrop_item()会报告为DockEvent::DragDrop { item, target }其中target说明它落在标签组还是 tiles 画布上。dock 不解释载荷。Tiles 画布Tiles节点把面板放在自由位置而非堆叠为标签。TilesState拥有几何移动、从五个方向中任一方向缩放、对相邻面板与网格的磁力吸附、z 排序、每次手势的撤销历史与单 tile 缩放。tile 的拖拽条是TilesRenderer唯一必填的部分——没有拖拽条的 tile 无法移动。其余一切都有默认值impl TilesRenderer for MySkin { fn render_drag_bar(self, tile: TileContext, _: mut Window, _: mut App) - AnyElement { div() .h(px(28.)) .on_mouse_down(MouseButton::Left, { let tile tile.clone(); move |event, window, cx| tile.begin_move(event.position, window, cx) }) .into_any_element() } fn grid_size(self, _: App) - Pixels { px(8.) } }吸附几何被导出供希望预览落点的皮肤使用magnetic_snap、snap_edge、round_to_grid、compute_resized_bounds、apply_boundary_constraints。提交前再次验证源与目标。事件与持久化事件表Emitter事件含义DockAreaLayoutChanged有变化发生。每次编辑都触发包括 tile 拖动的每一步——写盘前务必去抖DockAreaDragDrop { item, target }宿主自有拖放落定TabGroupDrop/DragDrop/ClosePanel/ActiveChanged/ZoomIn/ZoomOut标签组的意图由区域应用TilesStateBoundsChanged/BringToFront/ClosePanel/DragDrop/ZoomIn/ZoomOut画布的意图PanelZoomIn/ZoomOut/LayoutChanged面板自身的信号容器事件是容器向区域请求某事真正编辑树的总是区域。宿主通常只订阅DockEvent。持久化let state: DockAreaState area.read(cx).dump(cx); let json serde_json::to_string(state)?; let state: DockAreaState serde_json::from_str(json)?; area.update(cx, |area, cx| area.load(state, window, cx))?;DockAreaState携带你传给DockArea::new的版本号、中心布局以及每个 dock 的位置、尺寸与开启状态。每个节点写出自己的形状每个面板写出自己的dump返回值。持久化稳定 ID、节点种类、分栏比例、标签顺序、活动项、边缘 dock 与面板自有数据不要序列化 GPUI entity、焦点句柄或 renderer。恢复时容忍未知面板类型并对版本化数据做迁移和规范化。面板通过以panel_name为键的全局注册表重建register_panel(cx, FilesPanel, |context, window, cx| { let state context.state(); // the PanelState this panel dumped Arc::new(FilesPanel::restore(state, cx)) as Arcdyn PanelView });注册表实现见 dock/registry.rsregister_panel把构建器放进一个GlobalPanelRegistrybuild_panel按名查找。值得注意的设计构建器返回Arcdyn PanelView而非裸AnyView否则DockArea::load无法在恢复后向面板询问其dump已注册面板自己的持久化载荷会在 load 后的第一次保存中被丢弃dock/registry.rs。三个值得知道的行为未注册的面板不会被丢弃。它变成携带原状态的占位符——由某个带面板的构建保存的布局即使你的构建不认识那个面板也能原样往返而不丢失。槽位尺寸在写出时被解析。dump写出 split 实际绘制时所用的尺寸而不是树构建时所用的且绝不写出零。LayoutChanged触发频率远超你愿意保存的频率。去抖或按定时器/窗口关闭时保存。架构取舍为什么布局是数据gpui-base把布局引擎与绘制完全分离引擎什么都不画。在自带 chrome 的库中定制是一层层叠加在默认外观上的覆盖且受限于它暴露的接缝在这里renderer 返回元素base 把行为挂接到那些元素上因此两种互不相关的皮肤可以坐落在同一套行为之上——crates/component/src/dock与 showcase 示例 恰好就是如此。数据模型换来三个可观察的性质见 dock/mod.rs拖放不会重置它没碰过的东西。当容器即视图Qt、AvalonDock 模型时重排意味着创建和销毁视图拖放可能重置仅与所移动面板共享父级的那些面板的状态滚动偏移、焦点、进行中的输入。这里身份是经受住每次编辑与规范化规则的NodeId调和是对实体缓存的 diff稳态 pass 不创建也不销毁任何东西。折叠是纯函数而非延迟级联。最后一个面板离开标签组时组必须从父节点移除自己这可能逐级清空父节点。容器即视图时这是两个类型之间经由父句柄的相互递归——而父句柄必须在构造后安装在 GPUI 中意味着延迟 pass即存在树与自身不一致的窗口。normalize是一轮到不动点的后序 pass无父指针、无延迟工作编辑返回的瞬间树就自洽。编辑不花渲染成本。insert_panel、move_panel、split等操作作用于一个值不分配实体、不请求布局因此一串编辑可以先执行、被检查再绘制任何东西。这也是整个布局代数能用普通#[test]无需TestAppContext测试的原因——折叠规则因此拥有它们现有的覆盖率。成本也讲清楚一次编辑克隆一次树用于 diff规范化走到不动点真实布局两次 pass硬上限远高于此。对 dock 量级的树——几十个节点——两者相对一帧都可忽略。这不适合拥有数千节点的结构。命名映射词汇表尽量跟随同类系统。注意一词多义这里的Panel是可停靠内容与 dockview 一致VS Code 用 panel 指底部区域rc-dock 用它指标签容器。移植概念前先翻译这个词。Qt 是离得最远的异类它没有独立的节点类型因为QDockWidget同时是内容与布局编排的对象。可运行示例以下示例只依赖gpui-base本身——面板、布局以及实现全部三个 renderer trait 的皮肤不依赖gpui-component。这正是设计意图base 可独立使用想要不同外观的宿主只需写另一套皮肤。完整源码在 crates/base/examples/showcase/components/dock.rscargo run -p gpui-base-examples -- dock示例中值得留意的实现细节ShowcasePanel对 base 的唯一义务是稳定的panel_name标题与正文是示例自己的通过把 base 给的句柄 downcast 回EntityShowcasePanel到达皮肤dock.rs。render_split_handle只替换绘制——命中区域、光标与拖放逻辑仍由 base 提供dock.rs。dock 的 resize 拖动跟随指针越过整个区域因此监听器挂在 area frame 上而非拖拽条上拖拽条把DockContext暂存进RcRefCelldock.rs 与frame实现。示例不构建 tiles 画布但DockAreaRenderer仍必须命名一个 tiles renderer——因为布局或持久化文件中可能恰好含有一个Tiles节点dock.rs。面板隐藏时保留其在树中与标签条中的位置由皮肤决定不绘制它dock.rs。集成检查清单面板、标签组和需要持久化的节点使用稳定 ID给每个面板一个永不更改的panel_name并在调用load之前注册其构建器。安装 renderer否则除面板本身外什么都画不出来。领域状态保存在面板或应用 entityrenderer 只负责表现。在on_removed中释放资源而不仅是在set_active(false)中——离开的面板永远不会被告知false。布局修改只发生在事件回调并在安装/恢复后规范化。持久化LayoutChanged前先去抖——它在拖动的每一步都触发。面板需要回到原位时优先用visible而非移除。处理未知面板、空布局、关闭否决、拖放目标失效和版本迁移。定位content_frame返回的元素否则落点指示器没有锚点。验证键盘焦点、标签顺序、锁定、缩放、边缘 dock、减少动态效果和高对比度。【免费下载链接】gpui-kitRust GUI components for building fantastic cross-platform desktop application by using GPUI.项目地址: https://gitcode.com/GitHub_Trending/gp/gpui-kit创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表