Files
web-terminal/ios/App/WebTerm/Components/TerminalContextMenu.swift
Yaojia Wang 823432b1c8 feat(ipad): W1-W3 — adaptive split-view layout + finding fixes
T-iPad-2: AdaptiveRootView/LayoutPolicy (sole size-class decision), SplitRootView
(NavigationSplitView sidebar+detail), StackRootView (iPhone path verbatim, zero
regression); privacy shade hoisted to shared ZStack top for both branches
T-iPad-3: KeyBarVisibility predicate (hide when hardware keyboard present),
pointer context menu (copy/new-in-cwd/kill, all via existing channels)
T-iPad-4: Projects multi-column grid on iPad (idiom-gated), adaptive sheet detents
T-iPad-5 findings (4/4 fixed): kill onKillSession thread-through +
AppCoordinator.killCurrentSession; split route-gated to .sessions (iPad first-run
pairing); continue-last banner in split sidebar; iPad XCUITest deferred (covered
by SidebarSelectionTests)
Verified: iPhone 16 277 + iPad Pro 11 278 tests green; packages 261 + integration 10;
zero changes under ios/Packages, src/, public/
2026-07-05 19:58:30 +02:00

145 lines
5.6 KiB
Swift
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import UIKit
/// T-iPad-3 · /·
///
/// + `UIContextMenuInteraction` **
/// **
/// - `.copySelection` SwiftTerm `copy(_:)`
/// UI hover
/// - `.newInCwd` T-iOS-29 `onNewInCwd` cwd fresh spawn
/// - `.kill` `onKill`wiring `APIClient.killSession`
/// Origin G RO/G
///
/// iPad idiom `isPointerMenuEnabled` iPhone
/// SwiftTerm
/// One context-menu action, each mapped to an existing channel (no new path).
enum TerminalContextAction: String, CaseIterable, Sendable, Equatable {
case copySelection
case newInCwd
case kill
}
/// Display spec for one menu row (Chinese copy + SF Symbol + destructive flag).
struct TerminalContextItem: Equatable, Sendable {
let action: TerminalContextAction
let title: String
let systemImage: String
let isDestructive: Bool
}
enum TerminalContextMenu {
/// named constantsplan §4
enum Copy {
static let copySelection = "复制选区"
static let newInCwd = "在当前目录开新会话"
static let kill = "结束会话"
}
private enum Symbol {
static let copySelection = "doc.on.doc"
static let newInCwd = "plus.rectangle.on.folder"
static let kill = "xmark.circle"
}
/// iPad idiom size class
/// `LayoutPolicy` stack/split iPhone
/// interaction SwiftTerm
static func isPointerMenuEnabled(idiom: UIUserInterfaceIdiom) -> Bool {
idiom == .pad
}
///
static func items(
canCopySelection: Bool,
canNewInCwd: Bool,
canKill: Bool
) -> [TerminalContextItem] {
var result: [TerminalContextItem] = []
if canCopySelection {
result.append(TerminalContextItem(
action: .copySelection, title: Copy.copySelection,
systemImage: Symbol.copySelection, isDestructive: false
))
}
if canNewInCwd {
result.append(TerminalContextItem(
action: .newInCwd, title: Copy.newInCwd,
systemImage: Symbol.newInCwd, isDestructive: false
))
}
if canKill {
result.append(TerminalContextItem(
action: .kill, title: Copy.kill,
systemImage: Symbol.kill, isDestructive: true
))
}
return result
}
}
/// + `items` `perform`
/// wiring `onNewInCwd`T-iOS-29/ `onKill`APIClient.killSession
/// `onCopySelection`/`hasSelection`
@MainActor
struct TerminalContextMenuModel {
let onNewInCwd: (@MainActor () -> Void)?
let onKill: (@MainActor () -> Void)?
let onCopySelection: @MainActor () -> Void
let hasSelection: @MainActor () -> Bool
/// / wiring
/// =
var items: [TerminalContextItem] {
TerminalContextMenu.items(
canCopySelection: hasSelection(),
canNewInCwd: onNewInCwd != nil,
canKill: onKill != nil
)
}
///
func perform(_ action: TerminalContextAction) {
switch action {
case .copySelection:
onCopySelection()
case .newInCwd:
onNewInCwd?()
case .kill:
onKill?()
}
}
}
/// `UIContextMenuInteraction` `makeModel()`
/// / `UIMenu` `UIAction` `model.perform`
/// iPad `TerminalContextMenu.isPointerMenuEnabled`
@MainActor
final class TerminalContextMenuInteractionDelegate: NSObject, UIContextMenuInteractionDelegate {
private let makeModel: @MainActor () -> TerminalContextMenuModel
init(makeModel: @escaping @MainActor () -> TerminalContextMenuModel) {
self.makeModel = makeModel
}
func contextMenuInteraction(
_ interaction: UIContextMenuInteraction,
configurationForMenuAtLocation location: CGPoint
) -> UIContextMenuConfiguration? {
let model = makeModel()
let items = model.items
guard !items.isEmpty else { return nil } //
return UIContextMenuConfiguration(identifier: nil, previewProvider: nil) { _ in
UIMenu(title: "", children: items.map { item in
UIAction(
title: item.title,
image: UIImage(systemName: item.systemImage),
attributes: item.isDestructive ? .destructive : []
) { _ in
model.perform(item.action)
}
})
}
}
}