ui(app): command address menu items with native title/subtitle for #72

Picker(.menu) 的选项桥接会把多 Text 压平成独立菜单项(注记成了单独
item)。改为 Menu + adaptive-controls 桥接(Apple technote 文档化的
title/subtitle 机制):

- 菜单项主行 = IP,副标题 = 「注记 - 接口名称」(无注记时仅接口名称)
- 收起状态显示当前选中项的 {IP} - {name}(borderedButton 样式)
- 选择器下方的类型说明行保留,选中地址类型收起时也可见
This commit is contained in:
Qiu Yuzhou
2026-10-01 15:54:46 +08:00
parent 149cf39ea4
commit 24666df4ac
2 changed files with 20 additions and 18 deletions
+19 -17
View File
@@ -200,14 +200,25 @@ struct TerminalProxyEnvironmentCard: View {
content: {
VStack(spacing: 10) {
if control.snapshot.commandAddressPicker.isVisible {
Picker("命令地址", selection: commandAddressBinding) {
// Menu(而非 Picker):菜单项经 adaptive-controls 机制桥接,
// 两个 Text 映射为 NSMenuItem 的原生 title/subtitle;Picker 的
// 选项桥接会把多 Text 压平成独立菜单项。
Menu {
ForEach(
control.snapshot.commandAddressPicker.candidates, id: \.identity
) { candidate in
menuItemLabel(for: candidate).tag(candidate)
Button {
control.selectCommandAddress(candidate)
} label: {
Text(candidate.address)
Text(menuSubtitle(for: candidate))
}
}
} label: {
Text(control.snapshot.commandAddressPicker.selected.label)
.lineLimit(1)
}
.pickerStyle(.menu)
.menuStyle(.borderedButton)
.frame(maxWidth: .infinity, alignment: .leading)
if let annotation = control.snapshot.commandAddressPicker.selected.annotation {
Text("地址类型:\(annotation)")
@@ -232,20 +243,11 @@ struct TerminalProxyEnvironmentCard: View {
}
}
private var commandAddressBinding: Binding<TerminalCommandAddress> {
Binding(
get: { control.snapshot.commandAddressPicker.selected },
set: { address in control.selectCommandAddress(address) })
}
/// 菜单项文案:主行 `{IP} - {name}`;IPv6 类型注记作为第二个 Text——桥接
/// 为 NSMenuItem 的原生 subtitle 次行(macOS 14+),注记缺失时仅主行。
@ViewBuilder
private func menuItemLabel(for candidate: TerminalCommandAddress) -> some View {
Text(candidate.label)
if let annotation = candidate.annotation {
Text(annotation)
}
/// 菜单项副标题:接口名称 + IPv6 类型注记(ifconfig 同源),注记在前便于
/// 先辨认地址类型;无注记时仅接口名称。
private func menuSubtitle(for candidate: TerminalCommandAddress) -> String {
guard let annotation = candidate.annotation else { return candidate.displayName }
return "\(annotation) - \(candidate.displayName)"
}
private func commandButton(for shell: TerminalCommandShell) -> some View {
+1 -1
View File
@@ -22,7 +22,7 @@
| 所有 IPv4 与 IPv6 接口 | IPv4、IPv6,包含两个回环地址 | 127.0.0.1 |
| 仅所有 IPv6 接口 | IPv6 | ::1 |
每项主行显示 `{IP} - {name}`;带内核标志的 IPv6 地址附与 ifconfig 同源的类型注记(如 `autoconf secured`、`dynamic`),呈现为菜单项副标题行(两个 `Text` 桥接为 `NSMenuItem` 的原生 subtitle)与选择器下方的说明行,便于识别地址稳定性。接口名称优先取 `SCNetworkInterfaceGetLocalizedDisplayName(interface)`;名称缺失或无法映射到 SystemConfiguration 接口时,回退到 BSD 接口名,但不会因此放宽接口类型过滤。回环项回退名称为 lo0。
下拉菜单项主行显示 `{IP}`,副标题行显示「注记 - 接口名称」(无注记时仅接口名称;菜单项经 adaptive-controls 机制把两个 `Text` 桥接为 `NSMenuItem` 的原生 title/subtitle,注记与 ifconfig 同源,如 `autoconf secured`、`dynamic`),收起状态显示当前选中项的 `{IP} - {name}`,其下另有类型说明行,便于识别地址稳定性。接口名称优先取 `SCNetworkInterfaceGetLocalizedDisplayName(interface)`;名称缺失或无法映射到 SystemConfiguration 接口时,回退到 BSD 接口名,但不会因此放宽接口类型过滤。回环项回退名称为 lo0。
回环项置顶,双栈时 127.0.0.1 在 ::1 前。其余按接口名称排序,同一接口先 IPv4 后 IPv6,再按地址稳定排序。同一接口的重复 IP 合并;不同接口即使 IP 相同也保留接口身份。