feat: 顶带 Shimo 化重设计 + 第一版功能减法
- 顶带:实心圆窗口按钮、无边框 tab 胶囊、右上 mosh/SSH 协议徽标 - 减法:删「新建 window/回首页」按钮、tab 组 +、pane 拖拽调宽, window/pane 管理交还 tmux(连带清理 resizePane/newWindow/ui-resize) - 底部状态行去重:只留链路 + tmux 概览,协议由顶带徽标独占 - 徽标全局统一:淡染底+同色文字胶囊(线框废除),拼写走 TXTech 规范 - CLAUDE.md 记录无头截图横屏帧缓冲右侧裁切大坑 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -282,7 +282,7 @@ struct TerminalScreen: View {
|
||||
// 只在**朝轨道那一侧**留 2pt 缝;右边直接铺到屏幕物理边缘(沉浸)。
|
||||
// 底边不留缝:pane 底边即与侧边栏脚同高的那条分割线。
|
||||
.padding(.leading, TX.Layout.paneGap)
|
||||
TerminalActionBar(session: session, actions: barActions, safeBottom: safeBottom)
|
||||
TerminalActionBar(session: session, safeBottom: safeBottom)
|
||||
}
|
||||
.background(p.bgDeep)
|
||||
}
|
||||
@@ -348,18 +348,6 @@ struct TerminalScreen: View {
|
||||
router.goHome()
|
||||
}
|
||||
|
||||
/// 底部动作(**触控可点**,不是快捷键提示)。新建 window 在原生窗口下灰显。
|
||||
/// (分屏入口已随「放弃移动端分屏」移除;左侧状态行见 `TerminalActionBar.statusLine`。)
|
||||
private var barActions: [TerminalActionBar.Action] {
|
||||
let tmux = session.tmuxController
|
||||
return [
|
||||
.init(id: "window", icon: "plus.rectangle.on.rectangle", title: "新建 window",
|
||||
enabled: tmux != nil) {
|
||||
tmux?.newWindow()
|
||||
},
|
||||
.init(id: "home", icon: "house", title: "回首页") { router.goHome() },
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
/// 普通(非 tmux)单终端视图。
|
||||
@@ -424,7 +412,9 @@ struct TmuxStage: View {
|
||||
}
|
||||
}
|
||||
|
||||
/// 一个 window:按 tmux 可见布局树把多 pane 绝对定位为分屏(每 pane 一 surface)+ pane 间拖拽条。
|
||||
/// 一个 window:按 tmux 可见布局树把多 pane 绝对定位为分屏(每 pane 一 surface)。
|
||||
/// pane 尺寸调整**不做 app 侧 UI**(第一版减法,用户裁决):交给 tmux 自己(prefix+方向键等),
|
||||
/// app 只被动跟随 `%layout-change`。
|
||||
struct TmuxWindowView: View {
|
||||
@ObservedObject var window: TmuxWindow
|
||||
let controller: TmuxController
|
||||
@@ -455,10 +445,6 @@ struct TmuxWindowView: View {
|
||||
y: (CGFloat(item.rect.y) + CGFloat(item.rect.height) / 2) * ch)
|
||||
}
|
||||
}
|
||||
// pane 之间的拖拽条(在 pane 之上,热区 12pt)。
|
||||
ForEach(splitters(layout.root), id: \.id) { s in
|
||||
PaneSplitter(splitter: s, cellW: cw, cellH: ch, controller: controller)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Color.clear
|
||||
@@ -476,126 +462,6 @@ struct TmuxWindowView: View {
|
||||
}
|
||||
}
|
||||
|
||||
/// 收集所有相邻子节点之间的分隔位置。tmux 的 `resize-pane -x/-y` 作用于**包含该 pane 的那层分割**,
|
||||
/// 故 resize 目标取"前一个子节点里的任一 pane"即可。
|
||||
private func splitters(_ node: TmuxLayout.Node) -> [SplitterSpec] {
|
||||
var out: [SplitterSpec] = []
|
||||
func walk(_ n: TmuxLayout.Node) {
|
||||
switch n {
|
||||
case .leaf:
|
||||
break
|
||||
case .horizontal(let children, _):
|
||||
// 左右并排 → 竖直分隔条,拖动改前一个子节点的**宽度**。
|
||||
for (i, child) in children.enumerated() where i < children.count - 1 {
|
||||
let r = child.rect
|
||||
if let pane = firstPane(child) {
|
||||
out.append(SplitterSpec(
|
||||
id: "h-\(pane.raw)-\(i)", isVertical: true, pane: pane,
|
||||
x: r.x + r.width, y: r.y, length: r.height, currentSize: r.width))
|
||||
}
|
||||
}
|
||||
children.forEach(walk)
|
||||
case .vertical(let children, _):
|
||||
// 上下堆叠 → 水平分隔条,拖动改前一个子节点的**高度**。
|
||||
for (i, child) in children.enumerated() where i < children.count - 1 {
|
||||
let r = child.rect
|
||||
if let pane = firstPane(child) {
|
||||
out.append(SplitterSpec(
|
||||
id: "v-\(pane.raw)-\(i)", isVertical: false, pane: pane,
|
||||
x: r.x, y: r.y + r.height, length: r.width, currentSize: r.height))
|
||||
}
|
||||
}
|
||||
children.forEach(walk)
|
||||
}
|
||||
}
|
||||
walk(node)
|
||||
return out
|
||||
}
|
||||
|
||||
private func firstPane(_ node: TmuxLayout.Node) -> TmuxPaneID? {
|
||||
switch node {
|
||||
case .leaf(let p, _): return p
|
||||
case .horizontal(let cs, _), .vertical(let cs, _): return cs.compactMap(firstPane).first
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 一条分隔位置(格子坐标)。
|
||||
struct SplitterSpec {
|
||||
let id: String
|
||||
/// true = 竖直条(左右分,拖动改宽);false = 水平条(上下分,拖动改高)
|
||||
let isVertical: Bool
|
||||
/// resize 目标 pane(前一个子节点里的任一 pane)
|
||||
let pane: TmuxPaneID
|
||||
let x: Int, y: Int
|
||||
/// 条的长度(格子)
|
||||
let length: Int
|
||||
/// 前一个子节点当前的 cols / rows
|
||||
let currentSize: Int
|
||||
}
|
||||
|
||||
/// pane 拖拽条:热区 12pt、把手 44×3pt。
|
||||
///
|
||||
/// **拖动中只动视觉**(把手位移 + 高亮线),不改 pane frame、不发命令;**松手才发一次 `resize-pane`**,
|
||||
/// 然后等 tmux 回 `%layout-change` 更新真实 frame —— 与"路线 b"一致(客户端不自行重排)。
|
||||
struct PaneSplitter: View {
|
||||
let splitter: SplitterSpec
|
||||
let cellW: CGFloat
|
||||
let cellH: CGFloat
|
||||
let controller: TmuxController
|
||||
@State private var drag: CGFloat = 0
|
||||
@State private var dragging = false
|
||||
@EnvironmentObject var theme: TXThemeManager
|
||||
private var p: TXPalette { theme.palette }
|
||||
|
||||
private var hit: CGFloat { TX.Layout.paneDragHit }
|
||||
|
||||
var body: some View {
|
||||
Group {
|
||||
if splitter.isVertical {
|
||||
bar(width: hit, height: CGFloat(splitter.length) * cellH)
|
||||
.position(x: CGFloat(splitter.x) * cellW + drag,
|
||||
y: (CGFloat(splitter.y) + CGFloat(splitter.length) / 2) * cellH)
|
||||
} else {
|
||||
bar(width: CGFloat(splitter.length) * cellW, height: hit)
|
||||
.position(x: (CGFloat(splitter.x) + CGFloat(splitter.length) / 2) * cellW,
|
||||
y: CGFloat(splitter.y) * cellH + drag)
|
||||
}
|
||||
}
|
||||
.gesture(
|
||||
DragGesture(minimumDistance: 2)
|
||||
.onChanged { v in
|
||||
dragging = true
|
||||
drag = splitter.isVertical ? v.translation.width : v.translation.height
|
||||
}
|
||||
.onEnded { _ in
|
||||
// 位移 → 格子增量;松手发一次命令,视觉位移立刻归零(等 tmux 回布局)。
|
||||
let cell = splitter.isVertical ? cellW : cellH
|
||||
let delta = cell > 0 ? Int((drag / cell).rounded()) : 0
|
||||
let newSize = max(1, splitter.currentSize + delta)
|
||||
if delta != 0 {
|
||||
controller.resizePane(splitter.pane,
|
||||
cols: splitter.isVertical ? newSize : nil,
|
||||
rows: splitter.isVertical ? nil : newSize)
|
||||
}
|
||||
drag = 0
|
||||
dragging = false
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
/// 热区透明 + 居中 44×3pt 把手(拖动中把手转强调色)。
|
||||
private func bar(width: CGFloat, height: CGFloat) -> some View {
|
||||
ZStack {
|
||||
Color.clear.frame(width: width, height: height)
|
||||
RoundedRectangle(cornerRadius: 2)
|
||||
.fill(dragging ? TXAccent.base : p.borderStrong)
|
||||
.frame(width: splitter.isVertical ? 3 : 44,
|
||||
height: splitter.isVertical ? 44 : 3)
|
||||
.opacity(dragging ? 1 : 0.55)
|
||||
}
|
||||
.contentShape(Rectangle())
|
||||
}
|
||||
}
|
||||
|
||||
/// 单个 pane 的终端面 + 焦点边框 + 点击选中。pane 之间留 2pt 缝(设计:舞台底色透出为缝)。
|
||||
|
||||
Reference in New Issue
Block a user