import AppKit import SwiftUI // MARK: - The chip row /// A comment's or the draft's `attachments/`, as **chips** (05-card-window.md ▸ The comments column: /// "attachment chips when its `attachments/` is non-empty (Quick Look, the sidebar section's /// pattern)"). /// /// ### Chips, not rows — and the same parts /// /// The sidebar's inventory is a vertical list because it is a *complete* listing of a card's files in /// a narrow column. A comment's files are a handful of things said in passing, so they wrap /// horizontally under the text that mentions them. What does not change is the anatomy — the small /// QuickLook thumbnail with its Finder-icon fallback, the middle-truncated filename, Space/click to /// Quick Look — because that is what "the sidebar section's pattern" names, and a user who has /// learned the sidebar has learned this. /// /// ### The one difference that is a rule /// /// > Chips on an authoring surface carry remove (to the **system** Trash — the sidebar row's rule); a /// > posted comment's chips are read-only, Quick Look only — Edit the comment to change its files. /// /// `onRemove` is that sentence: `nil` is a posted comment's chip and there is no remove affordance at /// all — not a disabled one, because the file is not un-removable, it is simply not removable *here*. struct CommentAttachmentChips: View { let names: [String] /// Where each name lives — the pane resolves it, since only it knows which authoring surface (or /// which posted comment) these belong to. let url: (String) -> URL? let thumbnails: AttachmentThumbnailCache /// `nil` on a posted comment's read-only chips; the remove write on an authoring surface's. var onRemove: ((String) -> Void)? @Environment(\.displayScale) private var displayScale private var pointSize: CGFloat { CardWindowMetrics.bodyPointSize } var body: some View { // Wrapping, because a chip row is horizontal and a comment can carry more files than fit: // `Layout`-free wrapping through a flexible `WrappingHStack` would be a new layout to own, so // this leans on SwiftUI's own — a `LazyVGrid` with adaptive columns wraps and needs nothing. LazyVGrid( columns: [GridItem( .adaptive(minimum: CardWindowMetrics.commentChipMinimumWidth(bodyPointSize: pointSize)), spacing: CardWindowMetrics.previewPadding(bodyPointSize: pointSize), alignment: .leading )], alignment: .leading, spacing: CardWindowMetrics.previewPadding(bodyPointSize: pointSize) ) { ForEach(names, id: \.self) { name in chip(name) } } .frame(maxWidth: .infinity, alignment: .leading) } @ViewBuilder private func chip(_ name: String) -> some View { let fileURL = url(name) let side = CardWindowMetrics.attachmentThumbnailSide(bodyPointSize: pointSize) let padding = CardWindowMetrics.previewPadding(bodyPointSize: pointSize) HStack(spacing: padding) { CommentChipThumbnail( url: fileURL, side: side, thumbnails: thumbnails, displayScale: displayScale ) .frame(width: side, height: side) Text(name) .font(.caption) .lineLimit(1) .truncationMode(.middle) if let onRemove { Button { onRemove(name) } label: { Image(systemName: "xmark.circle.fill") .font(.caption) .foregroundStyle(.secondary) } .buttonStyle(.plain) .help("Remove") .accessibilityLabel("Remove \(name)") } } .padding(.horizontal, padding) .padding(.vertical, padding / 2) .background(.quaternary, in: Capsule(style: .continuous)) .contentShape(Capsule(style: .continuous)) // Quick Look on click, the chip being small enough that a select-then-Space dance would be // ceremony over a thing you can already point at. The panel's ←/→ then walk this surface's // files, exactly as Space over the sidebar walks the card's. .onTapGesture { quickLook(name) } .contextMenu { Button("Open") { guard let fileURL else { return } NSWorkspace.shared.open(fileURL) } Button("Reveal in Finder") { guard let fileURL else { return } NSWorkspace.shared.activateFileViewerSelecting([fileURL]) } if let onRemove { Divider() Button("Remove") { onRemove(name) } } } .help(name) .accessibilityElement(children: .combine) .accessibilityLabel(name) } private func quickLook(_ name: String) { guard let index = names.firstIndex(of: name) else { return } AttachmentQuickLook.shared.toggle(urls: names.compactMap(url), at: index) } } // MARK: - One chip's thumbnail /// The generated QuickLook thumbnail once there is one, the file's Finder icon until then — and /// forever, for anything QuickLook declines. `AttachmentRow`'s own fallback ladder, shared by being /// written the same way rather than by being the same view: the sidebar's row is a row, this is a /// chip, and only the picture is common. private struct CommentChipThumbnail: View { let url: URL? let side: CGFloat let thumbnails: AttachmentThumbnailCache let displayScale: CGFloat private var slot: AttachmentThumbnailKey.Slot? { url.map { AttachmentThumbnailKey.Slot(path: $0.path, side: side) } } var body: some View { content .task(id: url?.path) { guard let slot, let url else { return } await thumbnails.load(slot, url: url, scale: displayScale) } } @ViewBuilder private var content: some View { if let slot, let image = thumbnails.thumbnail(for: slot) { Image(decorative: image, scale: displayScale) .resizable() .aspectRatio(contentMode: .fit) } else if let url { Image(nsImage: thumbnails.icon(forFileAt: url)) .resizable() .aspectRatio(contentMode: .fit) } else { Image(systemName: "doc") .foregroundStyle(.secondary) } } }