把中间区域从「标签页 + 最多两格分屏」升级成一块可以随意切分的画布:左右、上下、四宫格、九宫格、十六宫格,或者不规则的组合都可以。本页上半部分是可以直接上手操作的原型,下半部分是数据模型、交互规则和实施计划。
布局用一棵树表示。叶子是格子(pane),中间节点是切分(split)。一个切分节点可以有任意多个子节点,每个子节点带一个比例值。这样九宫格就是「一个纵向切分,下面挂三个横向切分,每个横向切分再挂三格」,不需要二叉树那样层层嵌套。
type LayoutNode =
| { kind: "pane"; id: PaneId; content: TabRef | null } // null = 空格
| { kind: "split"; id: string; dir: "row" | "col";
children: LayoutNode[]; sizes: number[] }; // sizes 之和为 1
type CanvasLayout = {
root: LayoutNode;
focusedPaneId: PaneId;
zoomedPaneId?: PaneId; // 放大某一格时其余格不卸载,只是隐藏
};
每次修改之后都要做一遍规范化,这样树永远保持最简形状:
这个模型取代现在 center-tab-groups.ts 里的 MAX_CENTER_TAB_GROUP_MEMBERS = 2 分组模型。旧的两格分组可以无损迁移:迁移后就是一个 row 切分下挂两个格子。
| 松手位置 | 结果 |
|---|---|
| tab 栏内 | 重新排序,和现在一样。 |
| 某一格中央方块以外的区域(按离哪条边最近分成左、右、上、下四块) | 在这一侧切出新格,被拖的内容放进去;它随即从 tab 栏里消失,也就是「融入」画布。 |
| 某一格的中央方块(宽高各占中间 40%) | 替换这一格的内容,被换下来的内容回到 tab 栏。如果这一格是空格,就直接填进去。 |
| 画布最外沿,距离边框 6px 以内 | 在整个画布的这一侧加一整列或一整行,而不是只切某一格。 |
| 右侧 Resources 面板 | 和现在一样,把网页绑定成会话资源。 |
| 窗口以外的任何地方 | 新开一个窗口承载这个内容。 |
拖动的视觉效果:被拖的 tab 就是那个元素本身,用 transform 跟着鼠标走,离开 tab 栏以后照样跟着,原来的位置留出空位。它完全不透明,背景色是 color-mix(in srgb, 当前tab色 50%, bg-tertiary),外加一圈细边和一层阴影。不再显示额外的蓝色「New window」提示条,落点的效果改由 drop 预览框直接表达。
单屏、左右、上下、主 + 2、四宫格、九宫格、十六宫格。套用预设时,已经在画布里的内容按阅读顺序依次填进新格子;内容多出来就退回 tab 栏,格子多出来就留成空格。
| 内容类型 | 最小宽 × 高 | 格子太小时的处理 |
|---|---|---|
| 会话 | 280 × 200 | 隐藏侧边信息,输入框压缩成单行 |
| 网页 | 320 × 200 | 低于这个尺寸时原生视图冻结成截图 |
| 终端 | 200 × 120 | xterm 自己重新计算行列数 |
| 文件或程序 | 200 × 120 | — |
再往下压,就进入「迷你态」:格子只显示标题和类型,点一下就放大。原型里把宽度小于 150px 或高度小于 110px 的格子当作迷你态来演示。
WebContentsView,永远盖在 DOM 上面,所以:
registerVisibleWebTabBounds 上报自己的 bounds,缝隙拖动时要按帧同步;capturePage 截图并隐藏原生视图,松手后再恢复,否则 drop 预览框和被拖的 tab 会被网页挡住;window-state 设计),格子里只存 TabRef,不存内容本身。TabRef 变成空格,不让整个布局崩掉。| 阶段 | 内容 | 涉及文件 |
|---|---|---|
| P0 拖动手感 | tab 本身跟着鼠标走,拖出 tab 栏也跟着;拖动时不透明、用中间色;去掉蓝色 New window 提示条;拖动时冻结网页视图。 | use-tab-pointer-drag.ts、center-tabs.module.css、center-tab-strip.tsx、check-center-tabs.mjs |
| P1 布局树 | 新增 lib/tabs/canvas-layout.ts,包含纯函数的切分、移除、规范化、移动、预设、缝隙调整,并配完整单测;把现有两格分组迁移过来;渲染层改成递归的 CanvasNode 组件。 | center-tab-groups.ts、store/layout.ts、app-shell.tsx |
| P2 拖放分屏 | 五向 drop zone 加画布外沿、drop 预览框,拖格子标题栏,拖回 tab 栏;同步修订「分屏只能从右键菜单进入」这条设计约束。 | tab-drag-coordinator.ts(resolveTabDropIntent 增加 pane zone 这种 intent) |
| P3 多网页格 | 多个 WebContents 的 bounds 同步、活跃数上限、截图冻结。 | web-tab-pane.tsx、lib/desktop/*、desktop 主进程 |
| P4 完善 | 预设菜单、快捷键、放大、命名预设、持久化。 | — |
产品画布不显示常驻布局工具栏,也不预留工具栏高度。右键点击格子标题可使用预设、切分、等分和保存布局;拖放与快捷键直接可用。下方原型中的工具栏仅用于演示布局操作。
多格画布作为一个布局标签;同一内容只占一个格子;拖到格子中央替换内容,被替换的内容回到顶部标签栏。关闭最后一格后保留空布局,原内容仍在标签栏。
代码实现:布局树与旧两格迁移、预设、空格选择内容、五向拖放与外沿插入、格子拖回标签栏、分隔线调整与双击等分、放大还原、快捷键、命名预设及窗口持久化。拖动时原标签保持不透明并跟随指针;原生网页显示截图,结束后恢复。最多同时显示 8 个原生网页视图,其余以截图显示,点击切换;这不终止后台网页进程。跨窗口传输包含布局树,并保留传输体积与结构限制。
渲染通过递归布局计算得到格子与分隔线坐标,内容宿主保持稳定,避免调整树结构时重新挂载文件编辑器、终端和会话。放大仅隐藏其他格子。
把窗格拖到顶部标签栏时,显示与画布停靠一致的蓝色圆角落点预览。提示覆盖标签栏实际范围,移出或取消时消失;在范围内松手后恢复为独立标签。