feat(assistant): animate copy confirmation icons
This commit is contained in:
1 parent
0fae7c7fbe
commit
9666308c8e
5 files changed
+104
-9
No files matched your search
@@ -36,7 +36,7 @@ public struct AssistantCompactToolDetails: View {
|
||||
expanded ? AppStrings.Assistant.showLess.swiftUIKey : AppStrings.Assistant.showMore.swiftUIKey
|
||||
) { expanded.toggle() }
|
||||
.accessibilityIdentifier("AssistantTool.More")
|
||||
Button(AppStrings.Common.copy.swiftUIKey, systemImage: "doc.on.doc") { copy(preview.fullText) }
|
||||
AssistantCopyButton(text: preview.fullText, copy: copy)
|
||||
.accessibilityIdentifier("AssistantTool.Copy")
|
||||
}
|
||||
.font(.caption)
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
import SwiftUI
|
||||
|
||||
public struct AssistantCopyButton: View {
|
||||
private let text: String
|
||||
private let minimumLabelSize: CGFloat?
|
||||
private let copy: (String) -> Void
|
||||
@State private var copied = false
|
||||
@State private var copyID = UUID()
|
||||
|
||||
public init(text: String, minimumLabelSize: CGFloat? = nil, copy: @escaping (String) -> Void) {
|
||||
self.text = text
|
||||
self.minimumLabelSize = minimumLabelSize
|
||||
self.copy = copy
|
||||
}
|
||||
|
||||
public var body: some View {
|
||||
Button {
|
||||
copy(text)
|
||||
copied = true
|
||||
copyID = UUID()
|
||||
} label: {
|
||||
Label {
|
||||
Text(AppStrings.Common.copy)
|
||||
} icon: {
|
||||
// Keep the copy glyph's layout footprint when the narrower checkmark appears.
|
||||
Image(systemName: "doc.on.doc")
|
||||
.hidden()
|
||||
.overlay {
|
||||
Image(systemName: copied ? "checkmark" : "doc.on.doc")
|
||||
.contentTransition(.symbolEffect(.replace))
|
||||
}
|
||||
}
|
||||
.frame(minWidth: minimumLabelSize, minHeight: minimumLabelSize)
|
||||
.contentShape(.rect)
|
||||
}
|
||||
.accessibilityValue(Text(copied ? AppStrings.Common.copied : AppStrings.Common.copy))
|
||||
.task(id: copyID) {
|
||||
guard copied else { return }
|
||||
do { try await Task.sleep(for: .seconds(1.4)) } catch { return }
|
||||
copied = false
|
||||
}
|
||||
.onChange(of: text) { _, _ in
|
||||
copied = false
|
||||
copyID = UUID()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -70,7 +70,7 @@ public struct AssistantToolDetails: View {
|
||||
HStack {
|
||||
Text(AppStrings.Assistant.outputShortenedForDisplay).font(.caption2).foregroundStyle(.secondary)
|
||||
if let copy {
|
||||
Button(AppStrings.Common.copy.swiftUIKey, systemImage: "doc.on.doc") { copy(preview.full) }
|
||||
AssistantCopyButton(text: preview.full, copy: copy)
|
||||
.font(.caption).frame(minHeight: 44)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -681,8 +681,8 @@ struct MobileAssistantConversation: View {
|
||||
.foregroundStyle(.red)
|
||||
.textSelection(.enabled)
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
Button(AppStrings.Common.copy.swiftUIKey, systemImage: "doc.on.doc") {
|
||||
UIPasteboard.general.string = failure.reason
|
||||
AssistantCopyButton(text: failure.reason) {
|
||||
UIPasteboard.general.string = $0
|
||||
}
|
||||
.labelStyle(.iconOnly)
|
||||
.buttonStyle(.plain)
|
||||
@@ -809,9 +809,13 @@ struct MobileAssistantConversation: View {
|
||||
}
|
||||
}
|
||||
|
||||
messageAction(AppStrings.Common.copy, systemImage: "doc.on.doc") {
|
||||
UIPasteboard.general.string = message.text
|
||||
AssistantCopyButton(text: message.text, minimumLabelSize: 44) {
|
||||
UIPasteboard.general.string = $0
|
||||
}
|
||||
.font(.caption)
|
||||
.labelStyle(.iconOnly)
|
||||
.buttonStyle(.plain)
|
||||
.foregroundStyle(.secondary)
|
||||
|
||||
if let origin = message.origin {
|
||||
let presentation = MobileAssistantAgentOriginPresentation(
|
||||
|
||||
@@ -2813,8 +2813,26 @@ private class NativeButton: NSButton {
|
||||
@objc private func run() { handler?() }
|
||||
}
|
||||
|
||||
@MainActor
|
||||
private final class NativeCopyButtonCell: NSButtonCell {
|
||||
// The image view draws the symbol so it can use the system replacement transition.
|
||||
// Retain the cell's image and metrics for the existing button layout.
|
||||
override func drawImage(_ image: NSImage, withFrame frame: NSRect, in controlView: NSView) {}
|
||||
}
|
||||
|
||||
@MainActor
|
||||
private final class NativeCopySymbolView: NSImageView {
|
||||
override func hitTest(_ point: NSPoint) -> NSView? { nil }
|
||||
}
|
||||
|
||||
@MainActor
|
||||
private final class NativeCopyButton: NativeButton {
|
||||
override class var cellClass: AnyClass? {
|
||||
get { NativeCopyButtonCell.self }
|
||||
set {}
|
||||
}
|
||||
|
||||
private let symbolView = NativeCopySymbolView()
|
||||
private let copiedImage = NSImage(
|
||||
systemSymbolName: "checkmark",
|
||||
accessibilityDescription: String(localized: AppStrings.Common.copied)
|
||||
@@ -2831,16 +2849,28 @@ private final class NativeCopyButton: NativeButton {
|
||||
image = copyImage
|
||||
imagePosition = .imageOnly
|
||||
toolTip = help
|
||||
setAccessibilityLabel(help)
|
||||
widthAnchor.constraint(equalToConstant: 28).isActive = true
|
||||
heightAnchor.constraint(equalToConstant: 28).isActive = true
|
||||
symbolView.image = copyImage
|
||||
symbolView.imageScaling = .scaleNone
|
||||
symbolView.symbolConfiguration = NSImage.SymbolConfiguration(
|
||||
pointSize: NSFont.systemFontSize(for: controlSize),
|
||||
weight: .regular
|
||||
)
|
||||
symbolView.setAccessibilityElement(false)
|
||||
addSubview(symbolView)
|
||||
handler = { [weak self] in
|
||||
guard copyToPasteboard(text), let self else { return }
|
||||
image = copiedImage
|
||||
showSymbol(copiedImage)
|
||||
toolTip = String(localized: AppStrings.Common.copied)
|
||||
setAccessibilityValue(toolTip)
|
||||
resetWorkItem?.cancel()
|
||||
let work = DispatchWorkItem { [weak self] in
|
||||
self?.image = self?.copyImage
|
||||
self?.toolTip = String(localized: AppStrings.Common.copy)
|
||||
guard let self else { return }
|
||||
showSymbol(copyImage)
|
||||
toolTip = String(localized: AppStrings.Common.copy)
|
||||
setAccessibilityValue(nil)
|
||||
}
|
||||
resetWorkItem = work
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 1.4, execute: work)
|
||||
@@ -2848,4 +2878,18 @@ private final class NativeCopyButton: NativeButton {
|
||||
}
|
||||
@available(*, unavailable)
|
||||
required init?(coder: NSCoder) { fatalError() }
|
||||
|
||||
override func layout() {
|
||||
super.layout()
|
||||
symbolView.frame = bounds
|
||||
}
|
||||
|
||||
private func showSymbol(_ image: NSImage?) {
|
||||
guard let image, symbolView.image !== image else { return }
|
||||
if NSWorkspace.shared.accessibilityDisplayShouldReduceMotion {
|
||||
symbolView.image = image
|
||||
} else {
|
||||
symbolView.setSymbolImage(image, contentTransition: .replace)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user