secretive/Sources/Secretive/Views/CopyableView.swift
Max Goedjen 609adf04bf
Add glass appearance on macOS 26 (#609)
* Accessibility.

* Cleanup
2025-08-17 20:45:07 +00:00

178 lines
5.7 KiB
Swift

import SwiftUI
import UniformTypeIdentifiers
struct CopyableView: View {
var title: LocalizedStringKey
var image: Image
var text: String
@State private var interactionState: InteractionState = .normal
var content: some View {
VStack(alignment: .leading) {
HStack {
image
.renderingMode(.template)
.imageScale(.large)
.foregroundColor(primaryTextColor)
Text(title)
.font(.headline)
.foregroundColor(primaryTextColor)
Spacer()
if interactionState != .normal {
hoverIcon
.bold()
.textCase(.uppercase)
.foregroundColor(secondaryTextColor)
.transition(.opacity)
}
}
.padding(EdgeInsets(top: 20, leading: 20, bottom: 10, trailing: 20))
Divider()
Text(text)
.fixedSize(horizontal: false, vertical: true)
.foregroundColor(primaryTextColor)
.padding(EdgeInsets(top: 10, leading: 20, bottom: 20, trailing: 20))
.multilineTextAlignment(.leading)
.font(.system(.body, design: .monospaced))
}
._background(interactionState: interactionState)
.frame(minWidth: 150, maxWidth: .infinity)
}
var body: some View {
content
.onHover { hovering in
withAnimation {
interactionState = hovering ? .hovering : .normal
}
}
.onDrag({
NSItemProvider(item: NSData(data: text.data(using: .utf8)!), typeIdentifier: UTType.utf8PlainText.identifier)
}, preview: {
content
._background(interactionState: .dragging)
})
.onTapGesture {
copy()
withAnimation {
interactionState = .clicking
}
}
.gesture(
TapGesture()
.onEnded {
withAnimation {
interactionState = .normal
}
}
)
}
@ViewBuilder
var hoverIcon: some View {
switch interactionState {
case .hovering:
Image(systemName: "document.on.document")
.accessibilityLabel(String(localized: "copyable_click_to_copy_button"))
case .clicking:
Image(systemName: "checkmark.circle.fill")
.accessibilityLabel(String(localized: "copyable_copied"))
case .normal, .dragging:
EmptyView()
}
}
var primaryTextColor: Color {
switch interactionState {
case .normal, .hovering, .dragging:
return Color(.textColor)
case .clicking:
return .white
}
}
var secondaryTextColor: Color {
switch interactionState {
case .normal, .hovering, .dragging:
return Color(.secondaryLabelColor)
case .clicking:
return .white
}
}
func copy() {
NSPasteboard.general.declareTypes([.string], owner: nil)
NSPasteboard.general.setString(text, forType: .string)
}
}
fileprivate enum InteractionState {
case normal, hovering, clicking, dragging
}
extension View {
fileprivate func _background(interactionState: InteractionState) -> some View {
modifier(BackgroundViewModifier(interactionState: interactionState))
}
}
fileprivate struct BackgroundViewModifier: ViewModifier {
@Environment(\.colorScheme) private var colorScheme
let interactionState: InteractionState
func body(content: Content) -> some View {
if interactionState == .dragging {
content
.background(backgroundColor(interactionState: interactionState), in: RoundedRectangle(cornerRadius: 15))
} else {
if #available(macOS 26.0, *) {
content
// Very thin opacity lets user hover anywhere over the view, glassEffect doesn't allow.
.background(.white.opacity(0.01), in: RoundedRectangle(cornerRadius: 15))
.glassEffect(.regular.tint(backgroundColor(interactionState: interactionState)), in: RoundedRectangle(cornerRadius: 15))
} else {
content
.background(backgroundColor(interactionState: interactionState))
.cornerRadius(10)
}
}
}
func backgroundColor(interactionState: InteractionState) -> Color {
switch interactionState {
case .normal:
return colorScheme == .dark ? Color(white: 0.2) : Color(white: 0.885)
case .hovering, .dragging:
return colorScheme == .dark ? Color(white: 0.275) : Color(white: 0.82)
case .clicking:
return .accentColor
}
}
}
#if DEBUG
struct CopyableView_Previews: PreviewProvider {
static var previews: some View {
Group {
CopyableView(title: "secret_detail_sha256_fingerprint_label", image: Image(systemName: "figure.wave"), text: "Hello world.")
.padding()
CopyableView(title: "secret_detail_sha256_fingerprint_label", image: Image(systemName: "figure.wave"), text: "Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. ")
.padding()
}
}
}
#endif